From 0d33e60261bc97214fbb3aead6c5e30cae1b88a4 Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Mon, 19 Aug 2019 23:42:32 +0200 Subject: dummy: T1580: Python: support {add,remove}_interface in vyos.configinterface --- python/vyos/configinterface.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) (limited to 'python/vyos') diff --git a/python/vyos/configinterface.py b/python/vyos/configinterface.py index 0f5b0842c..843d1c739 100644 --- a/python/vyos/configinterface.py +++ b/python/vyos/configinterface.py @@ -151,3 +151,24 @@ def remove_interface_address(intf, addr): raise ConfigError('{} is not a valid interface address'.format(addr)) pass + +def remove_interface(ifname): + """ + Remove given interface from operating system, e.g. 'dum0' + """ + + if os.path.isdir('/sys/class/net/' + ifname): + os.system('ip link delete "{}"'.format(ifname)) + + pass + +def add_interface(type, ifname): + """ + Add given interface to operating system, e.g. add 'dummy' interface with + name 'dum0' + """ + + if not os.path.isdir('/sys/class/net/' + ifname): + os.system('ip link add "{}" type "{}"'.format(ifname, type)) + + pass -- cgit v1.2.3 From 4bd223a00ee35ff92aff4cb19d08c1c6c9190e10 Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Mon, 19 Aug 2019 23:44:41 +0200 Subject: Python: configinterface: remove debug print() statements --- python/vyos/configinterface.py | 2 -- 1 file changed, 2 deletions(-) (limited to 'python/vyos') diff --git a/python/vyos/configinterface.py b/python/vyos/configinterface.py index 843d1c739..188d5b9e2 100644 --- a/python/vyos/configinterface.py +++ b/python/vyos/configinterface.py @@ -123,11 +123,9 @@ def add_interface_address(intf, addr): os.system('/opt/vyatta/sbin/vyatta-dhcpv6-client.pl --start -ifname "{}"'.format(intf)) elif vyos.validate.is_ipv4(addr): if not vyos.validate.is_intf_addr_assigned(intf, addr): - print("Assigning {} to {}".format(addr, intf)) os.system('sudo ip -4 addr add "{}" broadcast + dev "{}"'.format(addr, intf)) elif vyos.validate.is_ipv6(addr): if not vyos.validate.is_intf_addr_assigned(intf, addr): - print("Assigning {} to {}".format(addr, intf)) os.system('sudo ip -6 addr add "{}" dev "{}"'.format(addr, intf)) else: raise ConfigError('{} is not a valid interface address'.format(addr)) -- cgit v1.2.3 From 4074315da3b651d1c629430cd3bd4e209a80c6bc Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Tue, 20 Aug 2019 10:29:33 +0200 Subject: vyos.configtree: add help for set method --- python/vyos/configtree.py | 8 ++++++++ 1 file changed, 8 insertions(+) (limited to 'python/vyos') diff --git a/python/vyos/configtree.py b/python/vyos/configtree.py index a812b62ec..8832a5a63 100644 --- a/python/vyos/configtree.py +++ b/python/vyos/configtree.py @@ -185,6 +185,14 @@ class ConfigTree(object): return self.__to_commands(self.__config).decode() def set(self, path, value=None, replace=True): + """Set new entry in VyOS configuration. + path: configuration path e.g. 'system dns forwarding listen-address' + value: value to be added to node, e.g. '172.18.254.201' + replace: True: current occurance will be replaced + False: new value will be appended to current occurances - use + this for adding values to a multi node + """ + check_path(path) path_str = " ".join(map(str, path)).encode() -- cgit v1.2.3 From 34e69abedb61b293dee097e67ecda45457da1b75 Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Tue, 20 Aug 2019 11:54:12 +0200 Subject: vyos.interfaces: T1595: add method to query for interface type As of now we only could list the available interfaces for a given interface type. There was no reverse mapping available which told us that interface eth0.201 is an ethernet interface or vtun0 is openvpn. --- python/vyos/interfaces.py | 11 +++++++++++ 1 file changed, 11 insertions(+) (limited to 'python/vyos') diff --git a/python/vyos/interfaces.py b/python/vyos/interfaces.py index 2e8ee4feb..d69ce9d04 100644 --- a/python/vyos/interfaces.py +++ b/python/vyos/interfaces.py @@ -43,3 +43,14 @@ def list_interfaces_of_type(typ): else: r = re.compile('^{0}\d+'.format(types_data[typ])) return list(filter(lambda i: re.match(r, i), all_intfs)) + +def get_type_of_interface(intf): + with open(intf_type_data_file, 'r') as f: + types_data = json.load(f) + + for key,val in types_data.items(): + r = re.compile('^{0}\d+'.format(val)) + if re.match(r, intf): + return key + + raise ValueError("No type found for interface name: {0}".format(intf)) -- cgit v1.2.3 From e5df80a3f54da23858c382a8e491ff9901317e1e Mon Sep 17 00:00:00 2001 From: Daniil Baturin Date: Tue, 20 Aug 2019 18:53:35 +0200 Subject: T1598: initial implementation of the hosts keeper daemon. --- debian/control | 1 + python/vyos/hostsd_client.py | 56 +++++++++ src/services/vyos-hostsd | 254 ++++++++++++++++++++++++++++++++++++++++ src/systemd/vyos-hostsd.service | 22 ++++ src/utils/vyos-hostsd-client | 82 +++++++++++++ 5 files changed, 415 insertions(+) create mode 100644 python/vyos/hostsd_client.py create mode 100755 src/services/vyos-hostsd create mode 100644 src/systemd/vyos-hostsd.service create mode 100755 src/utils/vyos-hostsd-client (limited to 'python/vyos') diff --git a/debian/control b/debian/control index a65d0158e..f46e5d08e 100644 --- a/debian/control +++ b/debian/control @@ -28,6 +28,7 @@ Depends: python3, python3-hurry.filesize, python3-vici (>= 5.7.2), python3-bottle, + python3-zmq, ipaddrcheck, tcpdump, tshark, diff --git a/python/vyos/hostsd_client.py b/python/vyos/hostsd_client.py new file mode 100644 index 000000000..e02aefe6f --- /dev/null +++ b/python/vyos/hostsd_client.py @@ -0,0 +1,56 @@ +import json + +import zmq + + +SOCKET_PATH = "ipc:///run/vyos-hostsd.sock" + + +class VyOSHostsdError(Exception): + pass + + +class Client(object): + def __init__(self): + context = zmq.Context() + self.__socket = context.socket(zmq.REQ) + self.__socket.RCVTIMEO = 10000 #ms + self.__socket.setsockopt(zmq.LINGER, 0) + self.__socket.connect(SOCKET_PATH) + + def _communicate(self, msg): + request = json.dumps(msg).encode() + self.__socket.send(request) + + reply_msg = self.__socket.recv().decode() + reply = json.loads(reply_msg) + if 'error' in reply: + raise VyOSHostsdError(reply['error']) + + def set_host_name(self, host_name, domain_name, search_domains): + msg = { + 'type': 'host_name', + 'op': 'set', + 'data': { + 'host_name': host_name, + 'domain_name': domain_name, + 'search_domains': search_domains + } + } + self._communicate(msg) + + def add_hosts(self, tag, hosts): + msg = {'type': 'hosts', 'op': 'add', 'tag': tag, 'data': hosts} + self._communicate(msg) + + def delete_hosts(self, tag): + msg = {'type': 'hosts', 'op': 'delete', 'tag': tag} + self._communicate(msg) + + def add_name_servers(self, tag, servers): + msg = {'type': 'name_servers', 'op': 'add', 'tag': tag, 'data': servers} + self._communicate(msg) + + def delete_name_servers(self, tag): + msg = {'type': 'name_servers', 'op': 'delete', 'tag': tag} + self._communicate(msg) diff --git a/src/services/vyos-hostsd b/src/services/vyos-hostsd new file mode 100755 index 000000000..972c2e3dc --- /dev/null +++ b/src/services/vyos-hostsd @@ -0,0 +1,254 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2019 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# +# + +import os +import sys +import time +import json +import traceback + +import zmq + +import jinja2 + +debug = True + +DATA_DIR = "/var/lib/vyos/" +STATE_FILE = os.path.join(DATA_DIR, "hostsd.state") + +SOCKET_PATH = "ipc:///run/vyos-hostsd.sock" + +RESOLV_CONF_FILE = '/etc/resolv.conf' +HOSTS_FILE = '/etc/hosts' + +hosts_tmpl_source = """ +### Autogenerated by VyOS ### +127.0.0.1 localhost +127.0.1.1 {{ host_name }}{% if domain_name %}.{{ domain_name }}{% endif %} + +# The following lines are desirable for IPv6 capable hosts +::1 localhost ip6-localhost ip6-loopback +fe00::0 ip6-localnet +ff00::0 ip6-mcastprefix +ff02::1 ip6-allnodes +ff02::2 ip6-allrouters + + +{%- if hosts %} +{% for h in hosts -%} +{{hosts[h]['address']}}\t{{h}}\t{% for a in hosts[h]['aliases'] %} {{a}} {% endfor %} +{% endfor %} +{%- endif %} +""" + +hosts_tmpl = jinja2.Template(hosts_tmpl_source) + +resolv_tmpl_source = """ +### Autogenerated by VyOS ### +{% for ns in name_servers -%} +nameserver {{ns}} +{% endfor -%} + +{%- if domain_name %} +domain {{ domain_name }} +{%- endif %} + +{%- if search_domains %} +search {{ search_domains | join(" ") }} +{%- endif %} + +""" + +resolv_tmpl = jinja2.Template(resolv_tmpl_source) + +# The state data includes a list of name servers +# and a list of hosts entries. +# +# Name servers have the following structure: +# {"server": {"tag": }} +# +# Hosts entries are similar: +# {"host": {"tag": , "address": }} +# +# The tag is either "static" or "dhcp-" +# It's used to distinguish entries created +# by different scripts so that they can be removed +# and re-created without having to track what needs +# to be changed +STATE = { + "name_servers": {}, + "hosts": {}, + "host_name": "vyos", + "domain_name": "", + "search_domains": []} + + +def make_resolv_conf(data): + resolv_conf = resolv_tmpl.render(data) + print("Writing /etc/resolv.conf") + with open(RESOLV_CONF_FILE, 'w') as f: + f.write(resolv_conf) + +def make_hosts_file(state): + print("Writing /etc/hosts") + hosts = hosts_tmpl.render(state) + with open(HOSTS_FILE, 'w') as f: + f.write(hosts) + +def add_hosts(data, entries, tag): + hosts = data['hosts'] + + if not entries: + return + + for e in entries: + host = e['host'] + hosts[host] = {} + hosts[host]['tag'] = tag + hosts[host]['address'] = e['address'] + hosts[host]['aliases'] = e['aliases'] + +def delete_hosts(data, tag): + hosts = data['hosts'] + keys_for_deletion = [] + + # You can't delete items from a dict while iterating over it, + # so we build a list of doomed items first + for h in hosts: + if hosts[h]['tag'] == tag: + keys_for_deletion.append(h) + + for k in keys_for_deletion: + del hosts[k] + +def add_name_servers(data, entries, tag): + name_servers = data['name_servers'] + + if not entries: + return + + for e in entries: + name_servers[e] = {} + name_servers[e]['tag'] = tag + +def delete_name_servers(data, tag): + name_servers = data['name_servers'] + keys_for_deletion = [] + + for ns in name_servers: + if name_servers[ns]['tag'] == tag: + keys_for_deletion.append(ns) + + for k in keys_for_deletion: + del name_servers[k] + +def set_host_name(state, data): + if data['host_name']: + state['host_name'] = data['host_name'] + if data['domain_name']: + state['domain_name'] = data['domain_name'] + if data['search_domains']: + state['search_domains'] = data['search_domains'] + +def get_option(msg, key): + if key in msg: + return msg[key] + else: + raise ValueError("Missing required option \"{0}\"".format(key)) + +def handle_message(msg_json): + msg = json.loads(msg_json) + + op = get_option(msg, 'op') + _type = get_option(msg, 'type') + + if op == 'delete': + tag = get_option(msg, 'tag') + + if _type == 'name_servers': + delete_name_servers(STATE, tag) + elif _type == 'hosts': + delete_hosts(STATE, tag) + else: + raise ValueError("Unknown message type {0}".format(_type)) + elif op == 'add': + tag = get_option(msg, 'tag') + entries = get_option(msg, 'data') + if _type == 'name_servers': + add_name_servers(STATE, entries, tag) + elif _type == 'hosts': + add_hosts(STATE, entries, tag) + else: + raise ValueError("Unknown message type {0}".format(_type)) + elif op == 'set': + # Host name/domain name/search domain are set without a tag, + # there can be only one anyway + data = get_option(msg, 'data') + if _type == 'host_name': + set_host_name(STATE, data) + else: + raise ValueError("Unknown message type {0}".format(_type)) + else: + raise ValueError("Unknown operation {0}".format(op)) + + make_resolv_conf(STATE) + make_hosts_file(STATE) + + print("Saving state to {0}".format(STATE_FILE)) + with open(STATE_FILE, 'w') as f: + json.dump(STATE, f) + + +if __name__ == '__main__': + # Create a directory for state checkpoints + os.makedirs(DATA_DIR, exist_ok=True) + if os.path.exists(STATE_FILE): + with open(STATE_FILE, 'r') as f: + try: + data = json.load(f) + STATE = data + except: + print(traceback.format_exc()) + print("Failed to load the state file, using default") + + context = zmq.Context() + socket = context.socket(zmq.REP) + socket.bind(SOCKET_PATH) + + while True: + # Wait for next request from client + message = socket.recv().decode() + print("Received a configuration change request") + if debug: + print("Request data: {0}".format(message)) + + resp = {} + + try: + handle_message(message) + except ValueError as e: + resp['error'] = str(e) + except: + print(traceback.format_exc()) + resp['error'] = "Internal error" + + if debug: + print("Sent response: {0}".format(resp)) + + # Send reply back to client + socket.send(json.dumps(resp).encode()) diff --git a/src/systemd/vyos-hostsd.service b/src/systemd/vyos-hostsd.service new file mode 100644 index 000000000..fe6c163d7 --- /dev/null +++ b/src/systemd/vyos-hostsd.service @@ -0,0 +1,22 @@ +[Unit] +Description=VyOS DNS configuration keeper +After=auditd.service systemd-user-sessions.service time-sync.target + +[Service] +ExecStart=/usr/bin/python3 -u /usr/libexec/vyos/services/vyos-hostsd +Type=idle +KillMode=process + +SyslogIdentifier=vyos-hostsd +SyslogFacility=daemon + +Restart=on-failure + +# Does't work but leave it here +User=root +Group=vyattacfg + +[Install] +# Installing in a earlier target leaves ExecStartPre waiting +WantedBy=getty.target + diff --git a/src/utils/vyos-hostsd-client b/src/utils/vyos-hostsd-client new file mode 100755 index 000000000..d3105c9cf --- /dev/null +++ b/src/utils/vyos-hostsd-client @@ -0,0 +1,82 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2019 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# +# + +import sys +import argparse + +import vyos.hostsd_client + +parser = argparse.ArgumentParser() +group = parser.add_mutually_exclusive_group() +group.add_argument('--add-hosts', action="store_true") +group.add_argument('--delete-hosts', action="store_true") +group.add_argument('--add-name-servers', action="store_true") +group.add_argument('--delete-name-servers', action="store_true") +group.add_argument('--set-host-name', action="store_true") + +parser.add_argument('--host', type=str, action="append") +parser.add_argument('--name-server', type=str, action="append") +parser.add_argument('--host-name', type=str) +parser.add_argument('--domain-name', type=str) +parser.add_argument('--search-domain', type=str, action="append") + +parser.add_argument('--tag', type=str) + +args = parser.parse_args() + +try: + client = vyos.hostsd_client.Client() + + if args.add_hosts: + if not args.tag: + raise ValueError("Tag is required for this operation") + data = [] + for h in args.host: + entry = {} + params = h.split(",") + if len(params) < 2: + raise ValueError("Malformed host entry") + entry['host'] = params[0] + entry['address'] = params[1] + entry['aliases'] = params[2:] + data.append(entry) + client.add_hosts(args.tag, data) + elif args.delete_hosts: + if not args.tag: + raise ValueError("Tag is required for this operation") + client.delete_hosts(args.tag) + elif args.add_name_servers: + if not args.tag: + raise ValueError("Tag is required for this operation") + client.add_name_servers(args.tag, args.name_server) + elif args.delete_name_servers: + if not args.tag: + raise ValueError("Tag is required for this operation") + client.delete_name_servers(args.tag) + elif args.set_host_name: + client.set_host_name(args.host_name, args.domain_name, args.search_domain) + else: + raise ValueError("Operation required") + +except ValueError as e: + print("Incorrect options: {0}".format(e)) + sys.exit(1) +except vyos.hostsd_client.VyOSHostsdError as e: + print("Server returned an error: {0}".format(e)) + sys.exit(1) + -- cgit v1.2.3 From 683835c1f6020172a270aedc7dcea7f09d5eb208 Mon Sep 17 00:00:00 2001 From: Daniil Baturin Date: Wed, 21 Aug 2019 08:46:14 +0200 Subject: T1598: handle the socket timeout exception in vyos.hostsd_client --- python/vyos/hostsd_client.py | 30 ++++++++++++++++++------------ 1 file changed, 18 insertions(+), 12 deletions(-) (limited to 'python/vyos') diff --git a/python/vyos/hostsd_client.py b/python/vyos/hostsd_client.py index e02aefe6f..e2f05071b 100644 --- a/python/vyos/hostsd_client.py +++ b/python/vyos/hostsd_client.py @@ -12,20 +12,26 @@ class VyOSHostsdError(Exception): class Client(object): def __init__(self): - context = zmq.Context() - self.__socket = context.socket(zmq.REQ) - self.__socket.RCVTIMEO = 10000 #ms - self.__socket.setsockopt(zmq.LINGER, 0) - self.__socket.connect(SOCKET_PATH) + try: + context = zmq.Context() + self.__socket = context.socket(zmq.REQ) + self.__socket.RCVTIMEO = 10000 #ms + self.__socket.setsockopt(zmq.LINGER, 0) + self.__socket.connect(SOCKET_PATH) + except zmq.error.Again: + raise VyOSHostsdError("Could not connect to vyos-hostsd") def _communicate(self, msg): - request = json.dumps(msg).encode() - self.__socket.send(request) - - reply_msg = self.__socket.recv().decode() - reply = json.loads(reply_msg) - if 'error' in reply: - raise VyOSHostsdError(reply['error']) + try: + request = json.dumps(msg).encode() + self.__socket.send(request) + + reply_msg = self.__socket.recv().decode() + reply = json.loads(reply_msg) + if 'error' in reply: + raise VyOSHostsdError(reply['error']) + except zmq.error.Again: + raise VyOSHostsdError("Could not connect to vyos-hostsd") def set_host_name(self, host_name, domain_name, search_domains): msg = { -- cgit v1.2.3 From 0b9c894fcece6df553a89e42147768ce6efaf372 Mon Sep 17 00:00:00 2001 From: hagbard Date: Wed, 21 Aug 2019 16:53:16 +0000 Subject: [interfaceconfig class] - moved get functionaility for mtu, mac and ifalias into its property --- python/vyos/interfaceconfig.py | 24 +++++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) (limited to 'python/vyos') diff --git a/python/vyos/interfaceconfig.py b/python/vyos/interfaceconfig.py index b8bfb707e..54517328a 100644 --- a/python/vyos/interfaceconfig.py +++ b/python/vyos/interfaceconfig.py @@ -46,7 +46,14 @@ class Interface: @property def mtu(self): - return self._mtu + try: + ret = subprocess.check_output(['ip -j link list dev ' + self._ifname], shell=True).decode() + a = json.loads(ret)[0] + return a['mtu'] + except subprocess.CalledProcessError as e: + if self._debug(): + self._debug(e) + return None @mtu.setter def mtu(self, mtu=None): @@ -62,7 +69,14 @@ class Interface: @property def macaddr(self): - return self._macaddr + try: + ret = subprocess.check_output(['ip -j -4 link show dev ' + self._ifname], stderr=subprocess.STDOUT, shell=True).decode() + j = json.loads(ret) + return j[0]['address'] + except subprocess.CalledProcessError as e: + if self._debug(): + self._debug(e) + return None @macaddr.setter def macaddr(self, mac=None): @@ -77,7 +91,7 @@ class Interface: @property def ifalias(self): - return self._ifalias + return open('/sys/class/net/{0}/ifalias'.format(self._ifname),'r').read() @ifalias.setter def ifalias(self, ifalias=None): @@ -116,6 +130,7 @@ class Interface: return False def get_mtu(self): + print ("function get_mtu() is depricated and will be removed soon") try: ret = subprocess.check_output(['ip -j link list dev ' + self._ifname], shell=True).decode() a = json.loads(ret)[0] @@ -125,7 +140,9 @@ class Interface: self._debug(e) return None + def get_macaddr(self): + print ("function get_macaddr() is depricated and will be removed soon") try: ret = subprocess.check_output(['ip -j -4 link show dev ' + self._ifname], stderr=subprocess.STDOUT, shell=True).decode() j = json.loads(ret) @@ -136,6 +153,7 @@ class Interface: return None def get_alias(self): + print ("function get_alias() is depricated and will be removed soon") return open('/sys/class/net/{0}/ifalias'.format(self._ifname),'r').read() def del_alias(self): -- cgit v1.2.3 From cee38e3ed090fcb98ffd49a7c8234060ea9b731f Mon Sep 17 00:00:00 2001 From: hagbard Date: Thu, 22 Aug 2019 02:07:26 +0000 Subject: [interfaceconfig] - linkstate as property and depriccated message for get_link_state() --- python/vyos/interfaceconfig.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) (limited to 'python/vyos') diff --git a/python/vyos/interfaceconfig.py b/python/vyos/interfaceconfig.py index 54517328a..83e7c03c0 100644 --- a/python/vyos/interfaceconfig.py +++ b/python/vyos/interfaceconfig.py @@ -89,6 +89,7 @@ class Interface: if self._debug(): self._debug(e) + @property def ifalias(self): return open('/sys/class/net/{0}/ifalias'.format(self._ifname),'r').read() @@ -101,9 +102,17 @@ class Interface: self._ifalias = str(ifalias) open('/sys/class/net/{0}/ifalias'.format(self._ifname),'w').write(self._ifalias) + @property def linkstate(self): - return self._linkstate + try: + ret = subprocess.check_output(['ip -j link show ' + self._ifname], shell=True).decode() + s = json.loads(ret) + return s[0]['operstate'].lower() + except subprocess.CalledProcessError as e: + if self._debug(): + self._debug(e) + return None @linkstate.setter def linkstate(self, state='up'): @@ -160,9 +169,7 @@ class Interface: open('/sys/class/net/{0}/ifalias'.format(self._ifname),'w').write() def get_link_state(self): - """ - returns either up/down or None if it can't find the state - """ + print ("function get_link_state() is depricated and will be removed soon") try: ret = subprocess.check_output(['ip -j link show ' + self._ifname], shell=True).decode() s = json.loads(ret) -- cgit v1.2.3 From ca1caa6c6effb82b22dad0db4c4f47247c3722ad Mon Sep 17 00:00:00 2001 From: Dmytro Aleksandrov Date: Thu, 22 Aug 2019 17:26:30 +0300 Subject: [op-mode] T1607 rewrite 'reset conntrack', 'reset & show ip[v6]' to python/xml syntax --- Makefile | 4 +- op-mode-definitions/dns-forwarding.xml | 3 + op-mode-definitions/ipv4-route.xml | 125 ++++++++++++++++++++++++++++++ op-mode-definitions/ipv6-route.xml | 133 ++++++++++++++++++++++++++++++++ op-mode-definitions/openvpn.xml | 3 + op-mode-definitions/reset-conntrack.xml | 16 ++++ python/vyos/util.py | 15 ++++ src/op_mode/clear_conntrack.py | 26 +++++++ src/op_mode/powerctrl.py | 17 +--- 9 files changed, 326 insertions(+), 16 deletions(-) create mode 100644 op-mode-definitions/ipv4-route.xml create mode 100644 op-mode-definitions/ipv6-route.xml create mode 100644 op-mode-definitions/reset-conntrack.xml create mode 100755 src/op_mode/clear_conntrack.py (limited to 'python/vyos') diff --git a/Makefile b/Makefile index ee01e5ad3..186e63678 100644 --- a/Makefile +++ b/Makefile @@ -32,7 +32,9 @@ op_mode_definitions: rm -f $(OP_TMPL_DIR)/show/node.def rm -f $(OP_TMPL_DIR)/show/interfaces/node.def rm -f $(OP_TMPL_DIR)/show/ip/node.def - rm -f $(OP_TMPL_DIR)/reset/node.def + rm -f $(OP_TMPL_DIR)/show/ip/route/node.def + rm -f $(OP_TMPL_DIR)/show/ipv6/node.def + rm -f $(OP_TMPL_DIR)/show/ipv6/route/node.def rm -f $(OP_TMPL_DIR)/restart/node.def rm -f $(OP_TMPL_DIR)/monitor/node.def rm -f $(OP_TMPL_DIR)/generate/node.def diff --git a/op-mode-definitions/dns-forwarding.xml b/op-mode-definitions/dns-forwarding.xml index ac141174f..785a05e9c 100644 --- a/op-mode-definitions/dns-forwarding.xml +++ b/op-mode-definitions/dns-forwarding.xml @@ -42,6 +42,9 @@ + + Reset a service + diff --git a/op-mode-definitions/ipv4-route.xml b/op-mode-definitions/ipv4-route.xml new file mode 100644 index 000000000..d2846a6f2 --- /dev/null +++ b/op-mode-definitions/ipv4-route.xml @@ -0,0 +1,125 @@ + + + + + Show system information + + + + + Show IPv4 information + + + + + Show IP multicast group membership + + netstat -gn4 + + + + + Show IP routes + + + + + Show kernel route cache + + ip -s route list cache + + + + Show kernel route cache for a given route + + <x.x.x.x> <x.x.x.x/x> + + + ip -s route list cache $5 + + + + Show kernel route table + + ip route list + + + + Show kernel route table for a given route + + <x.x.x.x> <x.x.x.x/x> + + + ip -s route list $5 + + + + + + + + + + + Reset a service + + + + + Reset Internet Protocol (IP) parameters + + + + + Reset Address Resolution Protocol (ARP) cache + + + + + Reset ARP cache for an IPv4 address + + <x.x.x.x> + + + sudo /sbin/ip neigh flush to "$5" + + + + Reset ARP cache for interface + + + + + sudo /sbin/ip neigh flush dev "$5" + + + + + + + Reset IP route + + + + + Flush the kernel route cache + + sudo /sbin/ip route flush cache + + + + + Flush the kernel route cache for a given route + + <x.x.x.x> <x.x.x.x/x> + + + sudo /sbin/ip route flush cache "$5" + + + + + + + + diff --git a/op-mode-definitions/ipv6-route.xml b/op-mode-definitions/ipv6-route.xml new file mode 100644 index 000000000..fbf6489ba --- /dev/null +++ b/op-mode-definitions/ipv6-route.xml @@ -0,0 +1,133 @@ + + + + + Show system information + + + + + Show IPv6 routing information + + + + + Show IPv6 multicast group membership + + netstat -gn6 + + + + + Show IPv6 Neighbor Discovery (ND) information + + ip -f inet6 neigh list + + + + + Show IPv6 routes + + + + + Show kernel IPv6 route cache + + ip -s -f inet6 route list cache + + + + Show kernel IPv6 route cache for a given route + + <h:h:h:h:h:h:h:h> <h:h:h:h:h:h:h:h/x> + + + ip -s -f inet6 route list cache $5 + + + + Show kernel IPv6 route table + + ip -f inet6 route list + + + + Show kernel IPv6 route table for a given route + + <h:h:h:h:h:h:h:h> <h:h:h:h:h:h:h:h/x> + + + ip -s -f inet6 route list $5 + + + + + + + + + + + + Reset a service + + + + + Reset Internet Protocol version 6 (IPv6) parameters + + + + + Reset IPv6 Neighbor Discovery (ND) cache + + + + + Reset ND cache for an IPv6 address + + <h:h:h:h:h:h:h:h> + + + sudo ip -f inet6 neigh flush to "$5" + + + + Reset IPv6 ND cache for interface + + + + + sudo ip -f inet6 neigh flush dev "$5" + + + + + + + Reset IPv6 route + + + + + Flush the kernel IPv6 route cache + + sudo ip -f inet6 route flush cache + + + + + Flush the kernel IPv6 route cache for a given route + + <h:h:h:h:h:h:h:h> <h:h:h:h:h:h:h:h/x> + + + sudo ip -f inet6 route flush cache "$5" + + + + + + + + diff --git a/op-mode-definitions/openvpn.xml b/op-mode-definitions/openvpn.xml index 4c958257a..ac0c42789 100644 --- a/op-mode-definitions/openvpn.xml +++ b/op-mode-definitions/openvpn.xml @@ -46,6 +46,9 @@ + + Reset a service + diff --git a/op-mode-definitions/reset-conntrack.xml b/op-mode-definitions/reset-conntrack.xml new file mode 100644 index 000000000..827ba4af4 --- /dev/null +++ b/op-mode-definitions/reset-conntrack.xml @@ -0,0 +1,16 @@ + + + + + Reset a service + + + + + Reset all currently tracked connections + + sudo ${vyos_op_scripts_dir}/clear_conntrack.py + + + + diff --git a/python/vyos/util.py b/python/vyos/util.py index 6ab606983..88bb0c8f4 100644 --- a/python/vyos/util.py +++ b/python/vyos/util.py @@ -18,6 +18,7 @@ import re import grp import time import subprocess +import sys import psutil @@ -176,3 +177,17 @@ def wait_for_commit_lock(): while commit_in_progress(): time.sleep(1) +def ask_yes_no(question, default=False) -> bool: + """Ask a yes/no question via input() and return their answer.""" + default_msg = "[Y/n]" if default else "[y/N]" + while True: + sys.stdout.write("%s %s " % (question, default_msg)) + c = input().lower() + if c == '': + return default + elif c in ("y", "ye", "yes"): + return True + elif c in ("n", "no"): + return False + else: + sys.stdout.write("Please respond with yes/y or no/n\n") diff --git a/src/op_mode/clear_conntrack.py b/src/op_mode/clear_conntrack.py new file mode 100755 index 000000000..0e52b9086 --- /dev/null +++ b/src/op_mode/clear_conntrack.py @@ -0,0 +1,26 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2019 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import subprocess +import sys + +from vyos.util import ask_yes_no + +if not ask_yes_no("This will clear all currently tracked and expected connections. Continue?"): + sys.exit(1) +else: + subprocess.check_call(['/usr/sbin/conntrack -F'], shell=True, stderr=subprocess.DEVNULL) + subprocess.check_call(['/usr/sbin/conntrack -F expect'], shell=True, stderr=subprocess.DEVNULL) diff --git a/src/op_mode/powerctrl.py b/src/op_mode/powerctrl.py index 2f6112fb7..e3644e063 100755 --- a/src/op_mode/powerctrl.py +++ b/src/op_mode/powerctrl.py @@ -22,20 +22,7 @@ import re from datetime import datetime, timedelta, time as type_time, date as type_date from subprocess import check_output, CalledProcessError, STDOUT - -def yn(msg, default=False): - default_msg = "[Y/n]" if default else "[y/N]" - while True: - sys.stdout.write("%s %s " % (msg,default_msg)) - c = input().lower() - if c == '': - return default - elif c in ("y", "ye","yes"): - return True - elif c in ("n", "no"): - return False - else: - sys.stdout.write("Please respond with yes/y or no/n\n") +from vyos.util import ask_yes_no def valid_time(s): @@ -80,7 +67,7 @@ def cancel_shutdown(): def execute_shutdown(time, reboot = True, ask=True): if not ask: action = "reboot" if reboot else "poweroff" - if not yn("Are you sure you want to %s this system?" % action): + if not ask_yes_no("Are you sure you want to %s this system?" % action): sys.exit(0) action = "-r" if reboot else "-P" -- cgit v1.2.3 From 84957a3418db23716b9e80f38733ed5e0bd4252e Mon Sep 17 00:00:00 2001 From: DmitriyEshenko Date: Fri, 23 Aug 2019 22:00:57 +0000 Subject: [dummy] T1609 migrate to vyos.interfaceconfig, adding check ip-cidr, adding vyos.interfaceconfig common ipv4/ipv6 functions --- interface-definitions/interfaces-dummy.xml | 3 ++ python/vyos/interfaceconfig.py | 56 ++++++++++++++++++++++++++++++ src/conf_mode/interface-dummy.py | 19 +++++----- 3 files changed, 69 insertions(+), 9 deletions(-) (limited to 'python/vyos') diff --git a/interface-definitions/interfaces-dummy.xml b/interface-definitions/interfaces-dummy.xml index 4450b69b7..c9860fe3b 100644 --- a/interface-definitions/interfaces-dummy.xml +++ b/interface-definitions/interfaces-dummy.xml @@ -28,6 +28,9 @@ IPv6 address and prefix length + + + diff --git a/python/vyos/interfaceconfig.py b/python/vyos/interfaceconfig.py index 83e7c03c0..56e2d515c 100644 --- a/python/vyos/interfaceconfig.py +++ b/python/vyos/interfaceconfig.py @@ -21,6 +21,7 @@ import re import json import socket import subprocess +import ipaddress dhclient_conf_dir = r'/var/lib/dhcp/dhclient_' @@ -188,6 +189,29 @@ class Interface: self._debug(e) return None + def get_addr(self, ret_prefix=None): + """ + universal: reads all IPs assigned to an interface and returns it in a list, + or None if no IP address is assigned to the interface. Also may return + in prefix format if set ret_prefix + """ + ips = [] + try: + ret = subprocess.check_output(['ip -j addr show dev ' + self._ifname], stderr=subprocess.STDOUT, shell=True).decode() + j = json.loads(ret) + for i in j: + if len(i) != 0: + for addr in i['addr_info']: + if ret_prefix: + ips.append(addr['local'] + "/" + str(addr['prefixlen'])) + else: + ips.append(addr['local']) + return ips + except subprocess.CalledProcessError as e: + if self._debug(): + self._debug(e) + return None + def get_ipv4_addr(self): """ reads all IPs assigned to an interface and returns it in a list, @@ -227,6 +251,38 @@ class Interface: self._debug(e) return None + def add_addr(self, ipaddr=[]): + """ + universal: add ipv4/ipv6 addresses on the interface + """ + for ip in ipaddr: + proto = '-4' + if ipaddress.ip_address(ip.split(r'/')[0]).version == 6: + proto = '-6' + + try: + ret = subprocess.check_output(['ip ' + proto + ' address add ' + ip + ' dev ' + self._ifname], stderr=subprocess.STDOUT, shell=True).decode() + except subprocess.CalledProcessError as e: + if self._debug(): + self._debug(e) + return None + return True + + def del_addr(self, ipaddr=[]): + """ + universal: delete ipv4/ipv6 addresses on the interface + """ + for ip in ipaddr: + proto = '-4' + if ipaddress.ip_address(ip.split(r'/')[0]).version == 6: + proto = '-6' + try: + ret = subprocess.check_output(['ip ' + proto + ' address del ' + ip + ' dev ' + self._ifname], stderr=subprocess.STDOUT, shell=True).decode() + except subprocess.CalledProcessError as e: + if self._debug(): + self._debug(e) + return None + return True def add_ipv4_addr(self, ipaddr=[]): """ diff --git a/src/conf_mode/interface-dummy.py b/src/conf_mode/interface-dummy.py index 8c939ce95..c7c5ac99c 100755 --- a/src/conf_mode/interface-dummy.py +++ b/src/conf_mode/interface-dummy.py @@ -20,8 +20,6 @@ import os import sys import copy -import vyos.configinterface as VyIfconfig - from vyos.interfaceconfig import Interface from vyos.config import Config from vyos import ConfigError @@ -86,20 +84,23 @@ def generate(dummy): def apply(dummy): # Remove dummy interface if dummy['deleted']: - VyIfconfig.remove_interface(dummy['intf']) + Interface(dummy['intf']).remove_interface() else: # Interface will only be added if it yet does not exist - VyIfconfig.add_interface('dummy', dummy['intf']) + Interface(dummy['intf'], 'dummy') # update interface description used e.g. within SNMP - VyIfconfig.set_description(dummy['intf'], dummy['description']) + if dummy['description']: + Interface(dummy['intf']).ifalias = dummy['description'] # Configure interface address(es) - for addr in dummy['address_remove']: - VyIfconfig.remove_interface_address(dummy['intf'], addr) + if len(dummy['address_remove']) > 0: + Interface(dummy['intf']).del_addr(dummy['address_remove']) - for addr in dummy['address']: - VyIfconfig.add_interface_address(dummy['intf'], addr) + if len(dummy['address']) > 0: + # delete already existing addreses from list + addresess = diff(dummy['address'], Interface(dummy['intf']).get_addr(1)) + Interface(dummy['intf']).add_addr(addresess) if dummy['disable']: Interface(dummy['intf']).linkstate = 'down' -- cgit v1.2.3 From b812bae9e317f7bcc91371316af28d07345682ba Mon Sep 17 00:00:00 2001 From: Daniil Baturin Date: Mon, 26 Aug 2019 19:58:04 +0200 Subject: T1598: add a vyos-hostsd operation for retrieving name servers by tag. --- python/vyos/hostsd_client.py | 7 +++++++ src/services/vyos-hostsd | 18 +++++++++++++++++- 2 files changed, 24 insertions(+), 1 deletion(-) (limited to 'python/vyos') diff --git a/python/vyos/hostsd_client.py b/python/vyos/hostsd_client.py index e2f05071b..f009aba98 100644 --- a/python/vyos/hostsd_client.py +++ b/python/vyos/hostsd_client.py @@ -30,6 +30,8 @@ class Client(object): reply = json.loads(reply_msg) if 'error' in reply: raise VyOSHostsdError(reply['error']) + else: + return reply["data"] except zmq.error.Again: raise VyOSHostsdError("Could not connect to vyos-hostsd") @@ -60,3 +62,8 @@ class Client(object): def delete_name_servers(self, tag): msg = {'type': 'name_servers', 'op': 'delete', 'tag': tag} self._communicate(msg) + + def get_name_servers(self, tag): + msg = {'type': 'name_servers', 'op': 'get', 'tag': tag} + return self._communicate(msg) + diff --git a/src/services/vyos-hostsd b/src/services/vyos-hostsd index 1bcb2e1f5..8f70eb4e9 100755 --- a/src/services/vyos-hostsd +++ b/src/services/vyos-hostsd @@ -171,6 +171,14 @@ def set_host_name(state, data): if data['search_domains']: state['search_domains'] = data['search_domains'] +def get_name_servers(state, tag): + ns = [] + data = state['name_servers'] + for n in data: + if data[n]['tag'] == tag: + ns.append(n) + return ns + def get_option(msg, key): if key in msg: return msg[key] @@ -209,6 +217,13 @@ def handle_message(msg_json): set_host_name(STATE, data) else: raise ValueError("Unknown message type {0}".format(_type)) + elif op == 'get': + tag = get_option(msg, 'tag') + if _type == 'name_servers': + result = get_name_servers(STATE, tag) + else: + raise ValueError("Unimplemented") + return result else: raise ValueError("Unknown operation {0}".format(op)) @@ -254,7 +269,8 @@ if __name__ == '__main__': resp = {} try: - handle_message(message) + result = handle_message(message) + resp['data'] = result except ValueError as e: resp['error'] = str(e) except: -- cgit v1.2.3 From 10e5e6acf6711149c7502e5fbb3de6a75ede1adf Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Mon, 26 Aug 2019 22:43:05 +0200 Subject: Python: configinterface: remove in favour of pyroute --- python/vyos/configinterface.py | 172 ----------------------------------------- 1 file changed, 172 deletions(-) delete mode 100644 python/vyos/configinterface.py (limited to 'python/vyos') diff --git a/python/vyos/configinterface.py b/python/vyos/configinterface.py deleted file mode 100644 index 188d5b9e2..000000000 --- a/python/vyos/configinterface.py +++ /dev/null @@ -1,172 +0,0 @@ -# Copyright 2019 VyOS maintainers and contributors -# -# This library is free software; you can redistribute it and/or -# modify it under the terms of the GNU Lesser General Public -# License as published by the Free Software Foundation; either -# version 2.1 of the License, or (at your option) any later version. -# -# This library is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -# Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public -# License along with this library. If not, see . - -import os -import vyos.validate - -def validate_mac_address(addr): - # a mac address consits out of 6 octets - octets = len(addr.split(':')) - if octets != 6: - raise ValueError('wrong number of MAC octets: {} '.format(octets)) - - # validate against the first mac address byte if it's a multicast address - if int(addr.split(':')[0]) & 1: - raise ValueError('{} is a multicast MAC address'.format(addr)) - - # overall mac address is not allowed to be 00:00:00:00:00:00 - if sum(int(i, 16) for i in addr.split(':')) == 0: - raise ValueError('00:00:00:00:00:00 is not a valid MAC address') - - # check for VRRP mac address - if addr.split(':')[0] == '0' and addr.split(':')[1] == '0' and addr.split(':')[2] == '94' and addr.split(':')[3] == '0' and addr.split(':')[4] == '1': - raise ValueError('{} is a VRRP MAC address') - - pass - -def set_mac_address(intf, addr): - """ - Configure interface mac address using iproute2 command - """ - validate_mac_address(addr) - - os.system('ip link set {} address {}'.format(intf, addr)) - pass - -def set_description(intf, desc): - """ - Sets the interface secription reported usually by SNMP - """ - with open('/sys/class/net/' + intf + '/ifalias', 'w') as f: - f.write(desc) - - pass - -def set_arp_cache_timeout(intf, tmoMS): - """ - Configure the ARP cache entry timeout in milliseconds - """ - with open('/proc/sys/net/ipv4/neigh/' + intf + '/base_reachable_time_ms', 'w') as f: - f.write(tmoMS) - - pass - -def set_multicast_querier(intf, enable): - """ - Sets whether the bridge actively runs a multicast querier or not. When a - bridge receives a 'multicast host membership' query from another network host, - that host is tracked based on the time that the query was received plus the - multicast query interval time. - - use enable=1 to enable or enable=0 to disable - """ - - if int(enable) >= 0 and int(enable) <= 1: - with open('/sys/devices/virtual/net/' + intf + '/bridge/multicast_querier', 'w') as f: - f.write(str(enable)) - else: - raise ValueError("malformed configuration string on interface {}: enable={}".format(intf, enable)) - - pass - -def set_link_detect(intf, enable): - """ - 0 - Allow packets to be received for the address on this interface - even if interface is disabled or no carrier. - - 1 - Ignore packets received if interface associated with the incoming - address is down. - - 2 - Ignore packets received if interface associated with the incoming - address is down or has no carrier. - - Kernel Source: Documentation/networking/ip-sysctl.txt - """ - - # Note can't use sysctl it is broken for vif name because of dots - # link_filter values: - # 0 - always receive - # 1 - ignore receive if admin_down - # 2 - ignore receive if admin_down or link down - - with open('/proc/sys/net/ipv4/conf/' + intf + '/link_filter', 'w') as f: - if enable == True or enable == 1: - f.write('2') - if os.path.isfile('/usr/bin/vtysh'): - os.system('/usr/bin/vtysh -c "configure terminal" -c "interface {}" -c "link-detect"'.format(intf)) - else: - f.write('1') - if os.path.isfile('/usr/bin/vtysh'): - os.system('/usr/bin/vtysh -c "configure terminal" -c "interface {}" -c "no link-detect"'.format(intf)) - - pass - -def add_interface_address(intf, addr): - """ - Configure an interface IPv4/IPv6 address - """ - if addr == "dhcp": - os.system('/opt/vyatta/sbin/vyatta-interfaces.pl --dev="{}" --dhcp=start'.format(intf)) - elif addr == "dhcpv6": - os.system('/opt/vyatta/sbin/vyatta-dhcpv6-client.pl --start -ifname "{}"'.format(intf)) - elif vyos.validate.is_ipv4(addr): - if not vyos.validate.is_intf_addr_assigned(intf, addr): - os.system('sudo ip -4 addr add "{}" broadcast + dev "{}"'.format(addr, intf)) - elif vyos.validate.is_ipv6(addr): - if not vyos.validate.is_intf_addr_assigned(intf, addr): - os.system('sudo ip -6 addr add "{}" dev "{}"'.format(addr, intf)) - else: - raise ConfigError('{} is not a valid interface address'.format(addr)) - - pass - -def remove_interface_address(intf, addr): - """ - Remove IPv4/IPv6 address from given interface - """ - - if addr == "dhcp": - os.system('/opt/vyatta/sbin/vyatta-interfaces.pl --dev="{}" --dhcp=stop'.format(intf)) - elif addr == "dhcpv6": - os.system('/opt/vyatta/sbin/vyatta-dhcpv6-client.pl --stop -ifname "{}"'.format(intf)) - elif vyos.validate.is_ipv4(addr): - os.system('ip -4 addr del "{}" dev "{}"'.format(addr, intf)) - elif vyos.validate.is_ipv6(addr): - os.system('ip -6 addr del "{}" dev "{}"'.format(addr, intf)) - else: - raise ConfigError('{} is not a valid interface address'.format(addr)) - - pass - -def remove_interface(ifname): - """ - Remove given interface from operating system, e.g. 'dum0' - """ - - if os.path.isdir('/sys/class/net/' + ifname): - os.system('ip link delete "{}"'.format(ifname)) - - pass - -def add_interface(type, ifname): - """ - Add given interface to operating system, e.g. add 'dummy' interface with - name 'dum0' - """ - - if not os.path.isdir('/sys/class/net/' + ifname): - os.system('ip link add "{}" type "{}"'.format(ifname, type)) - - pass -- cgit v1.2.3 From 1ace4a35237889bceff7309df0c687bf32ab89a9 Mon Sep 17 00:00:00 2001 From: John Estabrook Date: Tue, 27 Aug 2019 08:13:02 -0500 Subject: [service https] T1443: Correct the use of listen/server_name directives --- interface-definitions/https.xml | 18 ++++++++++-- python/vyos/defaults.py | 2 +- src/conf_mode/https.py | 61 +++++++++++++++++++++++++++++++++++++---- 3 files changed, 71 insertions(+), 10 deletions(-) (limited to 'python/vyos') diff --git a/interface-definitions/https.xml b/interface-definitions/https.xml index 13d5c43ea..7a87133f3 100644 --- a/interface-definitions/https.xml +++ b/interface-definitions/https.xml @@ -9,7 +9,7 @@ 1001 - + Addresses to listen for HTTPS requests @@ -20,13 +20,25 @@ ipv6 HTTPS IPv6 address - + + '*' + any + + ^\\*$ - + + + + Server names: exact, wildcard, regex, or '_' (any) + + + + + TLS certificates diff --git a/python/vyos/defaults.py b/python/vyos/defaults.py index 3e4c02562..85d27d60d 100644 --- a/python/vyos/defaults.py +++ b/python/vyos/defaults.py @@ -29,7 +29,7 @@ cfg_vintage = 'vyatta' commit_lock = '/opt/vyatta/config/.lock' https_data = { - 'listen_address' : [ '127.0.0.1' ] + 'listen_addresses' : { '*': ['_'] } } api_data = { diff --git a/src/conf_mode/https.py b/src/conf_mode/https.py index 289eacf69..d5aa1f5b3 100755 --- a/src/conf_mode/https.py +++ b/src/conf_mode/https.py @@ -40,12 +40,21 @@ server { return 302 https://$server_name$request_uri; } +{% for addr, names in listen_addresses.items() %} server { # SSL configuration # +{% if addr == '*' %} listen 443 ssl default_server; listen [::]:443 ssl default_server; +{% else %} + listen {{ addr }}:443 ssl; +{% endif %} + +{% for name in names %} + server_name {{ name }}; +{% endfor %} {% if vyos_cert %} include {{ vyos_cert.conf }}; @@ -57,9 +66,42 @@ server { include snippets/snakeoil.conf; {% endif %} -{% for l_addr in listen_address %} - server_name {{ l_addr }}; -{% endfor %} + # proxy settings for HTTP API, if enabled; 503, if not + location ~ /(retrieve|configure) { +{% if api %} + proxy_pass http://localhost:{{ api.port }}; + proxy_buffering off; +{% else %} + return 503; +{% endif %} + } + + error_page 501 502 503 =200 @50*_json; + + location @50*_json { + default_type application/json; + return 200 '{"error": "Start service in configuration mode: set service https api"}'; + } + +} +{% else %} +server { + # SSL configuration + # + listen 443 ssl default_server; + listen [::]:443 ssl default_server; + + server_name _; + +{% if vyos_cert %} + include {{ vyos_cert.conf }}; +{% else %} + # + # Self signed certs generated by the ssl-cert package + # Don't use them in a production server! + # + include snippets/snakeoil.conf; +{% endif %} # proxy settings for HTTP API, if enabled; 503, if not location ~ /(retrieve|configure) { @@ -79,6 +121,8 @@ server { } } + +{% endfor %} """ def get_config(): @@ -89,9 +133,14 @@ def get_config(): else: conf.set_level('service https') - if conf.exists('listen-address'): - addrs = conf.return_values('listen-address') - https['listen_address'] = addrs[:] + if conf.exists('listen-addresses'): + addrs = {} + for addr in conf.list_nodes('listen-addresses'): + addrs[addr] = ['_'] + if conf.exists('listen-addresses {0} server-names'.format(addr)): + names = conf.return_values('listen-addresses {0} server-names'.format(addr)) + addrs[addr] = names[:] + https['listen_addresses'] = addrs if conf.exists('certificates'): if conf.exists('certificates system-generated-certificate'): -- cgit v1.2.3 From 34adc76f589cb01406155007ce49c5855764c79c Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Tue, 27 Aug 2019 19:49:18 +0200 Subject: Python/VyOS validate: add is_ip() to check for IPv4 or IPv4 address --- python/vyos/validate.py | 6 ++++++ 1 file changed, 6 insertions(+) (limited to 'python/vyos') diff --git a/python/vyos/validate.py b/python/vyos/validate.py index 97a401423..258f7f76a 100644 --- a/python/vyos/validate.py +++ b/python/vyos/validate.py @@ -16,6 +16,12 @@ import netifaces import ipaddress +def is_ip(addr): + """ + Check addr if it is an IPv4 or IPv6 address + """ + return is_ipv4(addr) or is_ipv6(addr) + def is_ipv4(addr): """ Check addr if it is an IPv4 address/network. Returns True/False -- cgit v1.2.3 From c58c276923220c4da9a1c3ca2f3f445dc5bcd0a5 Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Fri, 30 Aug 2019 10:32:18 +0200 Subject: Python/ifconfig: remove trailing whitespaces --- python/vyos/interfaceconfig.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) (limited to 'python/vyos') diff --git a/python/vyos/interfaceconfig.py b/python/vyos/interfaceconfig.py index 56e2d515c..30188166a 100644 --- a/python/vyos/interfaceconfig.py +++ b/python/vyos/interfaceconfig.py @@ -30,7 +30,7 @@ class Interface: if not ifname: raise Exception("interface name required") if not os.path.exists('/sys/class/net/{0}'.format(ifname)) and not type: - raise Exception("interface {0} not found".format(str(ifname))) + raise Exception("interface {0} not found".format(str(ifname))) else: if not os.path.exists('/sys/class/net/{0}'.format(ifname)): try: @@ -100,7 +100,7 @@ class Interface: if not ifalias: self._ifalias = self._ifname else: - self._ifalias = str(ifalias) + self._ifalias = str(ifalias) open('/sys/class/net/{0}/ifalias'.format(self._ifname),'w').write(self._ifalias) @@ -130,12 +130,12 @@ class Interface: def _debug(self, e=None): - """ + """ export DEBUG=1 to see debug messages """ if os.getenv('DEBUG') == '1': if e: - print ("Exception raised:\ncommand: {0}\nerror code: {1}\nsubprocess output: {2}".format(e.cmd, e.returncode, e.output.decode()) ) + print ("Exception raised:\ncommand: {0}\nerror code: {1}\nsubprocess output: {2}".format(e.cmd, e.returncode, e.output.decode()) ) return True return False @@ -214,7 +214,7 @@ class Interface: def get_ipv4_addr(self): """ - reads all IPs assigned to an interface and returns it in a list, + reads all IPs assigned to an interface and returns it in a list, or None if no IP address is assigned to the interface """ ips = [] @@ -346,13 +346,13 @@ class Interface: pidfile = dhclient_conf_dir + self._ifname + '.pid' leasefile = dhclient_conf_dir + self._ifname + '.leases' - a = [ + a = [ '# generated by interface_config.py', 'option rfc3442-classless-static-routes code 121 = array of unsigned integer 8;', 'interface \"' + self._ifname + '\" {', '\tsend host-name \"' + socket.gethostname() +'\";', '\trequest subnet-mask, broadcast-address, routers, domain-name-servers, rfc3442-classless-static-routes, domain-name, interface-mtu;', - '}' + '}' ] cnf = "" @@ -378,7 +378,7 @@ class Interface: pidfile = dhclient_conf_dir + self._ifname + '.pid' leasefile = dhclient_conf_dir + self._ifname + '.leases' if not os.path.exists(pidfile): - return 1 + return 1 try: ret = subprocess.check_output(['/sbin/dhclient -4 -r -pf ' + pidfile], shell=True).decode() return True @@ -453,5 +453,5 @@ class Interface: return True -#### TODO: dhcpv6-pd via dhclient +#### TODO: dhcpv6-pd via dhclient -- cgit v1.2.3 From 4057159c949f8caa5078e984b8d521850a1b73be Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Fri, 30 Aug 2019 10:39:43 +0200 Subject: Python/ifconfig: re-indent with 4 spaces for better readability Command user: $ autopep8 python/vyos/interfaceconfig.py --in-place --- python/vyos/interfaceconfig.py | 872 +++++++++++++++++++++-------------------- 1 file changed, 448 insertions(+), 424 deletions(-) (limited to 'python/vyos') diff --git a/python/vyos/interfaceconfig.py b/python/vyos/interfaceconfig.py index 30188166a..e354d900d 100644 --- a/python/vyos/interfaceconfig.py +++ b/python/vyos/interfaceconfig.py @@ -25,433 +25,457 @@ import ipaddress dhclient_conf_dir = r'/var/lib/dhcp/dhclient_' + class Interface: - def __init__(self, ifname=None, type=None): - if not ifname: - raise Exception("interface name required") - if not os.path.exists('/sys/class/net/{0}'.format(ifname)) and not type: - raise Exception("interface {0} not found".format(str(ifname))) - else: - if not os.path.exists('/sys/class/net/{0}'.format(ifname)): + + def __init__(self, ifname=None, type=None): + if not ifname: + raise Exception("interface name required") + if not os.path.exists('/sys/class/net/{0}'.format(ifname)) and not type: + raise Exception("interface {0} not found".format(str(ifname))) + else: + if not os.path.exists('/sys/class/net/{0}'.format(ifname)): + try: + ret = subprocess.check_output( + ['ip link add dev ' + str(ifname) + ' type ' + type], stderr=subprocess.STDOUT, shell=True).decode() + except subprocess.CalledProcessError as e: + if self._debug(): + self._debug(e) + if "Operation not supported" in str(e.output.decode()): + print(str(e.output.decode())) + sys.exit(0) + + self._ifname = str(ifname) + + @property + def mtu(self): try: - ret = subprocess.check_output(['ip link add dev ' + str(ifname) + ' type ' + type], stderr=subprocess.STDOUT, shell=True).decode() + ret = subprocess.check_output( + ['ip -j link list dev ' + self._ifname], shell=True).decode() + a = json.loads(ret)[0] + return a['mtu'] except subprocess.CalledProcessError as e: - if self._debug(): - self._debug(e) - if "Operation not supported" in str(e.output.decode()): - print(str(e.output.decode())) - sys.exit(0) - - self._ifname = str(ifname) - - - @property - def mtu(self): - try: - ret = subprocess.check_output(['ip -j link list dev ' + self._ifname], shell=True).decode() - a = json.loads(ret)[0] - return a['mtu'] - except subprocess.CalledProcessError as e: - if self._debug(): - self._debug(e) - return None - - @mtu.setter - def mtu(self, mtu=None): - if mtu < 68 or mtu > 9000: - raise ValueError("mtu size invalid value") - self._mtu = mtu - try: - ret = subprocess.check_output(['ip link set mtu ' + str(mtu) + ' dev ' + self._ifname], shell=True).decode() - except subprocess.CalledProcessError as e: - if self._debug(): - self._debug(e) - - - @property - def macaddr(self): - try: - ret = subprocess.check_output(['ip -j -4 link show dev ' + self._ifname], stderr=subprocess.STDOUT, shell=True).decode() - j = json.loads(ret) - return j[0]['address'] - except subprocess.CalledProcessError as e: - if self._debug(): - self._debug(e) - return None - - @macaddr.setter - def macaddr(self, mac=None): - if not re.search('^[a-f0-9:]{17}$', str(mac)): - raise ValueError("mac address invalid") - self._macaddr = str(mac) - try: - ret = subprocess.check_output(['ip link set address ' + mac + ' ' + self._ifname], shell=True).decode() - except subprocess.CalledProcessError as e: - if self._debug(): - self._debug(e) - - - @property - def ifalias(self): - return open('/sys/class/net/{0}/ifalias'.format(self._ifname),'r').read() - - @ifalias.setter - def ifalias(self, ifalias=None): - if not ifalias: - self._ifalias = self._ifname - else: - self._ifalias = str(ifalias) - open('/sys/class/net/{0}/ifalias'.format(self._ifname),'w').write(self._ifalias) - - - @property - def linkstate(self): - try: - ret = subprocess.check_output(['ip -j link show ' + self._ifname], shell=True).decode() - s = json.loads(ret) - return s[0]['operstate'].lower() - except subprocess.CalledProcessError as e: - if self._debug(): - self._debug(e) - return None - - @linkstate.setter - def linkstate(self, state='up'): - if str(state).lower() == 'up' or str(state).lower() == 'down': - self._linkstate = str(state).lower() - else: - self._linkstate = 'up' - try: - ret = subprocess.check_output(['ip link set dev ' + self._ifname + ' ' + state], shell=True).decode() - except subprocess.CalledProcessError as e: - if self._debug(): - self._debug(e) - - - - def _debug(self, e=None): - """ - export DEBUG=1 to see debug messages - """ - if os.getenv('DEBUG') == '1': - if e: - print ("Exception raised:\ncommand: {0}\nerror code: {1}\nsubprocess output: {2}".format(e.cmd, e.returncode, e.output.decode()) ) - return True - return False - - def get_mtu(self): - print ("function get_mtu() is depricated and will be removed soon") - try: - ret = subprocess.check_output(['ip -j link list dev ' + self._ifname], shell=True).decode() - a = json.loads(ret)[0] - return a['mtu'] - except subprocess.CalledProcessError as e: - if self._debug(): - self._debug(e) - return None - - - def get_macaddr(self): - print ("function get_macaddr() is depricated and will be removed soon") - try: - ret = subprocess.check_output(['ip -j -4 link show dev ' + self._ifname], stderr=subprocess.STDOUT, shell=True).decode() - j = json.loads(ret) - return j[0]['address'] - except subprocess.CalledProcessError as e: - if self._debug(): - self._debug(e) - return None - - def get_alias(self): - print ("function get_alias() is depricated and will be removed soon") - return open('/sys/class/net/{0}/ifalias'.format(self._ifname),'r').read() - - def del_alias(self): - open('/sys/class/net/{0}/ifalias'.format(self._ifname),'w').write() - - def get_link_state(self): - print ("function get_link_state() is depricated and will be removed soon") - try: - ret = subprocess.check_output(['ip -j link show ' + self._ifname], shell=True).decode() - s = json.loads(ret) - return s[0]['operstate'].lower() - except subprocess.CalledProcessError as e: - if self._debug(): - self._debug(e) - return None - - def remove_interface(self): - try: - ret = subprocess.check_output(['ip link del dev ' + self._ifname], shell=True).decode() - return 0 - except subprocess.CalledProcessError as e: - if self._debug(): - self._debug(e) - return None - - def get_addr(self, ret_prefix=None): - """ - universal: reads all IPs assigned to an interface and returns it in a list, - or None if no IP address is assigned to the interface. Also may return - in prefix format if set ret_prefix - """ - ips = [] - try: - ret = subprocess.check_output(['ip -j addr show dev ' + self._ifname], stderr=subprocess.STDOUT, shell=True).decode() - j = json.loads(ret) - for i in j: - if len(i) != 0: - for addr in i['addr_info']: - if ret_prefix: - ips.append(addr['local'] + "/" + str(addr['prefixlen'])) - else: - ips.append(addr['local']) - return ips - except subprocess.CalledProcessError as e: - if self._debug(): - self._debug(e) - return None - - def get_ipv4_addr(self): - """ - reads all IPs assigned to an interface and returns it in a list, - or None if no IP address is assigned to the interface - """ - ips = [] - try: - ret = subprocess.check_output(['ip -j -4 addr show dev ' + self._ifname], stderr=subprocess.STDOUT, shell=True).decode() - j = json.loads(ret) - for i in j: - if len(i) != 0: - for addr in i['addr_info']: - ips.append(addr['local']) - return ips - except subprocess.CalledProcessError as e: - if self._debug(): - self._debug(e) - return None - - - def get_ipv6_addr(self): - """ - reads all IPs assigned to an interface and returns it in a list, - or None if no IP address is assigned to the interface - """ - ips = [] - try: - ret = subprocess.check_output(['ip -j -6 addr show dev ' + self._ifname], stderr=subprocess.STDOUT, shell=True).decode() - j = json.loads(ret) - for i in j: - if len(i) != 0: - for addr in i['addr_info']: - ips.append(addr['local']) - return ips - except subprocess.CalledProcessError as e: - if self._debug(): - self._debug(e) - return None - - def add_addr(self, ipaddr=[]): - """ - universal: add ipv4/ipv6 addresses on the interface - """ - for ip in ipaddr: - proto = '-4' - if ipaddress.ip_address(ip.split(r'/')[0]).version == 6: - proto = '-6' - - try: - ret = subprocess.check_output(['ip ' + proto + ' address add ' + ip + ' dev ' + self._ifname], stderr=subprocess.STDOUT, shell=True).decode() - except subprocess.CalledProcessError as e: - if self._debug(): - self._debug(e) - return None - return True - - def del_addr(self, ipaddr=[]): - """ - universal: delete ipv4/ipv6 addresses on the interface - """ - for ip in ipaddr: - proto = '-4' - if ipaddress.ip_address(ip.split(r'/')[0]).version == 6: - proto = '-6' - try: - ret = subprocess.check_output(['ip ' + proto + ' address del ' + ip + ' dev ' + self._ifname], stderr=subprocess.STDOUT, shell=True).decode() - except subprocess.CalledProcessError as e: - if self._debug(): - self._debug(e) - return None - return True - - def add_ipv4_addr(self, ipaddr=[]): - """ - add addresses on the interface - """ - for ip in ipaddr: - try: - ret = subprocess.check_output(['ip -4 address add ' + ip + ' dev ' + self._ifname], stderr=subprocess.STDOUT, shell=True).decode() - except subprocess.CalledProcessError as e: - if self._debug(): - self._debug(e) - return None - return True - - - def del_ipv4_addr(self, ipaddr=[]): - """ - delete addresses on the interface - """ - for ip in ipaddr: - try: - ret = subprocess.check_output(['ip -4 address del ' + ip + ' dev ' + self._ifname], stderr=subprocess.STDOUT, shell=True).decode() - except subprocess.CalledProcessError as e: - if self._debug(): - self._debug(e) - return None - return True - - - def add_ipv6_addr(self, ipaddr=[]): - """ - add addresses on the interface - """ - for ip in ipaddr: - try: - ret = subprocess.check_output(['ip -6 address add ' + ip + ' dev ' + self._ifname], stderr=subprocess.STDOUT, shell=True).decode() - except subprocess.CalledProcessError as e: - if self._debug(): - self._debug(e) - return None - return True - - - def del_ipv6_addr(self, ipaddr=[]): - """ - delete addresses on the interface - """ - for ip in ipaddr: - try: - ret = subprocess.check_output(['ip -6 address del ' + ip + ' dev ' + self._ifname], stderr=subprocess.STDOUT, shell=True).decode() - except subprocess.CalledProcessError as e: - if self._debug(): - self._debug(e) - return None - return True - - - #### replace dhcpv4/v6 with systemd.networkd? - def set_dhcpv4(self): - conf_file = dhclient_conf_dir + self._ifname + '.conf' - pidfile = dhclient_conf_dir + self._ifname + '.pid' - leasefile = dhclient_conf_dir + self._ifname + '.leases' - - a = [ - '# generated by interface_config.py', - 'option rfc3442-classless-static-routes code 121 = array of unsigned integer 8;', - 'interface \"' + self._ifname + '\" {', - '\tsend host-name \"' + socket.gethostname() +'\";', - '\trequest subnet-mask, broadcast-address, routers, domain-name-servers, rfc3442-classless-static-routes, domain-name, interface-mtu;', - '}' - ] + if self._debug(): + self._debug(e) + return None + + @mtu.setter + def mtu(self, mtu=None): + if mtu < 68 or mtu > 9000: + raise ValueError("mtu size invalid value") + self._mtu = mtu + try: + ret = subprocess.check_output( + ['ip link set mtu ' + str(mtu) + ' dev ' + self._ifname], shell=True).decode() + except subprocess.CalledProcessError as e: + if self._debug(): + self._debug(e) + + @property + def macaddr(self): + try: + ret = subprocess.check_output( + ['ip -j -4 link show dev ' + self._ifname], stderr=subprocess.STDOUT, shell=True).decode() + j = json.loads(ret) + return j[0]['address'] + except subprocess.CalledProcessError as e: + if self._debug(): + self._debug(e) + return None + + @macaddr.setter + def macaddr(self, mac=None): + if not re.search('^[a-f0-9:]{17}$', str(mac)): + raise ValueError("mac address invalid") + self._macaddr = str(mac) + try: + ret = subprocess.check_output( + ['ip link set address ' + mac + ' ' + self._ifname], shell=True).decode() + except subprocess.CalledProcessError as e: + if self._debug(): + self._debug(e) + + @property + def ifalias(self): + return open('/sys/class/net/{0}/ifalias'.format(self._ifname), 'r').read() + + @ifalias.setter + def ifalias(self, ifalias=None): + if not ifalias: + self._ifalias = self._ifname + else: + self._ifalias = str(ifalias) + open('/sys/class/net/{0}/ifalias'.format( + self._ifname), 'w').write(self._ifalias) + + @property + def linkstate(self): + try: + ret = subprocess.check_output( + ['ip -j link show ' + self._ifname], shell=True).decode() + s = json.loads(ret) + return s[0]['operstate'].lower() + except subprocess.CalledProcessError as e: + if self._debug(): + self._debug(e) + return None + + @linkstate.setter + def linkstate(self, state='up'): + if str(state).lower() == 'up' or str(state).lower() == 'down': + self._linkstate = str(state).lower() + else: + self._linkstate = 'up' + try: + ret = subprocess.check_output( + ['ip link set dev ' + self._ifname + ' ' + state], shell=True).decode() + except subprocess.CalledProcessError as e: + if self._debug(): + self._debug(e) + + def _debug(self, e=None): + """ + export DEBUG=1 to see debug messages + """ + if os.getenv('DEBUG') == '1': + if e: + print ("Exception raised:\ncommand: {0}\nerror code: {1}\nsubprocess output: {2}".format( + e.cmd, e.returncode, e.output.decode())) + return True + return False + + def get_mtu(self): + print ("function get_mtu() is depricated and will be removed soon") + try: + ret = subprocess.check_output( + ['ip -j link list dev ' + self._ifname], shell=True).decode() + a = json.loads(ret)[0] + return a['mtu'] + except subprocess.CalledProcessError as e: + if self._debug(): + self._debug(e) + return None + + def get_macaddr(self): + print ("function get_macaddr() is depricated and will be removed soon") + try: + ret = subprocess.check_output( + ['ip -j -4 link show dev ' + self._ifname], stderr=subprocess.STDOUT, shell=True).decode() + j = json.loads(ret) + return j[0]['address'] + except subprocess.CalledProcessError as e: + if self._debug(): + self._debug(e) + return None + + def get_alias(self): + print ("function get_alias() is depricated and will be removed soon") + return open('/sys/class/net/{0}/ifalias'.format(self._ifname), 'r').read() + + def del_alias(self): + open('/sys/class/net/{0}/ifalias'.format(self._ifname), 'w').write() - cnf = "" - for ln in a: - cnf +=str(ln + "\n") - open(conf_file, 'w').write(cnf) - if os.path.exists(dhclient_conf_dir + self._ifname + '.pid'): - try: - ret = subprocess.check_output(['/sbin/dhclient -4 -r -pf ' + pidfile], shell=True).decode() - except subprocess.CalledProcessError as e: - if self._debug(): - self._debug(e) - try: - ret = subprocess.check_output(['/sbin/dhclient -4 -q -nw -cf ' + conf_file + ' -pf ' + pidfile + ' -lf ' + leasefile + ' ' + self._ifname], shell=True).decode() - return True - except subprocess.CalledProcessError as e: - if self._debug(): - self._debug(e) - return None - - def del_dhcpv4(self): - conf_file = dhclient_conf_dir + self._ifname + '.conf' - pidfile = dhclient_conf_dir + self._ifname + '.pid' - leasefile = dhclient_conf_dir + self._ifname + '.leases' - if not os.path.exists(pidfile): - return 1 - try: - ret = subprocess.check_output(['/sbin/dhclient -4 -r -pf ' + pidfile], shell=True).decode() - return True - except subprocess.CalledProcessError as e: - if self._debug(): - self._debug(e) - return None - - def get_dhcpv4(self): - pidfile = dhclient_conf_dir + self._ifname + '.pid' - if not os.path.exists(pidfile): - print ("no dhcp client running on interface {0}".format(self._ifname)) - return False - else: - pid = open(pidfile, 'r').read() - print("dhclient running on {0} with pid {1}".format(self._ifname, pid)) - return True - - - def set_dhcpv6(self): - conf_file = dhclient_conf_dir + self._ifname + '.v6conf' - pidfile = dhclient_conf_dir + self._ifname + '.v6pid' - leasefile = dhclient_conf_dir + self._ifname + '.v6leases' - a = [ - '# generated by interface_config.py', - 'interface \"' + self._ifname + '\" {', - '\trequest routers, domain-name-servers, domain-name;', - '}' + def get_link_state(self): + print ( + "function get_link_state() is depricated and will be removed soon") + try: + ret = subprocess.check_output( + ['ip -j link show ' + self._ifname], shell=True).decode() + s = json.loads(ret) + return s[0]['operstate'].lower() + except subprocess.CalledProcessError as e: + if self._debug(): + self._debug(e) + return None + + def remove_interface(self): + try: + ret = subprocess.check_output( + ['ip link del dev ' + self._ifname], shell=True).decode() + return 0 + except subprocess.CalledProcessError as e: + if self._debug(): + self._debug(e) + return None + + def get_addr(self, ret_prefix=None): + """ + universal: reads all IPs assigned to an interface and returns it in a list, + or None if no IP address is assigned to the interface. Also may return + in prefix format if set ret_prefix + """ + ips = [] + try: + ret = subprocess.check_output( + ['ip -j addr show dev ' + self._ifname], stderr=subprocess.STDOUT, shell=True).decode() + j = json.loads(ret) + for i in j: + if len(i) != 0: + for addr in i['addr_info']: + if ret_prefix: + ips.append( + addr['local'] + "/" + str(addr['prefixlen'])) + else: + ips.append(addr['local']) + return ips + except subprocess.CalledProcessError as e: + if self._debug(): + self._debug(e) + return None + + def get_ipv4_addr(self): + """ + reads all IPs assigned to an interface and returns it in a list, + or None if no IP address is assigned to the interface + """ + ips = [] + try: + ret = subprocess.check_output( + ['ip -j -4 addr show dev ' + self._ifname], stderr=subprocess.STDOUT, shell=True).decode() + j = json.loads(ret) + for i in j: + if len(i) != 0: + for addr in i['addr_info']: + ips.append(addr['local']) + return ips + except subprocess.CalledProcessError as e: + if self._debug(): + self._debug(e) + return None + + def get_ipv6_addr(self): + """ + reads all IPs assigned to an interface and returns it in a list, + or None if no IP address is assigned to the interface + """ + ips = [] + try: + ret = subprocess.check_output( + ['ip -j -6 addr show dev ' + self._ifname], stderr=subprocess.STDOUT, shell=True).decode() + j = json.loads(ret) + for i in j: + if len(i) != 0: + for addr in i['addr_info']: + ips.append(addr['local']) + return ips + except subprocess.CalledProcessError as e: + if self._debug(): + self._debug(e) + return None + + def add_addr(self, ipaddr=[]): + """ + universal: add ipv4/ipv6 addresses on the interface + """ + for ip in ipaddr: + proto = '-4' + if ipaddress.ip_address(ip.split(r'/')[0]).version == 6: + proto = '-6' + + try: + ret = subprocess.check_output( + ['ip ' + proto + ' address add ' + ip + ' dev ' + self._ifname], stderr=subprocess.STDOUT, shell=True).decode() + except subprocess.CalledProcessError as e: + if self._debug(): + self._debug(e) + return None + return True + + def del_addr(self, ipaddr=[]): + """ + universal: delete ipv4/ipv6 addresses on the interface + """ + for ip in ipaddr: + proto = '-4' + if ipaddress.ip_address(ip.split(r'/')[0]).version == 6: + proto = '-6' + try: + ret = subprocess.check_output( + ['ip ' + proto + ' address del ' + ip + ' dev ' + self._ifname], stderr=subprocess.STDOUT, shell=True).decode() + except subprocess.CalledProcessError as e: + if self._debug(): + self._debug(e) + return None + return True + + def add_ipv4_addr(self, ipaddr=[]): + """ + add addresses on the interface + """ + for ip in ipaddr: + try: + ret = subprocess.check_output( + ['ip -4 address add ' + ip + ' dev ' + self._ifname], stderr=subprocess.STDOUT, shell=True).decode() + except subprocess.CalledProcessError as e: + if self._debug(): + self._debug(e) + return None + return True + + def del_ipv4_addr(self, ipaddr=[]): + """ + delete addresses on the interface + """ + for ip in ipaddr: + try: + ret = subprocess.check_output( + ['ip -4 address del ' + ip + ' dev ' + self._ifname], stderr=subprocess.STDOUT, shell=True).decode() + except subprocess.CalledProcessError as e: + if self._debug(): + self._debug(e) + return None + return True + + def add_ipv6_addr(self, ipaddr=[]): + """ + add addresses on the interface + """ + for ip in ipaddr: + try: + ret = subprocess.check_output( + ['ip -6 address add ' + ip + ' dev ' + self._ifname], stderr=subprocess.STDOUT, shell=True).decode() + except subprocess.CalledProcessError as e: + if self._debug(): + self._debug(e) + return None + return True + + def del_ipv6_addr(self, ipaddr=[]): + """ + delete addresses on the interface + """ + for ip in ipaddr: + try: + ret = subprocess.check_output( + ['ip -6 address del ' + ip + ' dev ' + self._ifname], stderr=subprocess.STDOUT, shell=True).decode() + except subprocess.CalledProcessError as e: + if self._debug(): + self._debug(e) + return None + return True + + # replace dhcpv4/v6 with systemd.networkd? + def set_dhcpv4(self): + conf_file = dhclient_conf_dir + self._ifname + '.conf' + pidfile = dhclient_conf_dir + self._ifname + '.pid' + leasefile = dhclient_conf_dir + self._ifname + '.leases' + + a = [ + '# generated by interface_config.py', + 'option rfc3442-classless-static-routes code 121 = array of unsigned integer 8;', + 'interface \"' + self._ifname + '\" {', + '\tsend host-name \"' + socket.gethostname() + '\";', + '\trequest subnet-mask, broadcast-address, routers, domain-name-servers, rfc3442-classless-static-routes, domain-name, interface-mtu;', + '}' ] - cnf = "" - for ln in a: - cnf +=str(ln + "\n") - open(conf_file, 'w').write(cnf) - subprocess.call(['sysctl', '-q', '-w', 'net.ipv6.conf.' + self._ifname + '.accept_ra=0']) - if os.path.exists(pidfile): - try: - ret = subprocess.check_output(['/sbin/dhclient -6 -q -x -pf ' + pidfile], shell=True).decode() - except subprocess.CalledProcessError as e: - if self._debug(): - self._debug(e) - try: - ret = subprocess.check_output(['/sbin/dhclient -6 -q -nw -cf ' + conf_file + ' -pf ' + pidfile + ' -lf ' + leasefile + ' ' + self._ifname], shell=True).decode() - return True - except subprocess.CalledProcessError as e: - if self._debug(): - self._debug(e) - return None - - def del_dhcpv6(self): - conf_file = dhclient_conf_dir + self._ifname + '.v6conf' - pidfile = dhclient_conf_dir + self._ifname + '.v6pid' - leasefile = dhclient_conf_dir + self._ifname + '.v6leases' - if not os.path.exists(pidfile): - return 1 - try: - ret = subprocess.check_output(['/sbin/dhclient -6 -q -x -pf ' + pidfile], shell=True).decode() - subprocess.call(['sysctl', '-q', '-w', 'net.ipv6.conf.' + self._ifname + '.accept_ra=1']) - return True - except subprocess.CalledProcessError as e: - if self._debug(): - self._debug(e) - return None - - def get_dhcpv6(self): - pidfile = dhclient_conf_dir + self._ifname + '.v6pid' - if not os.path.exists(pidfile): - print ("no dhcpv6 client running on interface {0}".format(self._ifname)) - return False - else: - pid = open(pidfile, 'r').read() - print("dhclientv6 running on {0} with pid {1}".format(self._ifname, pid)) - return True - - -#### TODO: dhcpv6-pd via dhclient + cnf = "" + for ln in a: + cnf += str(ln + "\n") + open(conf_file, 'w').write(cnf) + if os.path.exists(dhclient_conf_dir + self._ifname + '.pid'): + try: + ret = subprocess.check_output( + ['/sbin/dhclient -4 -r -pf ' + pidfile], shell=True).decode() + except subprocess.CalledProcessError as e: + if self._debug(): + self._debug(e) + try: + ret = subprocess.check_output( + ['/sbin/dhclient -4 -q -nw -cf ' + conf_file + ' -pf ' + pidfile + ' -lf ' + leasefile + ' ' + self._ifname], shell=True).decode() + return True + except subprocess.CalledProcessError as e: + if self._debug(): + self._debug(e) + return None + + def del_dhcpv4(self): + conf_file = dhclient_conf_dir + self._ifname + '.conf' + pidfile = dhclient_conf_dir + self._ifname + '.pid' + leasefile = dhclient_conf_dir + self._ifname + '.leases' + if not os.path.exists(pidfile): + return 1 + try: + ret = subprocess.check_output( + ['/sbin/dhclient -4 -r -pf ' + pidfile], shell=True).decode() + return True + except subprocess.CalledProcessError as e: + if self._debug(): + self._debug(e) + return None + + def get_dhcpv4(self): + pidfile = dhclient_conf_dir + self._ifname + '.pid' + if not os.path.exists(pidfile): + print ( + "no dhcp client running on interface {0}".format(self._ifname)) + return False + else: + pid = open(pidfile, 'r').read() + print( + "dhclient running on {0} with pid {1}".format(self._ifname, pid)) + return True + + def set_dhcpv6(self): + conf_file = dhclient_conf_dir + self._ifname + '.v6conf' + pidfile = dhclient_conf_dir + self._ifname + '.v6pid' + leasefile = dhclient_conf_dir + self._ifname + '.v6leases' + a = [ + '# generated by interface_config.py', + 'interface \"' + self._ifname + '\" {', + '\trequest routers, domain-name-servers, domain-name;', + '}' + ] + cnf = "" + for ln in a: + cnf += str(ln + "\n") + open(conf_file, 'w').write(cnf) + subprocess.call( + ['sysctl', '-q', '-w', 'net.ipv6.conf.' + self._ifname + '.accept_ra=0']) + if os.path.exists(pidfile): + try: + ret = subprocess.check_output( + ['/sbin/dhclient -6 -q -x -pf ' + pidfile], shell=True).decode() + except subprocess.CalledProcessError as e: + if self._debug(): + self._debug(e) + try: + ret = subprocess.check_output( + ['/sbin/dhclient -6 -q -nw -cf ' + conf_file + ' -pf ' + pidfile + ' -lf ' + leasefile + ' ' + self._ifname], shell=True).decode() + return True + except subprocess.CalledProcessError as e: + if self._debug(): + self._debug(e) + return None + + def del_dhcpv6(self): + conf_file = dhclient_conf_dir + self._ifname + '.v6conf' + pidfile = dhclient_conf_dir + self._ifname + '.v6pid' + leasefile = dhclient_conf_dir + self._ifname + '.v6leases' + if not os.path.exists(pidfile): + return 1 + try: + ret = subprocess.check_output( + ['/sbin/dhclient -6 -q -x -pf ' + pidfile], shell=True).decode() + subprocess.call( + ['sysctl', '-q', '-w', 'net.ipv6.conf.' + self._ifname + '.accept_ra=1']) + return True + except subprocess.CalledProcessError as e: + if self._debug(): + self._debug(e) + return None + + def get_dhcpv6(self): + pidfile = dhclient_conf_dir + self._ifname + '.v6pid' + if not os.path.exists(pidfile): + print ( + "no dhcpv6 client running on interface {0}".format(self._ifname)) + return False + else: + pid = open(pidfile, 'r').read() + print( + "dhclientv6 running on {0} with pid {1}".format(self._ifname, pid)) + return True + + +# TODO: dhcpv6-pd via dhclient -- cgit v1.2.3 From c50327efe341bfc5444a8b888e4cd58bb251fc01 Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Fri, 30 Aug 2019 10:47:10 +0200 Subject: Python/ifconfig: ease __init__ if/else statements --- python/vyos/interfaceconfig.py | 25 ++++++++++++------------- 1 file changed, 12 insertions(+), 13 deletions(-) (limited to 'python/vyos') diff --git a/python/vyos/interfaceconfig.py b/python/vyos/interfaceconfig.py index e354d900d..1dd6487d7 100644 --- a/python/vyos/interfaceconfig.py +++ b/python/vyos/interfaceconfig.py @@ -25,25 +25,24 @@ import ipaddress dhclient_conf_dir = r'/var/lib/dhcp/dhclient_' - class Interface: - def __init__(self, ifname=None, type=None): if not ifname: raise Exception("interface name required") + if not os.path.exists('/sys/class/net/{0}'.format(ifname)) and not type: raise Exception("interface {0} not found".format(str(ifname))) - else: - if not os.path.exists('/sys/class/net/{0}'.format(ifname)): - try: - ret = subprocess.check_output( - ['ip link add dev ' + str(ifname) + ' type ' + type], stderr=subprocess.STDOUT, shell=True).decode() - except subprocess.CalledProcessError as e: - if self._debug(): - self._debug(e) - if "Operation not supported" in str(e.output.decode()): - print(str(e.output.decode())) - sys.exit(0) + + if not os.path.exists('/sys/class/net/{0}'.format(ifname)): + try: + ret = subprocess.check_output( + ['ip link add dev ' + str(ifname) + ' type ' + type], stderr=subprocess.STDOUT, shell=True).decode() + except subprocess.CalledProcessError as e: + if self._debug(): + self._debug(e) + if "Operation not supported" in str(e.output.decode()): + print(str(e.output.decode())) + sys.exit(0) self._ifname = str(ifname) -- cgit v1.2.3 From e7fee6a6f163ddd9a96142ef0298fdb2804b7928 Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Fri, 30 Aug 2019 11:07:58 +0200 Subject: Python/ifconfig: re-indent help strings --- python/vyos/interfaceconfig.py | 27 +++++++++++++++------------ 1 file changed, 15 insertions(+), 12 deletions(-) (limited to 'python/vyos') diff --git a/python/vyos/interfaceconfig.py b/python/vyos/interfaceconfig.py index 1dd6487d7..3ba9c225c 100644 --- a/python/vyos/interfaceconfig.py +++ b/python/vyos/interfaceconfig.py @@ -72,6 +72,9 @@ class Interface: @property def macaddr(self): + """ + get/set interface mac address + """ try: ret = subprocess.check_output( ['ip -j -4 link show dev ' + self._ifname], stderr=subprocess.STDOUT, shell=True).decode() @@ -134,7 +137,7 @@ class Interface: def _debug(self, e=None): """ - export DEBUG=1 to see debug messages + export DEBUG=1 to see debug messages """ if os.getenv('DEBUG') == '1': if e: @@ -199,9 +202,9 @@ class Interface: def get_addr(self, ret_prefix=None): """ - universal: reads all IPs assigned to an interface and returns it in a list, - or None if no IP address is assigned to the interface. Also may return - in prefix format if set ret_prefix + universal: reads all IPs assigned to an interface and returns it in a list, + or None if no IP address is assigned to the interface. Also may return + in prefix format if set ret_prefix """ ips = [] try: @@ -224,8 +227,8 @@ class Interface: def get_ipv4_addr(self): """ - reads all IPs assigned to an interface and returns it in a list, - or None if no IP address is assigned to the interface + reads all IPs assigned to an interface and returns it in a list, + or None if no IP address is assigned to the interface """ ips = [] try: @@ -244,8 +247,8 @@ class Interface: def get_ipv6_addr(self): """ - reads all IPs assigned to an interface and returns it in a list, - or None if no IP address is assigned to the interface + reads all IPs assigned to an interface and returns it in a list, + or None if no IP address is assigned to the interface """ ips = [] try: @@ -299,7 +302,7 @@ class Interface: def add_ipv4_addr(self, ipaddr=[]): """ - add addresses on the interface + add addresses on the interface """ for ip in ipaddr: try: @@ -313,7 +316,7 @@ class Interface: def del_ipv4_addr(self, ipaddr=[]): """ - delete addresses on the interface + delete addresses on the interface """ for ip in ipaddr: try: @@ -327,7 +330,7 @@ class Interface: def add_ipv6_addr(self, ipaddr=[]): """ - add addresses on the interface + add addresses on the interface """ for ip in ipaddr: try: @@ -341,7 +344,7 @@ class Interface: def del_ipv6_addr(self, ipaddr=[]): """ - delete addresses on the interface + delete addresses on the interface """ for ip in ipaddr: try: -- cgit v1.2.3 From d1af812bd2e8f4471d4b4c1361370038b5b47520 Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Fri, 30 Aug 2019 11:09:30 +0200 Subject: Python/ifconfig: re-work mtu getter/setter Instead of calling iprotue2 via a subprocess (which is only complicated and expensive), we rather directly interact with sysfs). --- python/vyos/interfaceconfig.py | 46 ++++++++++++++++++++++++++---------------- 1 file changed, 29 insertions(+), 17 deletions(-) (limited to 'python/vyos') diff --git a/python/vyos/interfaceconfig.py b/python/vyos/interfaceconfig.py index 3ba9c225c..3f751e421 100644 --- a/python/vyos/interfaceconfig.py +++ b/python/vyos/interfaceconfig.py @@ -48,27 +48,39 @@ class Interface: @property def mtu(self): - try: - ret = subprocess.check_output( - ['ip -j link list dev ' + self._ifname], shell=True).decode() - a = json.loads(ret)[0] - return a['mtu'] - except subprocess.CalledProcessError as e: - if self._debug(): - self._debug(e) - return None + """ + Get/set interface mtu in bytes. + + Example: + + from vyos.interfaceconfig import Interface + mtu = Interface('ens192').mtu + print(mtu) + """ + + mtu = 0 + with open('/sys/class/net/{0}/mtu'.format(self._ifname), 'r') as f: + mtu = f.read().rstrip('\n') + return mtu + @mtu.setter def mtu(self, mtu=None): + """ + Get/set interface mtu in bytes. + + Example: + + from vyos.interfaceconfig import Interface + Interface('ens192').mtu = 1400 + """ + if mtu < 68 or mtu > 9000: - raise ValueError("mtu size invalid value") - self._mtu = mtu - try: - ret = subprocess.check_output( - ['ip link set mtu ' + str(mtu) + ' dev ' + self._ifname], shell=True).decode() - except subprocess.CalledProcessError as e: - if self._debug(): - self._debug(e) + raise ValueError('Invalid MTU size: "{}"'.format(mru)) + + with open('/sys/class/net/{0}/mtu'.format(self._ifname), 'w') as f: + f.write(str(mtu)) + @property def macaddr(self): -- cgit v1.2.3 From fc0c87c83089dcd84a1b2dc93e4d27542e11bb54 Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Fri, 30 Aug 2019 11:10:43 +0200 Subject: Python/ifconfig: remove unused depricated methods --- python/vyos/interfaceconfig.py | 44 ------------------------------------------ 1 file changed, 44 deletions(-) (limited to 'python/vyos') diff --git a/python/vyos/interfaceconfig.py b/python/vyos/interfaceconfig.py index 3f751e421..edd8ef038 100644 --- a/python/vyos/interfaceconfig.py +++ b/python/vyos/interfaceconfig.py @@ -158,50 +158,6 @@ class Interface: return True return False - def get_mtu(self): - print ("function get_mtu() is depricated and will be removed soon") - try: - ret = subprocess.check_output( - ['ip -j link list dev ' + self._ifname], shell=True).decode() - a = json.loads(ret)[0] - return a['mtu'] - except subprocess.CalledProcessError as e: - if self._debug(): - self._debug(e) - return None - - def get_macaddr(self): - print ("function get_macaddr() is depricated and will be removed soon") - try: - ret = subprocess.check_output( - ['ip -j -4 link show dev ' + self._ifname], stderr=subprocess.STDOUT, shell=True).decode() - j = json.loads(ret) - return j[0]['address'] - except subprocess.CalledProcessError as e: - if self._debug(): - self._debug(e) - return None - - def get_alias(self): - print ("function get_alias() is depricated and will be removed soon") - return open('/sys/class/net/{0}/ifalias'.format(self._ifname), 'r').read() - - def del_alias(self): - open('/sys/class/net/{0}/ifalias'.format(self._ifname), 'w').write() - - def get_link_state(self): - print ( - "function get_link_state() is depricated and will be removed soon") - try: - ret = subprocess.check_output( - ['ip -j link show ' + self._ifname], shell=True).decode() - s = json.loads(ret) - return s[0]['operstate'].lower() - except subprocess.CalledProcessError as e: - if self._debug(): - self._debug(e) - return None - def remove_interface(self): try: ret = subprocess.check_output( -- cgit v1.2.3 From dff5f3f38f60b9da8e791593998e6f614da6e5ec Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Fri, 30 Aug 2019 11:31:00 +0200 Subject: Python/ifconfig: rework changing of interface MAC address --- python/vyos/interfaceconfig.py | 71 +++++++++++++++++++++++++++++------------- 1 file changed, 49 insertions(+), 22 deletions(-) (limited to 'python/vyos') diff --git a/python/vyos/interfaceconfig.py b/python/vyos/interfaceconfig.py index edd8ef038..c83b0d55c 100644 --- a/python/vyos/interfaceconfig.py +++ b/python/vyos/interfaceconfig.py @@ -46,6 +46,11 @@ class Interface: self._ifname = str(ifname) + def _cmd(self, command): + process = subprocess.Popen(command,stdout=subprocess.PIPE, shell=True) + proc_stdout = process.communicate()[0].strip() + pass + @property def mtu(self): """ @@ -83,31 +88,53 @@ class Interface: @property - def macaddr(self): + def mac(self): """ - get/set interface mac address + Get/set interface mac address + + Example: + + from vyos.interfaceconfig import Interface + mac = Interface('ens192').mac """ - try: - ret = subprocess.check_output( - ['ip -j -4 link show dev ' + self._ifname], stderr=subprocess.STDOUT, shell=True).decode() - j = json.loads(ret) - return j[0]['address'] - except subprocess.CalledProcessError as e: - if self._debug(): - self._debug(e) - return None + address = '' + with open('/sys/class/net/{0}/address'.format(self._ifname), 'r') as f: + address = f.read().rstrip('\n') + return address + + + @mac.setter + def mac(self, mac=None): + """ + Get/set interface mac address + + Example: + + from vyos.interfaceconfig import Interface + Interface('ens192').mac = '00:90:43:fe:fe:1b' + """ + # a mac address consits out of 6 octets + octets = len(mac.split(':')) + if octets != 6: + raise ValueError('wrong number of MAC octets: {} '.format(octets)) + + # validate against the first mac address byte if it's a multicast address + if int(mac.split(':')[0]) & 1: + raise ValueError('{} is a multicast MAC address'.format(mac)) + + # overall mac address is not allowed to be 00:00:00:00:00:00 + if sum(int(i, 16) for i in mac.split(':')) == 0: + raise ValueError('00:00:00:00:00:00 is not a valid MAC address') + + # check for VRRP mac address + if mac.split(':')[0] == '0' and addr.split(':')[1] == '0' and mac.split(':')[2] == '94' and mac.split(':')[3] == '0' and mac.split(':')[4] == '1': + raise ValueError('{} is a VRRP MAC address'.format(mac)) + + # Assemble command executed on system. Unfortunately there is no way + # of altering the MAC address via sysfs + cmd = 'ip link set dev "{}" address "{}"'.format(self._ifname, mac) + self._cmd(cmd) - @macaddr.setter - def macaddr(self, mac=None): - if not re.search('^[a-f0-9:]{17}$', str(mac)): - raise ValueError("mac address invalid") - self._macaddr = str(mac) - try: - ret = subprocess.check_output( - ['ip link set address ' + mac + ' ' + self._ifname], shell=True).decode() - except subprocess.CalledProcessError as e: - if self._debug(): - self._debug(e) @property def ifalias(self): -- cgit v1.2.3 From 3238a9cc621975c9417885b3f8552585dce594be Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Fri, 30 Aug 2019 11:41:48 +0200 Subject: Python/ifconfig: rework interface alias assignment --- python/vyos/interfaceconfig.py | 39 +++++++++++++++++++++++++++++++++------ 1 file changed, 33 insertions(+), 6 deletions(-) (limited to 'python/vyos') diff --git a/python/vyos/interfaceconfig.py b/python/vyos/interfaceconfig.py index c83b0d55c..c10f76e52 100644 --- a/python/vyos/interfaceconfig.py +++ b/python/vyos/interfaceconfig.py @@ -138,16 +138,43 @@ class Interface: @property def ifalias(self): - return open('/sys/class/net/{0}/ifalias'.format(self._ifname), 'r').read() + """ + Get/set interface alias name + + Example: + + from vyos.interfaceconfig import Interface + alias = Interface('ens192').ifalias + """ + + alias = '' + with open('/sys/class/net/{0}/ifalias'.format(self._ifname), 'r') as f: + alias = f.read().rstrip('\n') + return alias + @ifalias.setter def ifalias(self, ifalias=None): + """ + Get/set interface alias name + + Example: + + from vyos.interfaceconfig import Interface + Interface('ens192').ifalias = 'VyOS upstream interface' + + to clear interface alias e.g. delete it use: + + Interface('ens192').ifalias = '' + """ + + # clear interface alias if not ifalias: - self._ifalias = self._ifname - else: - self._ifalias = str(ifalias) - open('/sys/class/net/{0}/ifalias'.format( - self._ifname), 'w').write(self._ifalias) + ifalias = '\0' + + with open('/sys/class/net/{0}/ifalias'.format(self._ifname), 'w') as f: + f.write(str(ifalias)) + @property def linkstate(self): -- cgit v1.2.3 From bb5ff5e0b968612890791de48e43479b0352c532 Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Fri, 30 Aug 2019 11:46:19 +0200 Subject: Python/ifconfig: re-work __init__ interface creation --- python/vyos/interfaceconfig.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'python/vyos') diff --git a/python/vyos/interfaceconfig.py b/python/vyos/interfaceconfig.py index c10f76e52..c40912e3c 100644 --- a/python/vyos/interfaceconfig.py +++ b/python/vyos/interfaceconfig.py @@ -35,8 +35,8 @@ class Interface: if not os.path.exists('/sys/class/net/{0}'.format(ifname)): try: - ret = subprocess.check_output( - ['ip link add dev ' + str(ifname) + ' type ' + type], stderr=subprocess.STDOUT, shell=True).decode() + cmd = 'ip link add dev "{}" type "{}"'.format(ifname, type) + self._cmd(cmd) except subprocess.CalledProcessError as e: if self._debug(): self._debug(e) -- cgit v1.2.3 From cb1b72c5ccce1bd2d0f0b02eb4abeee0b5635d55 Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Fri, 30 Aug 2019 12:00:44 +0200 Subject: Python/ifconfig: replace linkstate() with up()/down() methods --- python/vyos/interfaceconfig.py | 55 ++++++++++++++++++++++++------------------ 1 file changed, 32 insertions(+), 23 deletions(-) (limited to 'python/vyos') diff --git a/python/vyos/interfaceconfig.py b/python/vyos/interfaceconfig.py index c40912e3c..f9e231267 100644 --- a/python/vyos/interfaceconfig.py +++ b/python/vyos/interfaceconfig.py @@ -176,30 +176,39 @@ class Interface: f.write(str(ifalias)) - @property - def linkstate(self): - try: - ret = subprocess.check_output( - ['ip -j link show ' + self._ifname], shell=True).decode() - s = json.loads(ret) - return s[0]['operstate'].lower() - except subprocess.CalledProcessError as e: - if self._debug(): - self._debug(e) - return None + def up(self): + """ + Bring interface up + + Example: + + from vyos.interfaceconfig import Interface + i = Interface('br100', type='bridge') + i.up() + """ + + # Assemble command executed on system. Unfortunately there is no way + # to up/down an interface via sysfs + cmd = 'ip link set dev "{}" up'.format(self._ifname) + self._cmd(cmd) + + + def down(self): + """ + Bring interface down + + Example: + + from vyos.interfaceconfig import Interface + i = Interface('br100', type='bridge') + i.down() + """ + + # Assemble command executed on system. Unfortunately there is no way + # to up/down an interface via sysfs + cmd = 'ip link set dev "{}" down'.format(self._ifname) + self._cmd(cmd) - @linkstate.setter - def linkstate(self, state='up'): - if str(state).lower() == 'up' or str(state).lower() == 'down': - self._linkstate = str(state).lower() - else: - self._linkstate = 'up' - try: - ret = subprocess.check_output( - ['ip link set dev ' + self._ifname + ' ' + state], shell=True).decode() - except subprocess.CalledProcessError as e: - if self._debug(): - self._debug(e) def _debug(self, e=None): """ -- cgit v1.2.3 From 17454c22554b6161dbc177f9e6b798ae6d73e7bc Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Fri, 30 Aug 2019 12:06:59 +0200 Subject: Python/ifconfig: re-work and rename remove_interface() -> remove() to delete an interface --- python/vyos/interfaceconfig.py | 39 ++++++++++++++++++++++++++++++--------- 1 file changed, 30 insertions(+), 9 deletions(-) (limited to 'python/vyos') diff --git a/python/vyos/interfaceconfig.py b/python/vyos/interfaceconfig.py index f9e231267..1d3215a0a 100644 --- a/python/vyos/interfaceconfig.py +++ b/python/vyos/interfaceconfig.py @@ -27,6 +27,15 @@ dhclient_conf_dir = r'/var/lib/dhcp/dhclient_' class Interface: def __init__(self, ifname=None, type=None): + """ + Create instance of an IP interface + + Example: + + from vyos.interfaceconfig import Interface + i = Interface('br111', type='bridge') + """ + if not ifname: raise Exception("interface name required") @@ -46,11 +55,32 @@ class Interface: self._ifname = str(ifname) + + def remove(self): + """ + Remove system interface + + Example: + + from vyos.interfaceconfig import Interface + i = Interface('br111') + i.remove() + """ + + # NOTE (Improvement): + # after interface removal no other commands should be allowed + # to be called and instead should raise an Exception: + + cmd = 'ip link del dev "{}"'.format(self._ifname) + self._cmd(cmd) + + def _cmd(self, command): process = subprocess.Popen(command,stdout=subprocess.PIPE, shell=True) proc_stdout = process.communicate()[0].strip() pass + @property def mtu(self): """ @@ -221,15 +251,6 @@ class Interface: return True return False - def remove_interface(self): - try: - ret = subprocess.check_output( - ['ip link del dev ' + self._ifname], shell=True).decode() - return 0 - except subprocess.CalledProcessError as e: - if self._debug(): - self._debug(e) - return None def get_addr(self, ret_prefix=None): """ -- cgit v1.2.3 From 83ded2c50e750acb73294ddde5383d8ffdf746ed Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Fri, 30 Aug 2019 12:15:30 +0200 Subject: Python/ifconfig: add @property statement on 'remove' call --- python/vyos/interfaceconfig.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) (limited to 'python/vyos') diff --git a/python/vyos/interfaceconfig.py b/python/vyos/interfaceconfig.py index 1d3215a0a..9fd0d83f9 100644 --- a/python/vyos/interfaceconfig.py +++ b/python/vyos/interfaceconfig.py @@ -55,7 +55,7 @@ class Interface: self._ifname = str(ifname) - + @property def remove(self): """ Remove system interface @@ -63,8 +63,8 @@ class Interface: Example: from vyos.interfaceconfig import Interface - i = Interface('br111') - i.remove() + i = Interface('br111', type='bridge') + i.remove """ # NOTE (Improvement): -- cgit v1.2.3 From 758715df97dea209bffd1a935b70a765a80d2d81 Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Fri, 30 Aug 2019 12:16:09 +0200 Subject: Python/ifconfig: replace up()/down() with 'state' property Commit cb1b72c5c ("Python/ifconfig: replace linkstate() with up()/down() methods") replaced the linkstate property in favour of up()/down() functions. Instead it really makes more sense to have a propery to also query the current linkstate from sysfs. --- python/vyos/interfaceconfig.py | 32 +++++++++++++------------------- 1 file changed, 13 insertions(+), 19 deletions(-) (limited to 'python/vyos') diff --git a/python/vyos/interfaceconfig.py b/python/vyos/interfaceconfig.py index 9fd0d83f9..5bed90494 100644 --- a/python/vyos/interfaceconfig.py +++ b/python/vyos/interfaceconfig.py @@ -205,38 +205,32 @@ class Interface: with open('/sys/class/net/{0}/ifalias'.format(self._ifname), 'w') as f: f.write(str(ifalias)) - - def up(self): + @property + def state(self): """ - Bring interface up + Enable (up) / Disable (down) an interface Example: from vyos.interfaceconfig import Interface - i = Interface('br100', type='bridge') - i.up() + i = Interface('ens192').link """ - # Assemble command executed on system. Unfortunately there is no way - # to up/down an interface via sysfs - cmd = 'ip link set dev "{}" up'.format(self._ifname) - self._cmd(cmd) - + state = '' + with open('/sys/class/net/{0}/operstate'.format(self._ifname), 'r') as f: + state = f.read().rstrip('\n') + return state - def down(self): - """ - Bring interface down - Example: + @state.setter + def state(self, state=None): - from vyos.interfaceconfig import Interface - i = Interface('br100', type='bridge') - i.down() - """ + if state not in ['up', 'down']: + raise ValueError('state must be "up" or "down"') # Assemble command executed on system. Unfortunately there is no way # to up/down an interface via sysfs - cmd = 'ip link set dev "{}" down'.format(self._ifname) + cmd = 'ip link set dev "{}" "{}"'.format(self._ifname, state) self._cmd(cmd) -- cgit v1.2.3 From ffd45bf460b312f1799dc8418724bf597b2ffa20 Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Fri, 30 Aug 2019 13:00:55 +0200 Subject: Python/ifconfig: re-work IP address creation/deletion --- python/vyos/interfaceconfig.py | 222 +++++++++++++++-------------------------- 1 file changed, 82 insertions(+), 140 deletions(-) (limited to 'python/vyos') diff --git a/python/vyos/interfaceconfig.py b/python/vyos/interfaceconfig.py index 5bed90494..9790fae49 100644 --- a/python/vyos/interfaceconfig.py +++ b/python/vyos/interfaceconfig.py @@ -23,6 +23,10 @@ import socket import subprocess import ipaddress +from vyos.validate import * +from ipaddress import IPv4Network, IPv6Address +from netifaces import ifaddresses, AF_INET, AF_INET6 + dhclient_conf_dir = r'/var/lib/dhcp/dhclient_' class Interface: @@ -246,161 +250,99 @@ class Interface: return False - def get_addr(self, ret_prefix=None): - """ - universal: reads all IPs assigned to an interface and returns it in a list, - or None if no IP address is assigned to the interface. Also may return - in prefix format if set ret_prefix + def get_addr(self): """ - ips = [] - try: - ret = subprocess.check_output( - ['ip -j addr show dev ' + self._ifname], stderr=subprocess.STDOUT, shell=True).decode() - j = json.loads(ret) - for i in j: - if len(i) != 0: - for addr in i['addr_info']: - if ret_prefix: - ips.append( - addr['local'] + "/" + str(addr['prefixlen'])) - else: - ips.append(addr['local']) - return ips - except subprocess.CalledProcessError as e: - if self._debug(): - self._debug(e) - return None + Retrieve assigned IPv4 and IPv6 addresses from given interface. + This is done using the netifaces and ipaddress python modules. - def get_ipv4_addr(self): - """ - reads all IPs assigned to an interface and returns it in a list, - or None if no IP address is assigned to the interface - """ - ips = [] - try: - ret = subprocess.check_output( - ['ip -j -4 addr show dev ' + self._ifname], stderr=subprocess.STDOUT, shell=True).decode() - j = json.loads(ret) - for i in j: - if len(i) != 0: - for addr in i['addr_info']: - ips.append(addr['local']) - return ips - except subprocess.CalledProcessError as e: - if self._debug(): - self._debug(e) - return None + Example: - def get_ipv6_addr(self): - """ - reads all IPs assigned to an interface and returns it in a list, - or None if no IP address is assigned to the interface + from vyos.interfaceconfig import Interface + i = Interface('ens192') + i.get_addrs() + ['172.16.33.30/24', 'fe80::20c:29ff:fe11:a174/64'] """ - ips = [] - try: - ret = subprocess.check_output( - ['ip -j -6 addr show dev ' + self._ifname], stderr=subprocess.STDOUT, shell=True).decode() - j = json.loads(ret) - for i in j: - if len(i) != 0: - for addr in i['addr_info']: - ips.append(addr['local']) - return ips - except subprocess.CalledProcessError as e: - if self._debug(): - self._debug(e) - return None - def add_addr(self, ipaddr=[]): - """ - universal: add ipv4/ipv6 addresses on the interface - """ - for ip in ipaddr: - proto = '-4' - if ipaddress.ip_address(ip.split(r'/')[0]).version == 6: - proto = '-6' + ipv4 = [] + ipv6 = [] - try: - ret = subprocess.check_output( - ['ip ' + proto + ' address add ' + ip + ' dev ' + self._ifname], stderr=subprocess.STDOUT, shell=True).decode() - except subprocess.CalledProcessError as e: - if self._debug(): - self._debug(e) - return None - return True + if AF_INET in ifaddresses(self._ifname).keys(): + for v4_addr in ifaddresses(self._ifname)[AF_INET]: + # we need to manually assemble a list of IPv4 address/prefix + prefix = '/' + str(IPv4Network('0.0.0.0/' + v4_addr['netmask']).prefixlen) + ipv4.append( v4_addr['addr'] + prefix ) - def del_addr(self, ipaddr=[]): - """ - universal: delete ipv4/ipv6 addresses on the interface - """ - for ip in ipaddr: - proto = '-4' - if ipaddress.ip_address(ip.split(r'/')[0]).version == 6: - proto = '-6' - try: - ret = subprocess.check_output( - ['ip ' + proto + ' address del ' + ip + ' dev ' + self._ifname], stderr=subprocess.STDOUT, shell=True).decode() - except subprocess.CalledProcessError as e: - if self._debug(): - self._debug(e) - return None - return True + if AF_INET6 in ifaddresses(self._ifname).keys(): + for v6_addr in ifaddresses(self._ifname)[AF_INET6]: + # Note that currently expanded netmasks are not supported. That means + # 2001:db00::0/24 is a valid argument while 2001:db00::0/ffff:ff00:: not. + # see https://docs.python.org/3/library/ipaddress.html + bits = bin( int(v6_addr['netmask'].replace(':',''), 16) ).count('1') + prefix = '/' + str(bits) - def add_ipv4_addr(self, ipaddr=[]): - """ - add addresses on the interface - """ - for ip in ipaddr: - try: - ret = subprocess.check_output( - ['ip -4 address add ' + ip + ' dev ' + self._ifname], stderr=subprocess.STDOUT, shell=True).decode() - except subprocess.CalledProcessError as e: - if self._debug(): - self._debug(e) - return None - return True + # we alsoneed to remove the interface suffix on link local addresses + v6_addr['addr'] = v6_addr['addr'].split('%')[0] + ipv6.append( v6_addr['addr'] + prefix ) + + return ipv4 + ipv6 - def del_ipv4_addr(self, ipaddr=[]): - """ - delete addresses on the interface - """ - for ip in ipaddr: - try: - ret = subprocess.check_output( - ['ip -4 address del ' + ip + ' dev ' + self._ifname], stderr=subprocess.STDOUT, shell=True).decode() - except subprocess.CalledProcessError as e: - if self._debug(): - self._debug(e) - return None - return True - def add_ipv6_addr(self, ipaddr=[]): + def add_addr(self, addr=None): """ - add addresses on the interface + Add IP address to interface. Address is only added if it yet not added + to that interface. + + Example: + + >>> from vyos.interfaceconfig import Interface + >>> j = Interface('br100', type='bridge') + >>> j.add_addr('192.0.2.1/24') + >>> j.add_addr('2001:db8::ffff/64') + >>> j.get_addr() + ['192.0.2.1/24', '2001:db8::ffff/64'] """ - for ip in ipaddr: - try: - ret = subprocess.check_output( - ['ip -6 address add ' + ip + ' dev ' + self._ifname], stderr=subprocess.STDOUT, shell=True).decode() - except subprocess.CalledProcessError as e: - if self._debug(): - self._debug(e) - return None - return True - def del_ipv6_addr(self, ipaddr=[]): + if not addr: + raise ValueError('No IP address specified') + + if not is_intf_addr_assigned(self._ifname, addr): + cmd = '' + if is_ipv4(addr): + cmd = 'sudo ip -4 addr add "{}" broadcast + dev "{}"'.format(addr, self._ifname) + elif is_ipv6(addr): + cmd = 'sudo ip -6 addr add "{}" dev "{}"'.format(addr, self._ifname) + + self._cmd(cmd) + + + def del_addr(self, addr=None): """ - delete addresses on the interface + Remove IP address from interface. + + Example: + >>> from vyos.interfaceconfig import Interface + >>> j = Interface('br100', type='bridge') + >>> j.add_addr('2001:db8::ffff/64') + >>> j.add_addr('192.0.2.1/24') + >>> j.get_addr() + ['192.0.2.1/24', '2001:db8::ffff/64'] + >>> j.del_addr('192.0.2.1/24') + >>> j.get_addr() + ['2001:db8::ffff/64'] """ - for ip in ipaddr: - try: - ret = subprocess.check_output( - ['ip -6 address del ' + ip + ' dev ' + self._ifname], stderr=subprocess.STDOUT, shell=True).decode() - except subprocess.CalledProcessError as e: - if self._debug(): - self._debug(e) - return None - return True + + if not addr: + raise ValueError('No IP address specified') + + if is_intf_addr_assigned(self._ifname, addr): + cmd = '' + if is_ipv4(addr): + cmd = 'ip -4 addr del "{}" dev "{}"'.format(addr, self._ifname) + elif is_ipv6(addr): + cmd = 'ip -6 addr del "{}" dev "{}"'.format(addr, self._ifname) + + self._cmd(cmd) + # replace dhcpv4/v6 with systemd.networkd? def set_dhcpv4(self): -- cgit v1.2.3 From 24495a18b2ba39cd0c5b024dbe63f3e7df92e69c Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Fri, 30 Aug 2019 13:25:23 +0200 Subject: Python/ifconfig: rename interfaceconfig.py -> ifconfig.py --- python/vyos/ifconfig.py | 491 ++++++++++++++++++++++++++++++++++++++ python/vyos/interfaceconfig.py | 471 ------------------------------------ src/conf_mode/interface-bridge.py | 2 +- 3 files changed, 492 insertions(+), 472 deletions(-) create mode 100644 python/vyos/ifconfig.py delete mode 100644 python/vyos/interfaceconfig.py (limited to 'python/vyos') diff --git a/python/vyos/ifconfig.py b/python/vyos/ifconfig.py new file mode 100644 index 000000000..5f28125af --- /dev/null +++ b/python/vyos/ifconfig.py @@ -0,0 +1,491 @@ +#!/usr/bin/python3 + +# Copyright 2019 VyOS maintainers and contributors +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library. If not, see . + +import sys +import os +import re +import json +import socket +import subprocess +import ipaddress + +from vyos.validate import * +from ipaddress import IPv4Network, IPv6Address +from netifaces import ifaddresses, AF_INET, AF_INET6 + +dhclient_conf_dir = r'/var/lib/dhcp/dhclient_' + +class Interface: + def __init__(self, ifname=None, type=None): + """ + Create instance of an IP interface + + Example: + + >>> from vyos.ifconfig import Interface + >>> i = Interface('br111', type='bridge') + """ + + if not ifname: + raise Exception("interface name required") + + if not os.path.exists('/sys/class/net/{0}'.format(ifname)) and not type: + raise Exception("interface {0} not found".format(str(ifname))) + + if not os.path.exists('/sys/class/net/{0}'.format(ifname)): + try: + cmd = 'ip link add dev "{}" type "{}"'.format(ifname, type) + self._cmd(cmd) + except subprocess.CalledProcessError as e: + if self._debug(): + self._debug(e) + if "Operation not supported" in str(e.output.decode()): + print(str(e.output.decode())) + sys.exit(0) + + self._ifname = str(ifname) + + @property + def remove(self): + """ + Remove system interface + + Example: + + >>> from vyos.ifconfig import Interface + >>> i = Interface('br111', type='bridge') + >>> i.remove + """ + + # NOTE (Improvement): + # after interface removal no other commands should be allowed + # to be called and instead should raise an Exception: + + cmd = 'ip link del dev "{}"'.format(self._ifname) + self._cmd(cmd) + + + def _cmd(self, command): + process = subprocess.Popen(command,stdout=subprocess.PIPE, shell=True) + proc_stdout = process.communicate()[0].strip() + pass + + + @property + def mtu(self): + """ + Get/set interface mtu in bytes. + + Example: + + >>> from vyos.ifconfig import Interface + >>> Interface('eth1').mtu + '1500' + """ + + mtu = 0 + with open('/sys/class/net/{0}/mtu'.format(self._ifname), 'r') as f: + mtu = f.read().rstrip('\n') + return mtu + + + @mtu.setter + def mtu(self, mtu=None): + """ + Get/set interface mtu in bytes. + + Example: + + >>> from vyos.ifconfig import Interface + >>> Interface('br100', type='bridge').mtu = 1400 + >>> Interface('br100').mtu + '1400' + """ + + if mtu < 68 or mtu > 9000: + raise ValueError('Invalid MTU size: "{}"'.format(mru)) + + with open('/sys/class/net/{0}/mtu'.format(self._ifname), 'w') as f: + f.write(str(mtu)) + + + @property + def mac(self): + """ + Get/set interface mac address + + Example: + + >>> from vyos.ifconfig import Interface + >>> Interface('eth1').mac + '00:0c:29:11:aa:cc' + """ + address = '' + with open('/sys/class/net/{0}/address'.format(self._ifname), 'r') as f: + address = f.read().rstrip('\n') + return address + + + @mac.setter + def mac(self, mac=None): + """ + Get/set interface mac address + + Example: + + >>> from vyos.ifconfig import Interface + >>> Interface('eth1').mac = '00:90:43:fe:fe:1b' + >>> Interface('eth1').mac + '00:90:43:fe:fe:1b' + """ + # a mac address consits out of 6 octets + octets = len(mac.split(':')) + if octets != 6: + raise ValueError('wrong number of MAC octets: {} '.format(octets)) + + # validate against the first mac address byte if it's a multicast address + if int(mac.split(':')[0]) & 1: + raise ValueError('{} is a multicast MAC address'.format(mac)) + + # overall mac address is not allowed to be 00:00:00:00:00:00 + if sum(int(i, 16) for i in mac.split(':')) == 0: + raise ValueError('00:00:00:00:00:00 is not a valid MAC address') + + # check for VRRP mac address + if mac.split(':')[0] == '0' and addr.split(':')[1] == '0' and mac.split(':')[2] == '94' and mac.split(':')[3] == '0' and mac.split(':')[4] == '1': + raise ValueError('{} is a VRRP MAC address'.format(mac)) + + # Assemble command executed on system. Unfortunately there is no way + # of altering the MAC address via sysfs + cmd = 'ip link set dev "{}" address "{}"'.format(self._ifname, mac) + self._cmd(cmd) + + + @property + def ifalias(self): + """ + Get/set interface alias name + + Example: + + >>> from vyos.ifconfig import Interface + >>> Interface('eth1').ifalias + '' + """ + + alias = '' + with open('/sys/class/net/{0}/ifalias'.format(self._ifname), 'r') as f: + alias = f.read().rstrip('\n') + return alias + + + @ifalias.setter + def ifalias(self, ifalias=None): + """ + Get/set interface alias name + + Example: + + >>> from vyos.ifconfig import Interface + >>> Interface('eth1').ifalias = 'VyOS upstream interface' + >>> Interface('eth1').ifalias + 'VyOS upstream interface' + + to clear interface alias e.g. delete it use: + + >>> Interface('eth1').ifalias = '' + >>> Interface('eth1').ifalias + '' + """ + + # clear interface alias + if not ifalias: + ifalias = '\0' + + with open('/sys/class/net/{0}/ifalias'.format(self._ifname), 'w') as f: + f.write(str(ifalias)) + + @property + def state(self): + """ + Enable (up) / Disable (down) an interface + + Example: + + >>> from vyos.ifconfig import Interface + >>> Interface('eth1').state + 'up' + """ + + state = '' + with open('/sys/class/net/{0}/operstate'.format(self._ifname), 'r') as f: + state = f.read().rstrip('\n') + return state + + + @state.setter + def state(self, state=None): + """ + Enable (up) / Disable (down) an interface + + Example: + + >>> from vyos.ifconfig import Interface + >>> Interface('eth1').state = 'down' + >>> Interface('eth1').state + 'down' + """ + + if state not in ['up', 'down']: + raise ValueError('state must be "up" or "down"') + + # Assemble command executed on system. Unfortunately there is no way + # to up/down an interface via sysfs + cmd = 'ip link set dev "{}" "{}"'.format(self._ifname, state) + self._cmd(cmd) + + + def _debug(self, e=None): + """ + export DEBUG=1 to see debug messages + """ + if os.getenv('DEBUG') == '1': + if e: + print ("Exception raised:\ncommand: {0}\nerror code: {1}\nsubprocess output: {2}".format( + e.cmd, e.returncode, e.output.decode())) + return True + return False + + + def get_addr(self): + """ + Retrieve assigned IPv4 and IPv6 addresses from given interface. + This is done using the netifaces and ipaddress python modules. + + Example: + + >>> from vyos.ifconfig import Interface + >>> Interface('eth1').get_addrs() + ['172.16.33.30/24', 'fe80::20c:29ff:fe11:a174/64'] + """ + + ipv4 = [] + ipv6 = [] + + if AF_INET in ifaddresses(self._ifname).keys(): + for v4_addr in ifaddresses(self._ifname)[AF_INET]: + # we need to manually assemble a list of IPv4 address/prefix + prefix = '/' + str(IPv4Network('0.0.0.0/' + v4_addr['netmask']).prefixlen) + ipv4.append( v4_addr['addr'] + prefix ) + + if AF_INET6 in ifaddresses(self._ifname).keys(): + for v6_addr in ifaddresses(self._ifname)[AF_INET6]: + # Note that currently expanded netmasks are not supported. That means + # 2001:db00::0/24 is a valid argument while 2001:db00::0/ffff:ff00:: not. + # see https://docs.python.org/3/library/ipaddress.html + bits = bin( int(v6_addr['netmask'].replace(':',''), 16) ).count('1') + prefix = '/' + str(bits) + + # we alsoneed to remove the interface suffix on link local addresses + v6_addr['addr'] = v6_addr['addr'].split('%')[0] + ipv6.append( v6_addr['addr'] + prefix ) + + return ipv4 + ipv6 + + + def add_addr(self, addr=None): + """ + Add IP address to interface. Address is only added if it yet not added + to that interface. + + Example: + + >>> from vyos.interfaceconfig import Interface + >>> j = Interface('br100', type='bridge') + >>> j.add_addr('192.0.2.1/24') + >>> j.add_addr('2001:db8::ffff/64') + >>> j.get_addr() + ['192.0.2.1/24', '2001:db8::ffff/64'] + """ + + if not addr: + raise ValueError('No IP address specified') + + if not is_intf_addr_assigned(self._ifname, addr): + cmd = '' + if is_ipv4(addr): + cmd = 'sudo ip -4 addr add "{}" broadcast + dev "{}"'.format(addr, self._ifname) + elif is_ipv6(addr): + cmd = 'sudo ip -6 addr add "{}" dev "{}"'.format(addr, self._ifname) + + self._cmd(cmd) + + + def del_addr(self, addr=None): + """ + Remove IP address from interface. + + Example: + >>> from vyos.interfaceconfig import Interface + >>> j = Interface('br100', type='bridge') + >>> j.add_addr('2001:db8::ffff/64') + >>> j.add_addr('192.0.2.1/24') + >>> j.get_addr() + ['192.0.2.1/24', '2001:db8::ffff/64'] + >>> j.del_addr('192.0.2.1/24') + >>> j.get_addr() + ['2001:db8::ffff/64'] + """ + + if not addr: + raise ValueError('No IP address specified') + + if is_intf_addr_assigned(self._ifname, addr): + cmd = '' + if is_ipv4(addr): + cmd = 'ip -4 addr del "{}" dev "{}"'.format(addr, self._ifname) + elif is_ipv6(addr): + cmd = 'ip -6 addr del "{}" dev "{}"'.format(addr, self._ifname) + + self._cmd(cmd) + + + # replace dhcpv4/v6 with systemd.networkd? + def set_dhcpv4(self): + conf_file = dhclient_conf_dir + self._ifname + '.conf' + pidfile = dhclient_conf_dir + self._ifname + '.pid' + leasefile = dhclient_conf_dir + self._ifname + '.leases' + + a = [ + '# generated by interface_config.py', + 'option rfc3442-classless-static-routes code 121 = array of unsigned integer 8;', + 'interface \"' + self._ifname + '\" {', + '\tsend host-name \"' + socket.gethostname() + '\";', + '\trequest subnet-mask, broadcast-address, routers, domain-name-servers, rfc3442-classless-static-routes, domain-name, interface-mtu;', + '}' + ] + + cnf = "" + for ln in a: + cnf += str(ln + "\n") + open(conf_file, 'w').write(cnf) + if os.path.exists(dhclient_conf_dir + self._ifname + '.pid'): + try: + ret = subprocess.check_output( + ['/sbin/dhclient -4 -r -pf ' + pidfile], shell=True).decode() + except subprocess.CalledProcessError as e: + if self._debug(): + self._debug(e) + try: + ret = subprocess.check_output( + ['/sbin/dhclient -4 -q -nw -cf ' + conf_file + ' -pf ' + pidfile + ' -lf ' + leasefile + ' ' + self._ifname], shell=True).decode() + return True + except subprocess.CalledProcessError as e: + if self._debug(): + self._debug(e) + return None + + def del_dhcpv4(self): + conf_file = dhclient_conf_dir + self._ifname + '.conf' + pidfile = dhclient_conf_dir + self._ifname + '.pid' + leasefile = dhclient_conf_dir + self._ifname + '.leases' + if not os.path.exists(pidfile): + return 1 + try: + ret = subprocess.check_output( + ['/sbin/dhclient -4 -r -pf ' + pidfile], shell=True).decode() + return True + except subprocess.CalledProcessError as e: + if self._debug(): + self._debug(e) + return None + + def get_dhcpv4(self): + pidfile = dhclient_conf_dir + self._ifname + '.pid' + if not os.path.exists(pidfile): + print ( + "no dhcp client running on interface {0}".format(self._ifname)) + return False + else: + pid = open(pidfile, 'r').read() + print( + "dhclient running on {0} with pid {1}".format(self._ifname, pid)) + return True + + def set_dhcpv6(self): + conf_file = dhclient_conf_dir + self._ifname + '.v6conf' + pidfile = dhclient_conf_dir + self._ifname + '.v6pid' + leasefile = dhclient_conf_dir + self._ifname + '.v6leases' + a = [ + '# generated by interface_config.py', + 'interface \"' + self._ifname + '\" {', + '\trequest routers, domain-name-servers, domain-name;', + '}' + ] + cnf = "" + for ln in a: + cnf += str(ln + "\n") + open(conf_file, 'w').write(cnf) + subprocess.call( + ['sysctl', '-q', '-w', 'net.ipv6.conf.' + self._ifname + '.accept_ra=0']) + if os.path.exists(pidfile): + try: + ret = subprocess.check_output( + ['/sbin/dhclient -6 -q -x -pf ' + pidfile], shell=True).decode() + except subprocess.CalledProcessError as e: + if self._debug(): + self._debug(e) + try: + ret = subprocess.check_output( + ['/sbin/dhclient -6 -q -nw -cf ' + conf_file + ' -pf ' + pidfile + ' -lf ' + leasefile + ' ' + self._ifname], shell=True).decode() + return True + except subprocess.CalledProcessError as e: + if self._debug(): + self._debug(e) + return None + + def del_dhcpv6(self): + conf_file = dhclient_conf_dir + self._ifname + '.v6conf' + pidfile = dhclient_conf_dir + self._ifname + '.v6pid' + leasefile = dhclient_conf_dir + self._ifname + '.v6leases' + if not os.path.exists(pidfile): + return 1 + try: + ret = subprocess.check_output( + ['/sbin/dhclient -6 -q -x -pf ' + pidfile], shell=True).decode() + subprocess.call( + ['sysctl', '-q', '-w', 'net.ipv6.conf.' + self._ifname + '.accept_ra=1']) + return True + except subprocess.CalledProcessError as e: + if self._debug(): + self._debug(e) + return None + + def get_dhcpv6(self): + pidfile = dhclient_conf_dir + self._ifname + '.v6pid' + if not os.path.exists(pidfile): + print ( + "no dhcpv6 client running on interface {0}".format(self._ifname)) + return False + else: + pid = open(pidfile, 'r').read() + print( + "dhclientv6 running on {0} with pid {1}".format(self._ifname, pid)) + return True + + +# TODO: dhcpv6-pd via dhclient diff --git a/python/vyos/interfaceconfig.py b/python/vyos/interfaceconfig.py deleted file mode 100644 index 9790fae49..000000000 --- a/python/vyos/interfaceconfig.py +++ /dev/null @@ -1,471 +0,0 @@ -#!/usr/bin/python3 - -# Copyright 2019 VyOS maintainers and contributors -# -# This library is free software; you can redistribute it and/or -# modify it under the terms of the GNU Lesser General Public -# License as published by the Free Software Foundation; either -# version 2.1 of the License, or (at your option) any later version. -# -# This library is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -# Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public -# License along with this library. If not, see . - -import sys -import os -import re -import json -import socket -import subprocess -import ipaddress - -from vyos.validate import * -from ipaddress import IPv4Network, IPv6Address -from netifaces import ifaddresses, AF_INET, AF_INET6 - -dhclient_conf_dir = r'/var/lib/dhcp/dhclient_' - -class Interface: - def __init__(self, ifname=None, type=None): - """ - Create instance of an IP interface - - Example: - - from vyos.interfaceconfig import Interface - i = Interface('br111', type='bridge') - """ - - if not ifname: - raise Exception("interface name required") - - if not os.path.exists('/sys/class/net/{0}'.format(ifname)) and not type: - raise Exception("interface {0} not found".format(str(ifname))) - - if not os.path.exists('/sys/class/net/{0}'.format(ifname)): - try: - cmd = 'ip link add dev "{}" type "{}"'.format(ifname, type) - self._cmd(cmd) - except subprocess.CalledProcessError as e: - if self._debug(): - self._debug(e) - if "Operation not supported" in str(e.output.decode()): - print(str(e.output.decode())) - sys.exit(0) - - self._ifname = str(ifname) - - @property - def remove(self): - """ - Remove system interface - - Example: - - from vyos.interfaceconfig import Interface - i = Interface('br111', type='bridge') - i.remove - """ - - # NOTE (Improvement): - # after interface removal no other commands should be allowed - # to be called and instead should raise an Exception: - - cmd = 'ip link del dev "{}"'.format(self._ifname) - self._cmd(cmd) - - - def _cmd(self, command): - process = subprocess.Popen(command,stdout=subprocess.PIPE, shell=True) - proc_stdout = process.communicate()[0].strip() - pass - - - @property - def mtu(self): - """ - Get/set interface mtu in bytes. - - Example: - - from vyos.interfaceconfig import Interface - mtu = Interface('ens192').mtu - print(mtu) - """ - - mtu = 0 - with open('/sys/class/net/{0}/mtu'.format(self._ifname), 'r') as f: - mtu = f.read().rstrip('\n') - return mtu - - - @mtu.setter - def mtu(self, mtu=None): - """ - Get/set interface mtu in bytes. - - Example: - - from vyos.interfaceconfig import Interface - Interface('ens192').mtu = 1400 - """ - - if mtu < 68 or mtu > 9000: - raise ValueError('Invalid MTU size: "{}"'.format(mru)) - - with open('/sys/class/net/{0}/mtu'.format(self._ifname), 'w') as f: - f.write(str(mtu)) - - - @property - def mac(self): - """ - Get/set interface mac address - - Example: - - from vyos.interfaceconfig import Interface - mac = Interface('ens192').mac - """ - address = '' - with open('/sys/class/net/{0}/address'.format(self._ifname), 'r') as f: - address = f.read().rstrip('\n') - return address - - - @mac.setter - def mac(self, mac=None): - """ - Get/set interface mac address - - Example: - - from vyos.interfaceconfig import Interface - Interface('ens192').mac = '00:90:43:fe:fe:1b' - """ - # a mac address consits out of 6 octets - octets = len(mac.split(':')) - if octets != 6: - raise ValueError('wrong number of MAC octets: {} '.format(octets)) - - # validate against the first mac address byte if it's a multicast address - if int(mac.split(':')[0]) & 1: - raise ValueError('{} is a multicast MAC address'.format(mac)) - - # overall mac address is not allowed to be 00:00:00:00:00:00 - if sum(int(i, 16) for i in mac.split(':')) == 0: - raise ValueError('00:00:00:00:00:00 is not a valid MAC address') - - # check for VRRP mac address - if mac.split(':')[0] == '0' and addr.split(':')[1] == '0' and mac.split(':')[2] == '94' and mac.split(':')[3] == '0' and mac.split(':')[4] == '1': - raise ValueError('{} is a VRRP MAC address'.format(mac)) - - # Assemble command executed on system. Unfortunately there is no way - # of altering the MAC address via sysfs - cmd = 'ip link set dev "{}" address "{}"'.format(self._ifname, mac) - self._cmd(cmd) - - - @property - def ifalias(self): - """ - Get/set interface alias name - - Example: - - from vyos.interfaceconfig import Interface - alias = Interface('ens192').ifalias - """ - - alias = '' - with open('/sys/class/net/{0}/ifalias'.format(self._ifname), 'r') as f: - alias = f.read().rstrip('\n') - return alias - - - @ifalias.setter - def ifalias(self, ifalias=None): - """ - Get/set interface alias name - - Example: - - from vyos.interfaceconfig import Interface - Interface('ens192').ifalias = 'VyOS upstream interface' - - to clear interface alias e.g. delete it use: - - Interface('ens192').ifalias = '' - """ - - # clear interface alias - if not ifalias: - ifalias = '\0' - - with open('/sys/class/net/{0}/ifalias'.format(self._ifname), 'w') as f: - f.write(str(ifalias)) - - @property - def state(self): - """ - Enable (up) / Disable (down) an interface - - Example: - - from vyos.interfaceconfig import Interface - i = Interface('ens192').link - """ - - state = '' - with open('/sys/class/net/{0}/operstate'.format(self._ifname), 'r') as f: - state = f.read().rstrip('\n') - return state - - - @state.setter - def state(self, state=None): - - if state not in ['up', 'down']: - raise ValueError('state must be "up" or "down"') - - # Assemble command executed on system. Unfortunately there is no way - # to up/down an interface via sysfs - cmd = 'ip link set dev "{}" "{}"'.format(self._ifname, state) - self._cmd(cmd) - - - def _debug(self, e=None): - """ - export DEBUG=1 to see debug messages - """ - if os.getenv('DEBUG') == '1': - if e: - print ("Exception raised:\ncommand: {0}\nerror code: {1}\nsubprocess output: {2}".format( - e.cmd, e.returncode, e.output.decode())) - return True - return False - - - def get_addr(self): - """ - Retrieve assigned IPv4 and IPv6 addresses from given interface. - This is done using the netifaces and ipaddress python modules. - - Example: - - from vyos.interfaceconfig import Interface - i = Interface('ens192') - i.get_addrs() - ['172.16.33.30/24', 'fe80::20c:29ff:fe11:a174/64'] - """ - - ipv4 = [] - ipv6 = [] - - if AF_INET in ifaddresses(self._ifname).keys(): - for v4_addr in ifaddresses(self._ifname)[AF_INET]: - # we need to manually assemble a list of IPv4 address/prefix - prefix = '/' + str(IPv4Network('0.0.0.0/' + v4_addr['netmask']).prefixlen) - ipv4.append( v4_addr['addr'] + prefix ) - - if AF_INET6 in ifaddresses(self._ifname).keys(): - for v6_addr in ifaddresses(self._ifname)[AF_INET6]: - # Note that currently expanded netmasks are not supported. That means - # 2001:db00::0/24 is a valid argument while 2001:db00::0/ffff:ff00:: not. - # see https://docs.python.org/3/library/ipaddress.html - bits = bin( int(v6_addr['netmask'].replace(':',''), 16) ).count('1') - prefix = '/' + str(bits) - - # we alsoneed to remove the interface suffix on link local addresses - v6_addr['addr'] = v6_addr['addr'].split('%')[0] - ipv6.append( v6_addr['addr'] + prefix ) - - return ipv4 + ipv6 - - - def add_addr(self, addr=None): - """ - Add IP address to interface. Address is only added if it yet not added - to that interface. - - Example: - - >>> from vyos.interfaceconfig import Interface - >>> j = Interface('br100', type='bridge') - >>> j.add_addr('192.0.2.1/24') - >>> j.add_addr('2001:db8::ffff/64') - >>> j.get_addr() - ['192.0.2.1/24', '2001:db8::ffff/64'] - """ - - if not addr: - raise ValueError('No IP address specified') - - if not is_intf_addr_assigned(self._ifname, addr): - cmd = '' - if is_ipv4(addr): - cmd = 'sudo ip -4 addr add "{}" broadcast + dev "{}"'.format(addr, self._ifname) - elif is_ipv6(addr): - cmd = 'sudo ip -6 addr add "{}" dev "{}"'.format(addr, self._ifname) - - self._cmd(cmd) - - - def del_addr(self, addr=None): - """ - Remove IP address from interface. - - Example: - >>> from vyos.interfaceconfig import Interface - >>> j = Interface('br100', type='bridge') - >>> j.add_addr('2001:db8::ffff/64') - >>> j.add_addr('192.0.2.1/24') - >>> j.get_addr() - ['192.0.2.1/24', '2001:db8::ffff/64'] - >>> j.del_addr('192.0.2.1/24') - >>> j.get_addr() - ['2001:db8::ffff/64'] - """ - - if not addr: - raise ValueError('No IP address specified') - - if is_intf_addr_assigned(self._ifname, addr): - cmd = '' - if is_ipv4(addr): - cmd = 'ip -4 addr del "{}" dev "{}"'.format(addr, self._ifname) - elif is_ipv6(addr): - cmd = 'ip -6 addr del "{}" dev "{}"'.format(addr, self._ifname) - - self._cmd(cmd) - - - # replace dhcpv4/v6 with systemd.networkd? - def set_dhcpv4(self): - conf_file = dhclient_conf_dir + self._ifname + '.conf' - pidfile = dhclient_conf_dir + self._ifname + '.pid' - leasefile = dhclient_conf_dir + self._ifname + '.leases' - - a = [ - '# generated by interface_config.py', - 'option rfc3442-classless-static-routes code 121 = array of unsigned integer 8;', - 'interface \"' + self._ifname + '\" {', - '\tsend host-name \"' + socket.gethostname() + '\";', - '\trequest subnet-mask, broadcast-address, routers, domain-name-servers, rfc3442-classless-static-routes, domain-name, interface-mtu;', - '}' - ] - - cnf = "" - for ln in a: - cnf += str(ln + "\n") - open(conf_file, 'w').write(cnf) - if os.path.exists(dhclient_conf_dir + self._ifname + '.pid'): - try: - ret = subprocess.check_output( - ['/sbin/dhclient -4 -r -pf ' + pidfile], shell=True).decode() - except subprocess.CalledProcessError as e: - if self._debug(): - self._debug(e) - try: - ret = subprocess.check_output( - ['/sbin/dhclient -4 -q -nw -cf ' + conf_file + ' -pf ' + pidfile + ' -lf ' + leasefile + ' ' + self._ifname], shell=True).decode() - return True - except subprocess.CalledProcessError as e: - if self._debug(): - self._debug(e) - return None - - def del_dhcpv4(self): - conf_file = dhclient_conf_dir + self._ifname + '.conf' - pidfile = dhclient_conf_dir + self._ifname + '.pid' - leasefile = dhclient_conf_dir + self._ifname + '.leases' - if not os.path.exists(pidfile): - return 1 - try: - ret = subprocess.check_output( - ['/sbin/dhclient -4 -r -pf ' + pidfile], shell=True).decode() - return True - except subprocess.CalledProcessError as e: - if self._debug(): - self._debug(e) - return None - - def get_dhcpv4(self): - pidfile = dhclient_conf_dir + self._ifname + '.pid' - if not os.path.exists(pidfile): - print ( - "no dhcp client running on interface {0}".format(self._ifname)) - return False - else: - pid = open(pidfile, 'r').read() - print( - "dhclient running on {0} with pid {1}".format(self._ifname, pid)) - return True - - def set_dhcpv6(self): - conf_file = dhclient_conf_dir + self._ifname + '.v6conf' - pidfile = dhclient_conf_dir + self._ifname + '.v6pid' - leasefile = dhclient_conf_dir + self._ifname + '.v6leases' - a = [ - '# generated by interface_config.py', - 'interface \"' + self._ifname + '\" {', - '\trequest routers, domain-name-servers, domain-name;', - '}' - ] - cnf = "" - for ln in a: - cnf += str(ln + "\n") - open(conf_file, 'w').write(cnf) - subprocess.call( - ['sysctl', '-q', '-w', 'net.ipv6.conf.' + self._ifname + '.accept_ra=0']) - if os.path.exists(pidfile): - try: - ret = subprocess.check_output( - ['/sbin/dhclient -6 -q -x -pf ' + pidfile], shell=True).decode() - except subprocess.CalledProcessError as e: - if self._debug(): - self._debug(e) - try: - ret = subprocess.check_output( - ['/sbin/dhclient -6 -q -nw -cf ' + conf_file + ' -pf ' + pidfile + ' -lf ' + leasefile + ' ' + self._ifname], shell=True).decode() - return True - except subprocess.CalledProcessError as e: - if self._debug(): - self._debug(e) - return None - - def del_dhcpv6(self): - conf_file = dhclient_conf_dir + self._ifname + '.v6conf' - pidfile = dhclient_conf_dir + self._ifname + '.v6pid' - leasefile = dhclient_conf_dir + self._ifname + '.v6leases' - if not os.path.exists(pidfile): - return 1 - try: - ret = subprocess.check_output( - ['/sbin/dhclient -6 -q -x -pf ' + pidfile], shell=True).decode() - subprocess.call( - ['sysctl', '-q', '-w', 'net.ipv6.conf.' + self._ifname + '.accept_ra=1']) - return True - except subprocess.CalledProcessError as e: - if self._debug(): - self._debug(e) - return None - - def get_dhcpv6(self): - pidfile = dhclient_conf_dir + self._ifname + '.v6pid' - if not os.path.exists(pidfile): - print ( - "no dhcpv6 client running on interface {0}".format(self._ifname)) - return False - else: - pid = open(pidfile, 'r').read() - print( - "dhclientv6 running on {0} with pid {1}".format(self._ifname, pid)) - return True - - -# TODO: dhcpv6-pd via dhclient diff --git a/src/conf_mode/interface-bridge.py b/src/conf_mode/interface-bridge.py index 9918cbec7..d5661be93 100755 --- a/src/conf_mode/interface-bridge.py +++ b/src/conf_mode/interface-bridge.py @@ -23,7 +23,7 @@ from pyroute2 import IPDB from netifaces import interfaces from vyos.config import Config from vyos.validate import is_ip -from vyos.interfaceconfig import Interface as IF +from vyos.ifconfig import Interface as IF from vyos import ConfigError default_config_data = { -- cgit v1.2.3 From 76f498220c796a39a92ce3bed47c1cc5bac1a99d Mon Sep 17 00:00:00 2001 From: Dmytro Aleksandrov Date: Fri, 30 Aug 2019 18:43:44 +0300 Subject: [op-mode] T1621 rewrite misc commands to python/xml syntax --- op-mode-definitions/bandwidth-test.xml | 29 +++++ op-mode-definitions/disks.xml | 50 ++++++++ op-mode-definitions/generate-ssh-server-key.xml | 16 +++ op-mode-definitions/show-disk.xml | 23 ---- op-mode-definitions/show-history.xml | 31 +++++ op-mode-definitions/show-host.xml | 12 ++ op-mode-definitions/telnet.xml | 8 +- op-mode-definitions/terminal.xml | 93 +++++++++++++++ python/vyos/util.py | 8 ++ src/completion/list_disks.py | 36 ++++++ src/completion/list_disks.sh | 5 - src/helpers/vyos-sudo.py | 9 +- src/op_mode/format_disk.py | 148 ++++++++++++++++++++++++ src/op_mode/generate_ssh_server_key.py | 27 +++++ src/op_mode/toggle_help_binding.sh | 25 ++++ 15 files changed, 480 insertions(+), 40 deletions(-) create mode 100644 op-mode-definitions/bandwidth-test.xml create mode 100644 op-mode-definitions/disks.xml create mode 100644 op-mode-definitions/generate-ssh-server-key.xml delete mode 100644 op-mode-definitions/show-disk.xml create mode 100644 op-mode-definitions/show-history.xml create mode 100755 src/completion/list_disks.py delete mode 100755 src/completion/list_disks.sh create mode 100755 src/op_mode/format_disk.py create mode 100755 src/op_mode/generate_ssh_server_key.py create mode 100755 src/op_mode/toggle_help_binding.sh (limited to 'python/vyos') diff --git a/op-mode-definitions/bandwidth-test.xml b/op-mode-definitions/bandwidth-test.xml new file mode 100644 index 000000000..d1e459b17 --- /dev/null +++ b/op-mode-definitions/bandwidth-test.xml @@ -0,0 +1,29 @@ + + + + + + + Initiate or wait for bandwidth test + + + + + Wait for bandwidth test connections (port TCP/5001) + + iperf -s + + + + Initiate a bandwidth test to specified host (port TCP/5001) + + <hostname> <x.x.x.x> <h:h:h:h:h:h:h:h> + + + iperf -c $4 + + + + + + diff --git a/op-mode-definitions/disks.xml b/op-mode-definitions/disks.xml new file mode 100644 index 000000000..fb39c4f3c --- /dev/null +++ b/op-mode-definitions/disks.xml @@ -0,0 +1,50 @@ + + + + + Format a device + + + + + Format a disk drive + + + + + + + + Format this disk the same as another disk + + + + + ${vyos_op_scripts_dir}/format_disk.py --target $3 --proto $5 + + + + + + + + + + + Show status of disk device + + + + + + + + Show disk drive formatting + + ${vyos_op_scripts_dir}/show_disk_format.sh $3 + + + + + + diff --git a/op-mode-definitions/generate-ssh-server-key.xml b/op-mode-definitions/generate-ssh-server-key.xml new file mode 100644 index 000000000..a6ebf1b78 --- /dev/null +++ b/op-mode-definitions/generate-ssh-server-key.xml @@ -0,0 +1,16 @@ + + + + + Generate an object + + + + + Regenerate the host SSH keys and restart the SSH server + + ${vyos_op_scripts_dir}/generate_ssh_server_key.py + + + + diff --git a/op-mode-definitions/show-disk.xml b/op-mode-definitions/show-disk.xml deleted file mode 100644 index 37da07fbe..000000000 --- a/op-mode-definitions/show-disk.xml +++ /dev/null @@ -1,23 +0,0 @@ - - - - - - - Show status of disk device - - - - - - - - Show disk drive formatting - - ${vyos_op_scripts_dir}/show_disk_format.sh $3 - - - - - - diff --git a/op-mode-definitions/show-history.xml b/op-mode-definitions/show-history.xml new file mode 100644 index 000000000..7fb286264 --- /dev/null +++ b/op-mode-definitions/show-history.xml @@ -0,0 +1,31 @@ + + + + + + + Show command history + + HISTTIMEFORMAT='%FT%T%z ' HISTFILE="$HOME/.bash_history" \set -o history; history + + + + Show recent command history + + HISTTIMEFORMAT='%FT%T%z ' HISTFILE="$HOME/.bash_history" \set -o history; history 20 + + + + + + + Show last N commands in history + + <NUMBER> + + + HISTTIMEFORMAT='%FT%T%z ' HISTFILE="$HOME/.bash_history" \set -o history; history $3 + + + + diff --git a/op-mode-definitions/show-host.xml b/op-mode-definitions/show-host.xml index d7f8104aa..eee1288a1 100644 --- a/op-mode-definitions/show-host.xml +++ b/op-mode-definitions/show-host.xml @@ -7,6 +7,12 @@ Show host information + + + Show host current date + + /bin/date + Show domain name @@ -25,6 +31,12 @@ /usr/bin/host $4 + + + Show host operating system details + + /bin/uname -a + diff --git a/op-mode-definitions/telnet.xml b/op-mode-definitions/telnet.xml index 7ec75be88..c5bb6d283 100644 --- a/op-mode-definitions/telnet.xml +++ b/op-mode-definitions/telnet.xml @@ -5,23 +5,23 @@ Telnet to a node - + Telnet to a host <hostname> <x.x.x.x> <h:h:h:h:h:h:h:h> - /usr/bin/telnet $2 + /usr/bin/telnet $3 - + Telnet to a host:port <0-65535> - /usr/bin/telnet $2 $3 + /usr/bin/telnet $3 $5 diff --git a/op-mode-definitions/terminal.xml b/op-mode-definitions/terminal.xml index db74f867e..9c4e629cb 100644 --- a/op-mode-definitions/terminal.xml +++ b/op-mode-definitions/terminal.xml @@ -26,4 +26,97 @@ + + + Set operational options + + + + + Bash builtin set command + + <OPTION> + + + builtin $3 + + + + + Control console behaviors + + + + + Reconfigure console keyboard layout + + sudo dpkg-reconfigure -f dialog keyboard-configuration && sudo systemctl restart keyboard-setup + + + + + + + Control terminal behaviors + + + + + + Set key behaviors + + + + + Enable/disable getting help using question mark (default enabled) + + enable disable + + + ${vyos_op_scripts_dir}/toggle_help_binding.sh $5 + + + + + + + Set terminal pager to default (less) + + VYATTA_PAGER=${_vyatta_default_pager} + + + + Set terminal pager + + <PROGRAM> + + + VYATTA_PAGER=$4 + + + + + Set terminal to given number of rows (0 disables paging) + + <NUMBER> + + + if [ "$4" -eq 0 ]; then VYATTA_PAGER=cat; else VYATTA_PAGER=${_vyatta_default_pager}; stty rows $4; fi + + + + + Set terminal to given number of columns + + <NUMBER> + + + stty columns $4 + + + + + + + diff --git a/python/vyos/util.py b/python/vyos/util.py index 88bb0c8f4..67a602f7a 100644 --- a/python/vyos/util.py +++ b/python/vyos/util.py @@ -15,6 +15,7 @@ import os import re +import getpass import grp import time import subprocess @@ -191,3 +192,10 @@ def ask_yes_no(question, default=False) -> bool: return False else: sys.stdout.write("Please respond with yes/y or no/n\n") + + +def is_admin() -> bool: + """Look if current user is in sudo group""" + current_user = getpass.getuser() + (_, _, _, admin_group_members) = grp.getgrnam('sudo') + return current_user in admin_group_members diff --git a/src/completion/list_disks.py b/src/completion/list_disks.py new file mode 100755 index 000000000..ff1135e23 --- /dev/null +++ b/src/completion/list_disks.py @@ -0,0 +1,36 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2019 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +# Completion script used by show disks to collect physical disk + +import argparse + +parser = argparse.ArgumentParser() +parser.add_argument("-e", "--exclude", type=str, help="Exclude specified device from the result list") +args = parser.parse_args() + +disks = set() +with open('/proc/partitions') as partitions_file: + for line in partitions_file: + fields = line.strip().split() + if len(fields) == 4 and fields[3].isalpha() and fields[3] != 'name': + disks.add(fields[3]) + +if args.exclude: + disks.remove(args.exclude) + +for disk in disks: + print(disk) diff --git a/src/completion/list_disks.sh b/src/completion/list_disks.sh deleted file mode 100755 index f32e558fd..000000000 --- a/src/completion/list_disks.sh +++ /dev/null @@ -1,5 +0,0 @@ -#!/bin/bash - -# Completion script used by show disks to collect physical disk - -awk 'NR > 2 && $4 !~ /[0-9]$/ { print $4 }' . -import getpass -import grp import os import sys - -def is_admin() -> bool: - """Look if current user is in sudo group""" - current_user = getpass.getuser() - (_, _, _, admin_group_members) = grp.getgrnam('sudo') - return current_user in admin_group_members +from vyos.util import is_admin if __name__ == '__main__': diff --git a/src/op_mode/format_disk.py b/src/op_mode/format_disk.py new file mode 100755 index 000000000..5a3b250ee --- /dev/null +++ b/src/op_mode/format_disk.py @@ -0,0 +1,148 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2019 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import argparse +import os +import re +import subprocess +import sys +from datetime import datetime +from time import sleep + +from vyos.util import is_admin, ask_yes_no + + +def list_disks(): + disks = set() + with open('/proc/partitions') as partitions_file: + for line in partitions_file: + fields = line.strip().split() + if len(fields) == 4 and fields[3].isalpha() and fields[3] != 'name': + disks.add(fields[3]) + return disks + + +def is_busy(disk: str): + """Check if given disk device is busy by re-reading it's partition table""" + + cmd = 'sudo blockdev --rereadpt /dev/{}'.format(disk) + status = subprocess.call([cmd], shell=True, stderr=subprocess.DEVNULL) + return status != 0 + + +def backup_partitions(disk: str): + """Save sfdisk partitions output to a backup file""" + + device_path = '/dev/' + disk + backup_ts = datetime.now().strftime('%Y-%m-%d-%H:%M') + backup_file = '/var/tmp/backup_{}.{}'.format(disk, backup_ts) + cmd = 'sudo /sbin/sfdisk -d {} > {}'.format(device_path, backup_file) + subprocess.check_call([cmd], shell=True) + + +def list_partitions(disk: str): + """List partition numbers of a given disk""" + + parts = set() + part_num_expr = re.compile(disk + '([0-9]+)') + with open('/proc/partitions') as partitions_file: + for line in partitions_file: + fields = line.strip().split() + if len(fields) == 4 and fields[3] != 'name' and part_num_expr.match(fields[3]): + part_idx = part_num_expr.match(fields[3]).group(1) + parts.add(int(part_idx)) + return parts + + +def delete_partition(disk: str, partition_idx: int): + cmd = 'sudo /sbin/parted /dev/{} rm {}'.format(disk, partition_idx) + subprocess.check_call([cmd], shell=True) + + +def format_disk_like(target: str, proto: str): + cmd = 'sudo /sbin/sfdisk -d /dev/{} | sudo /sbin/sfdisk --force /dev/{}'.format(proto, target) + subprocess.check_call([cmd], shell=True) + + +if __name__ == '__main__': + parser = argparse.ArgumentParser() + group = parser.add_argument_group() + group.add_argument('-t', '--target', type=str, required=True, help='Target device to format') + group.add_argument('-p', '--proto', type=str, required=True, help='Prototype device to use as reference') + args = parser.parse_args() + + if not is_admin(): + print('Must be admin or root to format disk') + sys.exit(1) + + target_disk = args.target + eligible_target_disks = list_disks() + + proto_disk = args.proto + eligible_proto_disks = eligible_target_disks.copy() + eligible_proto_disks.remove(target_disk) + + fmt = { + 'target_disk': target_disk, + 'proto_disk': proto_disk, + } + + if proto_disk == target_disk: + print('The two disk drives must be different.') + sys.exit(1) + + if not os.path.exists('/dev/' + proto_disk): + print('Device /dev/{proto_disk} does not exist'.format_map(fmt)) + sys.exit(1) + + if not os.path.exists('/dev/' + target_disk): + print('Device /dev/{target_disk} does not exist'.format_map(fmt)) + sys.exit(1) + + if target_disk not in eligible_target_disks: + print('Device {target_disk} can not be formatted'.format_map(fmt)) + sys.exit(1) + + if proto_disk not in eligible_proto_disks: + print('Device {proto_disk} can not be used as a prototype for {target_disk}'.format_map(fmt)) + sys.exit(1) + + if is_busy(target_disk): + print("Disk device {target_disk} is busy. Can't format it now".format_map(fmt)) + sys.exit(1) + + print('This will re-format disk {target_disk} so that it has the same disk\n' + 'partion sizes and offsets as {proto_disk}. This will not copy\n' + 'data from {proto_disk} to {target_disk}. But this will erase all\n' + 'data on {target_disk}.\n'.format_map(fmt)) + + if not ask_yes_no("Do you wish to proceed?"): + print('OK. Disk drive {target_disk} will not be re-formated'.format_map(fmt)) + sys.exit(0) + + print('OK. Re-formating disk drive {target_disk}...'.format_map(fmt)) + + print('Making backup copy of partitions...') + backup_partitions(target_disk) + sleep(1) + + print('Deleting old partitions...') + for p in list_partitions(target_disk): + delete_partition(disk=target_disk, partition_idx=p) + + print('Creating new partitions on {target_disk} based on {proto_disk}...'.format_map(fmt)) + format_disk_like(target=target_disk, proto=proto_disk) + print('Done.') diff --git a/src/op_mode/generate_ssh_server_key.py b/src/op_mode/generate_ssh_server_key.py new file mode 100755 index 000000000..f205919b8 --- /dev/null +++ b/src/op_mode/generate_ssh_server_key.py @@ -0,0 +1,27 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2019 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import subprocess +import sys + +from vyos.util import ask_yes_no + +if not ask_yes_no('Do you really want to remove the existing SSH host keys?'): + sys.exit(0) +else: + subprocess.check_call(['sudo rm -v /etc/ssh/ssh_host_*'], shell=True) + subprocess.check_call(['sudo dpkg-reconfigure openssh-server'], shell=True) + subprocess.check_call(['sudo systemctl restart ssh'], shell=True) diff --git a/src/op_mode/toggle_help_binding.sh b/src/op_mode/toggle_help_binding.sh new file mode 100755 index 000000000..a8708f3da --- /dev/null +++ b/src/op_mode/toggle_help_binding.sh @@ -0,0 +1,25 @@ +#!/bin/bash +# +# Copyright (C) 2019 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +# Script for [un-]binding the question mark key for getting help +if [ "$1" == 'disable' ]; then + sed -i "/^bind '\"?\": .* # vyatta key binding$/d" $HOME/.bashrc + echo "bind '\"?\": self-insert' # vyatta key binding" >> $HOME/.bashrc + bind '"?": self-insert' +else + sed -i "/^bind '\"?\": .* # vyatta key binding$/d" $HOME/.bashrc + bind '"?": possible-completions' +fi -- cgit v1.2.3 From f8aab22927274ab6ba210be946b368f27ee75582 Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Fri, 30 Aug 2019 22:46:41 +0200 Subject: Python/ifconfig: remove ipv4/ipv6 handling, iproute2 autodetects addr family --- python/vyos/ifconfig.py | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) (limited to 'python/vyos') diff --git a/python/vyos/ifconfig.py b/python/vyos/ifconfig.py index 5f28125af..987f0b13e 100644 --- a/python/vyos/ifconfig.py +++ b/python/vyos/ifconfig.py @@ -326,12 +326,7 @@ class Interface: raise ValueError('No IP address specified') if not is_intf_addr_assigned(self._ifname, addr): - cmd = '' - if is_ipv4(addr): - cmd = 'sudo ip -4 addr add "{}" broadcast + dev "{}"'.format(addr, self._ifname) - elif is_ipv6(addr): - cmd = 'sudo ip -6 addr add "{}" dev "{}"'.format(addr, self._ifname) - + cmd = 'sudo ip addr add "{}" dev "{}"'.format(addr, self._ifname) self._cmd(cmd) @@ -355,12 +350,7 @@ class Interface: raise ValueError('No IP address specified') if is_intf_addr_assigned(self._ifname, addr): - cmd = '' - if is_ipv4(addr): - cmd = 'ip -4 addr del "{}" dev "{}"'.format(addr, self._ifname) - elif is_ipv6(addr): - cmd = 'ip -6 addr del "{}" dev "{}"'.format(addr, self._ifname) - + cmd = 'ip addr del "{}" dev "{}"'.format(addr, self._ifname) self._cmd(cmd) -- cgit v1.2.3 From 13fb3d9da7bb61dcd593c440ae798fd75474ebed Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Fri, 30 Aug 2019 22:51:55 +0200 Subject: Python/ifconfig: cleanup import statements --- python/vyos/ifconfig.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) (limited to 'python/vyos') diff --git a/python/vyos/ifconfig.py b/python/vyos/ifconfig.py index 987f0b13e..d9f28c8ef 100644 --- a/python/vyos/ifconfig.py +++ b/python/vyos/ifconfig.py @@ -17,9 +17,6 @@ import sys import os -import re -import json -import socket import subprocess import ipaddress @@ -360,11 +357,15 @@ class Interface: pidfile = dhclient_conf_dir + self._ifname + '.pid' leasefile = dhclient_conf_dir + self._ifname + '.leases' + hostname = 'vyos' + with open('/etc/hostname', 'r') as f: + hostname = f.read().rstrip('\n') + a = [ '# generated by interface_config.py', 'option rfc3442-classless-static-routes code 121 = array of unsigned integer 8;', 'interface \"' + self._ifname + '\" {', - '\tsend host-name \"' + socket.gethostname() + '\";', + '\tsend host-name \"' + hostname + '\";', '\trequest subnet-mask, broadcast-address, routers, domain-name-servers, rfc3442-classless-static-routes, domain-name, interface-mtu;', '}' ] -- cgit v1.2.3 From 56933983373634d8c56daeca7abd47feb7a5c791 Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Sat, 31 Aug 2019 13:00:40 +0200 Subject: Python/ifconfig: T1557: fix DHCP/DHCPv6 daemon and add Bridge/Dummy interface --- python/vyos/ifconfig.py | 843 ++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 673 insertions(+), 170 deletions(-) (limited to 'python/vyos') diff --git a/python/vyos/ifconfig.py b/python/vyos/ifconfig.py index d9f28c8ef..0cd27592a 100644 --- a/python/vyos/ifconfig.py +++ b/python/vyos/ifconfig.py @@ -1,5 +1,3 @@ -#!/usr/bin/python3 - # Copyright 2019 VyOS maintainers and contributors # # This library is free software; you can redistribute it and/or @@ -19,44 +17,78 @@ import sys import os import subprocess import ipaddress +import jinja2 from vyos.validate import * from ipaddress import IPv4Network, IPv6Address from netifaces import ifaddresses, AF_INET, AF_INET6 - -dhclient_conf_dir = r'/var/lib/dhcp/dhclient_' +from time import sleep + +dhcp_cfg = """ +# generated by ifconfig.py +option rfc3442-classless-static-routes code 121 = array of unsigned integer 8; +interface "{{ intf }}" { + send host-name "{{ hostname }}"; + request subnet-mask, broadcast-address, routers, domain-name-servers, rfc3442-classless-static-routes, domain-name, interface-mtu; +} +""" + +dhcpv6_cfg = """ +# generated by ifconfig.py +interface "{{ intf }}" { + request routers, domain-name-servers, domain-name; +} +""" + +dhclient_base = r'/var/lib/dhcp/dhclient_' class Interface: - def __init__(self, ifname=None, type=None): + def __init__(self, ifname=None, type=None, debug=False): """ Create instance of an IP interface Example: >>> from vyos.ifconfig import Interface - >>> i = Interface('br111', type='bridge') + >>> i = Interface('eth0') """ if not ifname: - raise Exception("interface name required") - - if not os.path.exists('/sys/class/net/{0}'.format(ifname)) and not type: - raise Exception("interface {0} not found".format(str(ifname))) - - if not os.path.exists('/sys/class/net/{0}'.format(ifname)): - try: - cmd = 'ip link add dev "{}" type "{}"'.format(ifname, type) - self._cmd(cmd) - except subprocess.CalledProcessError as e: - if self._debug(): - self._debug(e) - if "Operation not supported" in str(e.output.decode()): - print(str(e.output.decode())) - sys.exit(0) + raise Exception('interface name required') + + if not os.path.exists('/sys/class/net/{}'.format(ifname)) and not type: + raise Exception('interface "{}" not found'.format(str(ifname))) + # variable already referenced from _debug() + self._debug = debug self._ifname = str(ifname) - @property + if not os.path.exists('/sys/class/net/{}'.format(ifname)): + cmd = 'ip link add dev "{}" type "{}"'.format(ifname, type) + self._cmd(cmd) + + # per interface DHCP config files + self._dhcp_cfg_file = dhclient_base + self._ifname + '.conf' + self._dhcp_pid_file = dhclient_base + self._ifname + '.pid' + self._dhcp_lease_file = dhclient_base + self._ifname + '.leases' + + # per interface DHCPv6 config files + self._dhcpv6_cfg_file = dhclient_base + self._ifname + '.v6conf' + self._dhcpv6_pid_file = dhclient_base + self._ifname + '.v6pid' + self._dhcpv6_lease_file = dhclient_base + self._ifname + '.v6leases' + + + def _debug_msg(self, msg): + if self._debug: + print('"DEBUG/{}: {}'.format(self._ifname, msg)) + + + def set_debug(self, debug): + if debug not in [True, False]: + raise ValueError('must specify True or False for debug') + self._debug = debug + + def remove(self): """ Remove system interface @@ -64,21 +96,28 @@ class Interface: Example: >>> from vyos.ifconfig import Interface - >>> i = Interface('br111', type='bridge') - >>> i.remove + >>> i = Interface('eth0') + >>> i.remove() """ + # stop DHCP(v6) if running + self.del_dhcp() + self.del_dhcpv6() + # NOTE (Improvement): # after interface removal no other commands should be allowed # to be called and instead should raise an Exception: - cmd = 'ip link del dev "{}"'.format(self._ifname) self._cmd(cmd) def _cmd(self, command): + self._debug_msg(command) + process = subprocess.Popen(command,stdout=subprocess.PIPE, shell=True) proc_stdout = process.communicate()[0].strip() + + # add exception handling code pass @@ -90,7 +129,7 @@ class Interface: Example: >>> from vyos.ifconfig import Interface - >>> Interface('eth1').mtu + >>> Interface('eth0').mtu '1500' """ @@ -108,8 +147,8 @@ class Interface: Example: >>> from vyos.ifconfig import Interface - >>> Interface('br100', type='bridge').mtu = 1400 - >>> Interface('br100').mtu + >>> Interface('eth0').mtu = 1400 + >>> Interface('eth0').mtu '1400' """ @@ -128,7 +167,7 @@ class Interface: Example: >>> from vyos.ifconfig import Interface - >>> Interface('eth1').mac + >>> Interface('eth0').mac '00:0c:29:11:aa:cc' """ address = '' @@ -145,8 +184,8 @@ class Interface: Example: >>> from vyos.ifconfig import Interface - >>> Interface('eth1').mac = '00:90:43:fe:fe:1b' - >>> Interface('eth1').mac + >>> Interface('eth0').mac = '00:90:43:fe:fe:1b' + >>> Interface('eth0').mac '00:90:43:fe:fe:1b' """ # a mac address consits out of 6 octets @@ -172,6 +211,97 @@ class Interface: self._cmd(cmd) + @property + def arp_cache_tmo(self): + """ + Get configured ARP cache timeout value from interface. Example shows + default value of 30 seconds. + + Example: + + >>> from vyos.ifconfig import Interface + >>> Interface('eth0').arp_cache_tmo + '30000' + """ + + alias = '' + with open('/proc/sys/net/ipv4/neigh/{0}/base_reachable_time_ms'.format(self._ifname), 'r') as f: + alias = f.read().rstrip('\n') + return alias + + + @arp_cache_tmo.setter + def arp_cache_tmo(self, tmo=None): + """ + Set ARP cache timeout value in seconds for this. + + Example: + + >>> from vyos.ifconfig import Interface + >>> Interface('eth0').arp_cache_tmo = '40000' + """ + + # clear interface alias + if not tmo: + raise ValueError('Timeout value required') + + # Kernel interface is on milli seconds + tmo = int(tmo) * 1000 + with open('/proc/sys/net/ipv4/neigh/{0}/base_reachable_time_ms'.format(self._ifname), 'w') as f: + f.write(str(tmo)) + + @property + def link_detect(self): + """ + How does the kernel act when receiving packets on 'down' interfaces + + Example: + + >>> from vyos.ifconfig import Interface + >>> Interface('eth0').link_detect + '0' + """ + + alias = '' + with open('/proc/sys/net/ipv4/conf/{0}/link_filter'.format(self._ifname), 'r') as f: + alias = f.read().rstrip('\n') + return alias + + + @link_detect.setter + def link_detect(self, link_filter=None): + """ + Konfigure kernel response in packets received on interfaces that are 'down' + + 0 - Allow packets to be received for the address on this interface + even if interface is disabled or no carrier. + + 1 - Ignore packets received if interface associated with the incoming + address is down. + + 2 - Ignore packets received if interface associated with the incoming + address is down or has no carrier. + + Default value is 0. Note that some distributions enable it in startup + scripts. + + Example: + + >>> from vyos.ifconfig import Interface + >>> Interface('eth0').link_detect = '1' + """ + + # clear interface alias + if not link_filter: + raise ValueError() + + if link_filter >= 0 and link_filter <= 2: + with open('/proc/sys/net/ipv4/conf/{0}/link_filter'.format(self._ifname), 'w') as f: + f.write(str(link_filter)) + else: + raise ValueError() + + @property def ifalias(self): """ @@ -180,7 +310,7 @@ class Interface: Example: >>> from vyos.ifconfig import Interface - >>> Interface('eth1').ifalias + >>> Interface('eth0').ifalias '' """ @@ -198,14 +328,14 @@ class Interface: Example: >>> from vyos.ifconfig import Interface - >>> Interface('eth1').ifalias = 'VyOS upstream interface' - >>> Interface('eth1').ifalias + >>> Interface('eth0').ifalias = 'VyOS upstream interface' + >>> Interface('eth0').ifalias 'VyOS upstream interface' to clear interface alias e.g. delete it use: - >>> Interface('eth1').ifalias = '' - >>> Interface('eth1').ifalias + >>> Interface('eth0').ifalias = '' + >>> Interface('eth0').ifalias '' """ @@ -216,6 +346,7 @@ class Interface: with open('/sys/class/net/{0}/ifalias'.format(self._ifname), 'w') as f: f.write(str(ifalias)) + @property def state(self): """ @@ -224,7 +355,7 @@ class Interface: Example: >>> from vyos.ifconfig import Interface - >>> Interface('eth1').state + >>> Interface('eth0').state 'up' """ @@ -242,8 +373,8 @@ class Interface: Example: >>> from vyos.ifconfig import Interface - >>> Interface('eth1').state = 'down' - >>> Interface('eth1').state + >>> Interface('eth0').state = 'down' + >>> Interface('eth0').state 'down' """ @@ -256,18 +387,6 @@ class Interface: self._cmd(cmd) - def _debug(self, e=None): - """ - export DEBUG=1 to see debug messages - """ - if os.getenv('DEBUG') == '1': - if e: - print ("Exception raised:\ncommand: {0}\nerror code: {1}\nsubprocess output: {2}".format( - e.cmd, e.returncode, e.output.decode())) - return True - return False - - def get_addr(self): """ Retrieve assigned IPv4 and IPv6 addresses from given interface. @@ -276,7 +395,7 @@ class Interface: Example: >>> from vyos.ifconfig import Interface - >>> Interface('eth1').get_addrs() + >>> Interface('eth0').get_addrs() ['172.16.33.30/24', 'fe80::20c:29ff:fe11:a174/64'] """ @@ -311,8 +430,8 @@ class Interface: Example: - >>> from vyos.interfaceconfig import Interface - >>> j = Interface('br100', type='bridge') + >>> from vyos.ifconfig import Interface + >>> j = Interface('eth0') >>> j.add_addr('192.0.2.1/24') >>> j.add_addr('2001:db8::ffff/64') >>> j.get_addr() @@ -332,8 +451,9 @@ class Interface: Remove IP address from interface. Example: - >>> from vyos.interfaceconfig import Interface - >>> j = Interface('br100', type='bridge') + + >>> from vyos.ifconfig import Interface + >>> j = Interface('eth0') >>> j.add_addr('2001:db8::ffff/64') >>> j.add_addr('192.0.2.1/24') >>> j.get_addr() @@ -352,131 +472,514 @@ class Interface: # replace dhcpv4/v6 with systemd.networkd? - def set_dhcpv4(self): - conf_file = dhclient_conf_dir + self._ifname + '.conf' - pidfile = dhclient_conf_dir + self._ifname + '.pid' - leasefile = dhclient_conf_dir + self._ifname + '.leases' + def set_dhcp(self): + """ + Configure interface as DHCP client. The dhclient binary is automatically + started in background! - hostname = 'vyos' + Example: + + >>> from vyos.ifconfig import Interface + >>> j = Interface('eth0') + >>> j.set_dhcp() + """ + + dhcp = { + 'hostname': 'vyos', + 'intf': self._ifname + } + + # read configured system hostname. + # maybe change to vyos hostd client ??? with open('/etc/hostname', 'r') as f: - hostname = f.read().rstrip('\n') - - a = [ - '# generated by interface_config.py', - 'option rfc3442-classless-static-routes code 121 = array of unsigned integer 8;', - 'interface \"' + self._ifname + '\" {', - '\tsend host-name \"' + hostname + '\";', - '\trequest subnet-mask, broadcast-address, routers, domain-name-servers, rfc3442-classless-static-routes, domain-name, interface-mtu;', - '}' - ] - - cnf = "" - for ln in a: - cnf += str(ln + "\n") - open(conf_file, 'w').write(cnf) - if os.path.exists(dhclient_conf_dir + self._ifname + '.pid'): - try: - ret = subprocess.check_output( - ['/sbin/dhclient -4 -r -pf ' + pidfile], shell=True).decode() - except subprocess.CalledProcessError as e: - if self._debug(): - self._debug(e) - try: - ret = subprocess.check_output( - ['/sbin/dhclient -4 -q -nw -cf ' + conf_file + ' -pf ' + pidfile + ' -lf ' + leasefile + ' ' + self._ifname], shell=True).decode() - return True - except subprocess.CalledProcessError as e: - if self._debug(): - self._debug(e) - return None + dhcp['hostname'] = f.read().rstrip('\n') + + # render DHCP configuration + tmpl = jinja2.Template(dhcp_cfg) + dhcp_text = tmpl.render(dhcp) + with open(self._dhcp_cfg_file, 'w') as f: + f.write(dhcp_text) + + cmd = 'start-stop-daemon --start --quiet --pidfile ' + self._dhcp_pid_file + cmd += ' --exec /sbin/dhclient --' + # now pass arguments to dhclient binary + cmd += ' -4 -nw -cf {} -pf {} -lf {} {}'.format(self._dhcp_cfg_file, self._dhcp_pid_file, self._dhcp_lease_file, self._ifname) + self._cmd(cmd) - def del_dhcpv4(self): - conf_file = dhclient_conf_dir + self._ifname + '.conf' - pidfile = dhclient_conf_dir + self._ifname + '.pid' - leasefile = dhclient_conf_dir + self._ifname + '.leases' - if not os.path.exists(pidfile): - return 1 - try: - ret = subprocess.check_output( - ['/sbin/dhclient -4 -r -pf ' + pidfile], shell=True).decode() - return True - except subprocess.CalledProcessError as e: - if self._debug(): - self._debug(e) - return None - def get_dhcpv4(self): - pidfile = dhclient_conf_dir + self._ifname + '.pid' - if not os.path.exists(pidfile): - print ( - "no dhcp client running on interface {0}".format(self._ifname)) - return False + def del_dhcp(self): + """ + De-configure interface as DHCP clinet. All auto generated files like + pid, config and lease will be removed. + + Example: + + >>> from vyos.ifconfig import Interface + >>> j = Interface('eth0') + >>> j.del_dhcp() + """ + + pid = 0 + if os.path.isfile(self._dhcp_pid_file): + with open(self._dhcp_pid_file, 'r') as f: + pid = int(f.read()) else: - pid = open(pidfile, 'r').read() - print( - "dhclient running on {0} with pid {1}".format(self._ifname, pid)) - return True + self._debug_msg('No DHCP client PID found') + return None + + # stop dhclient + cmd = 'start-stop-daemon --stop --quiet --pidfile {}'.format(self._dhcp_pid_file) + self._cmd(cmd) + + # cleanup old config file + if os.path.isfile(self._dhcp_cfg_file): + os.remove(self._dhcp_cfg_file) + + # cleanup old pid file + if os.path.isfile(self._dhcp_pid_file): + os.remove(self._dhcp_pid_file) + + # cleanup old lease file + if os.path.isfile(self._dhcp_lease_file): + os.remove(self._dhcp_lease_file) + def set_dhcpv6(self): - conf_file = dhclient_conf_dir + self._ifname + '.v6conf' - pidfile = dhclient_conf_dir + self._ifname + '.v6pid' - leasefile = dhclient_conf_dir + self._ifname + '.v6leases' - a = [ - '# generated by interface_config.py', - 'interface \"' + self._ifname + '\" {', - '\trequest routers, domain-name-servers, domain-name;', - '}' - ] - cnf = "" - for ln in a: - cnf += str(ln + "\n") - open(conf_file, 'w').write(cnf) - subprocess.call( - ['sysctl', '-q', '-w', 'net.ipv6.conf.' + self._ifname + '.accept_ra=0']) - if os.path.exists(pidfile): - try: - ret = subprocess.check_output( - ['/sbin/dhclient -6 -q -x -pf ' + pidfile], shell=True).decode() - except subprocess.CalledProcessError as e: - if self._debug(): - self._debug(e) - try: - ret = subprocess.check_output( - ['/sbin/dhclient -6 -q -nw -cf ' + conf_file + ' -pf ' + pidfile + ' -lf ' + leasefile + ' ' + self._ifname], shell=True).decode() - return True - except subprocess.CalledProcessError as e: - if self._debug(): - self._debug(e) - return None + """ + Configure interface as DHCPv6 client. The dhclient binary is automatically + started in background! + + Example: + + >>> from vyos.ifconfig import Interface + >>> j = Interface('eth0') + >>> j.set_dhcpv6() + """ + + dhcpv6 = { + 'intf': self._ifname + } + + # render DHCP configuration + tmpl = jinja2.Template(dhcpv6_cfg) + dhcpv6_text = tmpl.render(dhcpv6) + with open(self._dhcpv6_cfg_file, 'w') as f: + f.write(dhcpv6_text) + + # https://bugs.launchpad.net/ubuntu/+source/ifupdown/+bug/1447715 + # + # wee need to wait for IPv6 DAD to finish once and interface is added + # this suxx :-( + sleep(5) + + # no longer accept router announcements on this interface + cmd = 'sysctl -q -w net.ipv6.conf.{}.accept_ra=0'.format(self._ifname) + self._cmd(cmd) + + cmd = 'start-stop-daemon --start --quiet --pidfile ' + self._dhcpv6_pid_file + cmd += ' --exec /sbin/dhclient --' + # now pass arguments to dhclient binary + cmd += ' -6 -nw -cf {} -pf {} -lf {} {}'.format(self._dhcpv6_cfg_file, self._dhcpv6_pid_file, self._dhcpv6_lease_file, self._ifname) + self._cmd(cmd) + def del_dhcpv6(self): - conf_file = dhclient_conf_dir + self._ifname + '.v6conf' - pidfile = dhclient_conf_dir + self._ifname + '.v6pid' - leasefile = dhclient_conf_dir + self._ifname + '.v6leases' - if not os.path.exists(pidfile): - return 1 - try: - ret = subprocess.check_output( - ['/sbin/dhclient -6 -q -x -pf ' + pidfile], shell=True).decode() - subprocess.call( - ['sysctl', '-q', '-w', 'net.ipv6.conf.' + self._ifname + '.accept_ra=1']) - return True - except subprocess.CalledProcessError as e: - if self._debug(): - self._debug(e) + """ + De-configure interface as DHCPv6 clinet. All auto generated files like + pid, config and lease will be removed. + + Example: + + >>> from vyos.ifconfig import Interface + >>> j = Interface('eth0') + >>> j.del_dhcpv6() + """ + + pid = 0 + if os.path.isfile(self._dhcpv6_pid_file): + with open(self._dhcpv6_pid_file, 'r') as f: + pid = int(f.read()) + else: + self._debug_msg('No DHCPv6 client PID found') return None - def get_dhcpv6(self): - pidfile = dhclient_conf_dir + self._ifname + '.v6pid' - if not os.path.exists(pidfile): - print ( - "no dhcpv6 client running on interface {0}".format(self._ifname)) - return False + # stop dhclient + cmd = 'start-stop-daemon --stop --quiet --pidfile {}'.format(self._dhcpv6_pid_file) + self._cmd(cmd) + + # accept router announcements on this interface + cmd = 'sysctl -q -w net.ipv6.conf.{}.accept_ra=1'.format(self._ifname) + self._cmd(cmd) + + # cleanup old config file + if os.path.isfile(self._dhcpv6_cfg_file): + os.remove(self._dhcpv6_cfg_file) + + # cleanup old pid file + if os.path.isfile(self._dhcpv6_pid_file): + os.remove(self._dhcpv6_pid_file) + + # cleanup old lease file + if os.path.isfile(self._dhcpv6_lease_file): + os.remove(self._dhcpv6_lease_file) + + +class DummyIf(Interface): + def __init__(self, ifname=None): + super().__init__(ifname, type='dummy') + + +class BridgeIf(Interface): + def __init__(self, ifname=None): + super().__init__(ifname, type='bridge') + + @property + def ageing_time(self): + """ + Get bridge aging time in seconds. + + Example: + + >>> from vyos.ifconfig import Interface + >>> BridgeIf('br0').aging_time + '3' + """ + + time = 0 + with open('/sys/class/net/{0}/bridge/ageing_time'.format(self._ifname), 'r') as f: + time = int(f.read().rstrip('\n')) + + # kernel representation is in centiseconds - convert to seconds + return time/100 + + + @ageing_time.setter + def ageing_time(self, time=None): + """ + Set bridge aging time in seconds. + + Example: + + >>> from vyos.ifconfig import Interface + >>> BridgeIf('br0').ageing_time = 2 + """ + + if not time: + raise ValueError() + + # kernel representation is in centiseconds - convert from seconds to centiseconds + time = int(time) * 100 + + with open('/sys/class/net/{0}/bridge/ageing_time'.format(self._ifname), 'w') as f: + f.write(str(time)) + + @property + def forward_delay(self): + """ + Get bridge forwarding delay in seconds. + + Example: + + >>> from vyos.ifconfig import Interface + >>> BridgeIf('br0').ageing_time + '3""" + + time = 0 + with open('/sys/class/net/{0}/bridge/forward_delay'.format(self._ifname), 'r') as f: + time = int(f.read().rstrip('\n')) + + # kernel representation is in centiseconds - convert to seconds + return time/100 + + + @forward_delay.setter + def forward_delay(self, time=None): + """ + Set bridge forwarding delay in seconds. + + Example: + + >>> from vyos.ifconfig import Interface + >>> BridgeIf('br0').forward_delay = 15 + """ + + if not time: + raise ValueError() + + # kernel representation is in centiseconds - convert from seconds to centiseconds + time = int(time) * 100 + + with open('/sys/class/net/{0}/bridge/forward_delay'.format(self._ifname), 'w') as f: + f.write(str(time)) + + @property + def hello_time(self): + """ + Get bridge hello time in seconds. + + Example: + + >>> from vyos.ifconfig import Interface + >>> BridgeIf('br0').hello_time + '2' + """ + + time = 0 + with open('/sys/class/net/{0}/bridge/hello_time'.format(self._ifname), 'r') as f: + time = int(f.read().rstrip('\n')) + + # kernel representation is in centiseconds - convert to seconds + return time/100 + + + @hello_time.setter + def hello_time(self, time=None): + """ + Set bridge hello time in seconds. + + Example: + + >>> from vyos.ifconfig import Interface + >>> BridgeIf('br0').hello_time = 2 + """ + + if not time: + raise ValueError() + + # kernel representation is in centiseconds - convert from seconds to centiseconds + time = int(time) * 100 + + with open('/sys/class/net/{0}/bridge/hello_time'.format(self._ifname), 'w') as f: + f.write(str(time)) + + @property + def max_age(self): + """ + Get bridge max max message age in seconds. + + Example: + + >>> from vyos.ifconfig import Interface + >>> BridgeIf('br0').max_age + '20' + """ + + time = 0 + with open('/sys/class/net/{0}/bridge/max_age'.format(self._ifname), 'r') as f: + time = int(f.read().rstrip('\n')) + + # kernel representation is in centiseconds - convert to seconds + return time/100 + + + @max_age.setter + def max_age(self, time=None): + """ + Set bridge max message age in seconds. + + Example: + + >>> from vyos.ifconfig import Interface + >>> BridgeIf('br0').max_age = 30 + """ + + if not time: + raise ValueError() + + # kernel representation is in centiseconds - convert from seconds to centiseconds + time = int(time) * 100 + + with open('/sys/class/net/{0}/bridge/max_age'.format(self._ifname), 'w') as f: + f.write(str(time)) + + @property + def priority(self): + """ + Get bridge max aging time in seconds. + + Example: + + >>> from vyos.ifconfig import Interface + >>> BridgeIf('br0').priority + '32768' + """ + + priority = 0 + with open('/sys/class/net/{0}/bridge/priority'.format(self._ifname), 'r') as f: + priority = int(f.read().rstrip('\n')) + + # kernel representation is in centiseconds - convert to seconds + return priority + + + @priority.setter + def priority(self, priority=None): + """ + Set bridge max aging time in seconds. + + Example: + + >>> from vyos.ifconfig import Interface + >>> BridgeIf('br0').priority = 8192 + """ + + if not priority: + raise ValueError() + + with open('/sys/class/net/{0}/bridge/priority'.format(self._ifname), 'w') as f: + f.write(str(priority)) + + + @property + def stp_state(self): + """ + Get bridge STP state + + Example: + + >>> from vyos.ifconfig import Interface + >>> BridgeIf('br0').stp_state + '0' + """ + + state = 0 + with open('/sys/class/net/{0}/bridge/stp_state'.format(self._ifname), 'r') as f: + state = int(f.read().rstrip('\n')) + + return state + + + @stp_state.setter + def stp_state(self, state=None): + """ + Set bridge STP state. + 0 -> STP disabled, 1 -> STP enabled + + Example: + + >>> from vyos.ifconfig import Interface + >>> BridgeIf('br0').stp_state = 1 + """ + + if int(state) >= 0 and int(state) <= 1: + with open('/sys/class/net/{0}/bridge/stp_state'.format(self._ifname), 'w') as f: + f.write(str(state)) + else: + raise ValueError() + + + @property + def multicast_querier(self): + """ + Get bridge multicast querier membership state. + + Example: + + >>> from vyos.ifconfig import Interface + >>> BridgeIf('br0').multicast_querier + '0' + """ + + enable = 0 + with open('/sys/class/net/{0}/bridge/multicast_querier'.format(self._ifname), 'r') as f: + enable = int(f.read().rstrip('\n')) + + return enable + + + @multicast_querier.setter + def multicast_querier(self, enable=None): + """ + Sets whether the bridge actively runs a multicast querier or not. When a + bridge receives a 'multicast host membership' query from another network + host, that host is tracked based on the time that the query was received + plus the multicast query interval time. + + Use enable=1 to enable or enable=0 to disable + + Example: + + >>> from vyos.ifconfig import Interface + >>> BridgeIf('br0').multicast_querier = 1 + """ + + if int(enable) >= 0 and int(enable) <= 1: + with open('/sys/class/net/{0}/bridge/multicast_querier'.format(self._ifname), 'w') as f: + f.write(str(enable)) else: - pid = open(pidfile, 'r').read() - print( - "dhclientv6 running on {0} with pid {1}".format(self._ifname, pid)) - return True + raise ValueError() + + + def add_port(self, interface=None): + """ + Add bridge member port + + Example: + + >>> from vyos.ifconfig import Interface + >>> BridgeIf('br0').add_port('eth0') + >>> BridgeIf('br0').add_port('eth1') + """ + + if not interface: + raise ValueError('No interface address specified') + + cmd = 'ip link set dev "{}" master "{}"'.format(interface, self._ifname) + self._cmd(cmd) + + + def del_port(self, interface=None): + """ + Add bridge member port + + Example: + + >>> from vyos.ifconfig import Interface + >>> BridgeIf('br0').del_port('eth1') + """ + + if not interface: + raise ValueError('No interface address specified') + + cmd = 'ip link set dev "{}" nomaster'.format(interface) + self._cmd(cmd) + + + def set_cost(self, interface=None, cost=None): + """ + Set interface path cost, only relevant for STP enabled interfaces + + Example: + + >>> from vyos.ifconfig import Interface + >>> Interface('eth0').path_cost(4) + """ + + if not interface: + raise ValueError('interface not specified') + + if not cost: + raise ValueError('cost not specified') + + with open('/sys/class/net/{}/brif/{}/path_cost'.format(self._ifname, interface), 'w') as f: + f.write(str(cost)) + + + def set_priority(self, interface=None, priority=None): + """ + Set interface path priority, only relevant for STP enabled interfaces + + Example: + + >>> from vyos.ifconfig import Interface + >>> Interface('eth0').priority(4) + """ + + if not interface: + raise ValueError('interface not specified') + if not priority: + raise ValueError('priority not specified') -# TODO: dhcpv6-pd via dhclient + with open('/sys/class/net/{}/brif/{}/priority'.format(self._ifname, interface), 'w') as f: + f.write(str(priority)) -- cgit v1.2.3 From e461fcf77c8b071a26e5c57021890bb96ccb7504 Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Sat, 31 Aug 2019 13:08:45 +0200 Subject: loopback: T1601: migrate from pyroute2 -> vyos.ifconfig --- python/vyos/ifconfig.py | 5 +++++ src/conf_mode/interface-loopback.py | 17 ++++------------- 2 files changed, 9 insertions(+), 13 deletions(-) (limited to 'python/vyos') diff --git a/python/vyos/ifconfig.py b/python/vyos/ifconfig.py index 0cd27592a..944c1ef82 100644 --- a/python/vyos/ifconfig.py +++ b/python/vyos/ifconfig.py @@ -624,6 +624,11 @@ class Interface: os.remove(self._dhcpv6_lease_file) +class LoopbackIf(Interface): + def __init__(self, ifname=None): + super().__init__(ifname, type='loopback') + + class DummyIf(Interface): def __init__(self, ifname=None): super().__init__(ifname, type='dummy') diff --git a/src/conf_mode/interface-loopback.py b/src/conf_mode/interface-loopback.py index 5c1419b11..1dd68c039 100755 --- a/src/conf_mode/interface-loopback.py +++ b/src/conf_mode/interface-loopback.py @@ -18,7 +18,7 @@ from os import environ from sys import exit from copy import deepcopy -from pyroute2 import IPDB +from vyos.ifconfig import LoopbackIf from vyos.config import Config from vyos import ConfigError @@ -75,28 +75,19 @@ def generate(loopback): return None def apply(loopback): - ipdb = IPDB(mode='explicit') - lo_if = loopback['intf'] - - # the loopback device always exists - lo = ipdb.interfaces[lo_if] - # begin() a transaction prior to make any change - lo.begin() - + lo = LoopbackIf(loopback['intf']) if not loopback['deleted']: # update interface description used e.g. within SNMP # update interface description used e.g. within SNMP lo.ifalias = loopback['description'] # configure interface address(es) for addr in loopback['address']: - lo.add_ip(addr) + lo.add_addr(addr) # remove interface address(es) for addr in loopback['address_remove']: - lo.del_ip(addr) + lo.del_addr(addr) - # commit changes on loopback interface - lo.commit() return None if __name__ == '__main__': -- cgit v1.2.3 From 3a77a3f03068d4f7b2a51ecffc5f2aeb578c58ad Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Sat, 31 Aug 2019 13:20:36 +0200 Subject: Python/ifconfig: T1557: cleanup import section --- python/vyos/ifconfig.py | 2 -- 1 file changed, 2 deletions(-) (limited to 'python/vyos') diff --git a/python/vyos/ifconfig.py b/python/vyos/ifconfig.py index 944c1ef82..347df3318 100644 --- a/python/vyos/ifconfig.py +++ b/python/vyos/ifconfig.py @@ -13,10 +13,8 @@ # You should have received a copy of the GNU Lesser General Public # License along with this library. If not, see . -import sys import os import subprocess -import ipaddress import jinja2 from vyos.validate import * -- cgit v1.2.3 From f524254e0c0c5e5954ca3f2e939f6b510667ad8a Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Sat, 31 Aug 2019 18:49:29 +0200 Subject: Python/ifconfig: T1557: use read/write helpers to interface with sysfs --- python/vyos/ifconfig.py | 34 +++++++++++++++++++++++++--------- 1 file changed, 25 insertions(+), 9 deletions(-) (limited to 'python/vyos') diff --git a/python/vyos/ifconfig.py b/python/vyos/ifconfig.py index 347df3318..7ae1d71bb 100644 --- a/python/vyos/ifconfig.py +++ b/python/vyos/ifconfig.py @@ -118,6 +118,28 @@ class Interface: # add exception handling code pass + def _read_sysfs(self, filename): + """ + Provide a single primitive w/ error checking for reading from sysfs. + """ + var = None + with open(filename, 'r') as f: + var = f.read().rstrip('\n') + + self._debug_msg('read "{}" <- "{}"'.format(value, filename)) + return var + + + def _write_sysfs(self, filename, value): + """ + Provide a single primitive w/ error checking for writing to sysfs. + """ + with open(filename, 'w') as f: + f.write(str(value)) + + self._debug_msg('write "{}" -> "{}"'.format(value, filename)) + return None + @property def mtu(self): @@ -311,11 +333,7 @@ class Interface: >>> Interface('eth0').ifalias '' """ - - alias = '' - with open('/sys/class/net/{0}/ifalias'.format(self._ifname), 'r') as f: - alias = f.read().rstrip('\n') - return alias + return self._read_sysfs('/sys/class/net/{0}/ifalias'.format(self._ifname)) @ifalias.setter @@ -336,13 +354,11 @@ class Interface: >>> Interface('eth0').ifalias '' """ - - # clear interface alias if not ifalias: + # clear interface alias ifalias = '\0' - with open('/sys/class/net/{0}/ifalias'.format(self._ifname), 'w') as f: - f.write(str(ifalias)) + self._write_sysfs('/sys/class/net/{0}/ifalias'.format(self._ifname), ifalias) @property -- cgit v1.2.3 From 0e032f5df4f567195abc7562e8cf6b1164eeec7b Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Sat, 31 Aug 2019 18:59:07 +0200 Subject: Python/ifconfig: T1557: enable debugging with DEBUG=1 environment variable --- python/vyos/ifconfig.py | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) (limited to 'python/vyos') diff --git a/python/vyos/ifconfig.py b/python/vyos/ifconfig.py index 7ae1d71bb..58fed7f62 100644 --- a/python/vyos/ifconfig.py +++ b/python/vyos/ifconfig.py @@ -41,7 +41,7 @@ interface "{{ intf }}" { dhclient_base = r'/var/lib/dhcp/dhclient_' class Interface: - def __init__(self, ifname=None, type=None, debug=False): + def __init__(self, ifname=None, type=None): """ Create instance of an IP interface @@ -57,9 +57,11 @@ class Interface: if not os.path.exists('/sys/class/net/{}'.format(ifname)) and not type: raise Exception('interface "{}" not found'.format(str(ifname))) - # variable already referenced from _debug() - self._debug = debug self._ifname = str(ifname) + self._debug = False + + if os.getenv('DEBUG') == '1': + self._debug = True if not os.path.exists('/sys/class/net/{}'.format(ifname)): cmd = 'ip link add dev "{}" type "{}"'.format(ifname, type) @@ -81,12 +83,6 @@ class Interface: print('"DEBUG/{}: {}'.format(self._ifname, msg)) - def set_debug(self, debug): - if debug not in [True, False]: - raise ValueError('must specify True or False for debug') - self._debug = debug - - def remove(self): """ Remove system interface -- cgit v1.2.3 From 86d1291ec5323e921a0f995211e223bf1f7dcdff Mon Sep 17 00:00:00 2001 From: John Estabrook Date: Sat, 31 Aug 2019 17:25:07 -0500 Subject: [boot-config-loader] T1622: Add failsafe and back trace --- python/vyos/configsession.py | 3 + src/helpers/vyos-boot-config-loader.py | 101 +++++++++++++++++++++++++++++++++ 2 files changed, 104 insertions(+) create mode 100755 src/helpers/vyos-boot-config-loader.py (limited to 'python/vyos') diff --git a/python/vyos/configsession.py b/python/vyos/configsession.py index 8626839f2..acbdd3d5f 100644 --- a/python/vyos/configsession.py +++ b/python/vyos/configsession.py @@ -24,6 +24,7 @@ COMMENT = '/opt/vyatta/sbin/my_comment' COMMIT = '/opt/vyatta/sbin/my_commit' DISCARD = '/opt/vyatta/sbin/my_discard' SHOW_CONFIG = ['/bin/cli-shell-api', 'showConfig'] +LOAD_CONFIG = ['/bin/cli-shell-api', 'loadFile'] # Default "commit via" string APP = "vyos-http-api" @@ -155,3 +156,5 @@ class ConfigSession(object): if format == 'raw': return config_data + def load_config(self, file_path): + self.__run_command(LOAD_CONFIG + [file_path]) diff --git a/src/helpers/vyos-boot-config-loader.py b/src/helpers/vyos-boot-config-loader.py new file mode 100755 index 000000000..06c95765f --- /dev/null +++ b/src/helpers/vyos-boot-config-loader.py @@ -0,0 +1,101 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2019 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# +# + +import os +import sys +import subprocess +import traceback + +from vyos.configsession import ConfigSession, ConfigSessionError +from vyos.configtree import ConfigTree + +STATUS_FILE = '/tmp/vyos-config-status' +TRACE_FILE = '/tmp/boot-config-trace' + +session = ConfigSession(os.getpid(), 'vyos-boot-config-loader') +env = session.get_session_env() + +default_file_name = env['vyatta_sysconfdir'] + '/config.boot.default' + +if len(sys.argv) < 1: + print("Must be called with argument.") + sys.exit(1) +else: + file_name = sys.argv[1] + +def write_config_status(status): + with open(STATUS_FILE, 'w') as f: + f.write('{0}\n'.format(status)) + +def trace_to_file(trace_file_name): + with open(trace_file_name, 'w') as trace_file: + traceback.print_exc(file=trace_file) + +def failsafe(): + try: + with open(default_file_name, 'r') as f: + config_file = f.read() + except Exception as e: + print("Catastrophic: no default config file " + "'{0}'".format(default_file_name)) + sys.exit(1) + + config = ConfigTree(config_file) + if not config.exists(['system', 'login', 'user', 'vyos', + 'authentication', 'encrypted-password']): + print("No password entry in default config file;") + print("unable to recover password for user 'vyos'.") + sys.exit(1) + else: + passwd = config.return_value(['system', 'login', 'user', 'vyos', + 'authentication', + 'encrypted-password']) + + cmd = ("useradd -s /bin/bash -G 'users,sudo' -m -N -p '{0}' " + "vyos".format(passwd)) + try: + subprocess.check_call(cmd, shell=True) + except subprocess.CalledProcessError as e: + sys.exit("{0}".format(e)) + + with open('/etc/motd', 'a+') as f: + f.write('\n\n') + f.write('!!!!!\n') + f.write('There were errors loading the initial configuration;\n') + f.write('please examine the errors in {0} and correct.' + '\n'.format(TRACE_FILE)) + f.write('!!!!!\n\n') + +try: + with open(file_name, 'r') as f: + config_file = f.read() +except Exception as e: + write_config_status(1) + failsafe() + trace_to_file(TRACE_FILE) + sys.exit("{0}".format(e)) + +try: + session.load_config(file_name) + session.commit() + write_config_status(0) +except ConfigSessionError as e: + write_config_status(1) + failsafe() + trace_to_file(TRACE_FILE) + sys.exit(1) -- cgit v1.2.3 From c96ef2f9fc8a005f3b2b9b3bde7fb58d38b9475e Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Sun, 1 Sep 2019 11:08:39 +0200 Subject: Python/ifconfig: T1557: migrate all sysfs calls to {read,write}_sysfs helper Introduced in commit f524254 ("Python/ifconfig: T1557: use read/write helpers to interface with sysfs") migrate all remaining calls to this new helper. This enables us to have a single debug call and a single place for error checking. --- python/vyos/ifconfig.py | 359 +++++++++++++++--------------------------------- 1 file changed, 109 insertions(+), 250 deletions(-) (limited to 'python/vyos') diff --git a/python/vyos/ifconfig.py b/python/vyos/ifconfig.py index 58fed7f62..cdbebed49 100644 --- a/python/vyos/ifconfig.py +++ b/python/vyos/ifconfig.py @@ -41,26 +41,30 @@ interface "{{ intf }}" { dhclient_base = r'/var/lib/dhcp/dhclient_' class Interface: - def __init__(self, ifname=None, type=None): + def __init__(self, ifname, type=None): """ - Create instance of an IP interface + This is the base interface class which supports basic IP/MAC address + operations as well as DHCP(v6). Other interface which represent e.g. + and ethernet bridge are implemented as derived classes adding all + additional functionality. - Example: + DEBUG: + This class has embedded debugging (print) which can be enabled by + creating the following file: + vyos@vyos# touch /tmp/vyos.ifconfig.debug + Example: >>> from vyos.ifconfig import Interface >>> i = Interface('eth0') """ - if not ifname: - raise Exception('interface name required') - if not os.path.exists('/sys/class/net/{}'.format(ifname)) and not type: raise Exception('interface "{}" not found'.format(str(ifname))) self._ifname = str(ifname) self._debug = False - if os.getenv('DEBUG') == '1': + if os.path.isfile('/tmp/vyos.ifconfig.debug'): self._debug = True if not os.path.exists('/sys/class/net/{}'.format(ifname)): @@ -88,7 +92,6 @@ class Interface: Remove system interface Example: - >>> from vyos.ifconfig import Interface >>> i = Interface('eth0') >>> i.remove() @@ -114,6 +117,7 @@ class Interface: # add exception handling code pass + def _read_sysfs(self, filename): """ Provide a single primitive w/ error checking for reading from sysfs. @@ -130,10 +134,10 @@ class Interface: """ Provide a single primitive w/ error checking for writing to sysfs. """ + self._debug_msg('write "{}" -> "{}"'.format(value, filename)) with open(filename, 'w') as f: f.write(str(value)) - self._debug_msg('write "{}" -> "{}"'.format(value, filename)) return None @@ -143,37 +147,30 @@ class Interface: Get/set interface mtu in bytes. Example: - >>> from vyos.ifconfig import Interface >>> Interface('eth0').mtu '1500' """ - - mtu = 0 - with open('/sys/class/net/{0}/mtu'.format(self._ifname), 'r') as f: - mtu = f.read().rstrip('\n') - return mtu + return self._read_sysfs('/sys/class/net/{0}/mtu' + .format(self._ifname)) @mtu.setter - def mtu(self, mtu=None): + def mtu(self, mtu): """ Get/set interface mtu in bytes. Example: - >>> from vyos.ifconfig import Interface >>> Interface('eth0').mtu = 1400 >>> Interface('eth0').mtu '1400' """ - if mtu < 68 or mtu > 9000: raise ValueError('Invalid MTU size: "{}"'.format(mru)) - with open('/sys/class/net/{0}/mtu'.format(self._ifname), 'w') as f: - f.write(str(mtu)) - + return self._write_sysfs('/sys/class/net/{0}/mtu' + .format(self._ifname), mtu) @property def mac(self): @@ -181,24 +178,20 @@ class Interface: Get/set interface mac address Example: - >>> from vyos.ifconfig import Interface >>> Interface('eth0').mac '00:0c:29:11:aa:cc' """ - address = '' - with open('/sys/class/net/{0}/address'.format(self._ifname), 'r') as f: - address = f.read().rstrip('\n') - return address + return self._read_sysfs('/sys/class/net/{0}/address' + .format(self._ifname)) @mac.setter - def mac(self, mac=None): + def mac(self, mac): """ Get/set interface mac address Example: - >>> from vyos.ifconfig import Interface >>> Interface('eth0').mac = '00:90:43:fe:fe:1b' >>> Interface('eth0').mac @@ -230,41 +223,30 @@ class Interface: @property def arp_cache_tmo(self): """ - Get configured ARP cache timeout value from interface. Example shows - default value of 30 seconds. + Get configured ARP cache timeout value from interface in seconds. + Internal Kernel representation is in milliseconds. Example: - >>> from vyos.ifconfig import Interface >>> Interface('eth0').arp_cache_tmo - '30000' + '30' """ - - alias = '' - with open('/proc/sys/net/ipv4/neigh/{0}/base_reachable_time_ms'.format(self._ifname), 'r') as f: - alias = f.read().rstrip('\n') - return alias + return (self._read_sysfs('/proc/sys/net/ipv4/neigh/{0}/base_reachable_time_ms' + .format(self._ifname)) / 1000) @arp_cache_tmo.setter - def arp_cache_tmo(self, tmo=None): + def arp_cache_tmo(self, tmo): """ - Set ARP cache timeout value in seconds for this. + Set ARP cache timeout value in seconds. Internal Kernel representation + is in milliseconds. Example: - >>> from vyos.ifconfig import Interface - >>> Interface('eth0').arp_cache_tmo = '40000' + >>> Interface('eth0').arp_cache_tmo = '40' """ - - # clear interface alias - if not tmo: - raise ValueError('Timeout value required') - - # Kernel interface is on milli seconds - tmo = int(tmo) * 1000 - with open('/proc/sys/net/ipv4/neigh/{0}/base_reachable_time_ms'.format(self._ifname), 'w') as f: - f.write(str(tmo)) + return self._write_sysfs('/proc/sys/net/ipv4/neigh/{0}/base_reachable_time_ms' + .format(self._ifname), (int(tmo) * 1000)) @property def link_detect(self): @@ -272,20 +254,16 @@ class Interface: How does the kernel act when receiving packets on 'down' interfaces Example: - >>> from vyos.ifconfig import Interface >>> Interface('eth0').link_detect '0' """ - - alias = '' - with open('/proc/sys/net/ipv4/conf/{0}/link_filter'.format(self._ifname), 'r') as f: - alias = f.read().rstrip('\n') - return alias + return self._read_sysfs('/proc/sys/net/ipv4/conf/{0}/link_filter' + .format(self._ifname)) @link_detect.setter - def link_detect(self, link_filter=None): + def link_detect(self, link_filter): """ Konfigure kernel response in packets received on interfaces that are 'down' @@ -302,18 +280,11 @@ class Interface: scripts. Example: - >>> from vyos.ifconfig import Interface >>> Interface('eth0').link_detect = '1' """ - - # clear interface alias - if not link_filter: - raise ValueError() - if link_filter >= 0 and link_filter <= 2: - with open('/proc/sys/net/ipv4/conf/{0}/link_filter'.format(self._ifname), 'w') as f: - f.write(str(link_filter)) + return self._write_sysfs('/proc/sys/net/ipv4/conf/{0}/link_filter'.format(self._ifname), link_filter) else: raise ValueError() @@ -338,7 +309,6 @@ class Interface: Get/set interface alias name Example: - >>> from vyos.ifconfig import Interface >>> Interface('eth0').ifalias = 'VyOS upstream interface' >>> Interface('eth0').ifalias @@ -363,31 +333,24 @@ class Interface: Enable (up) / Disable (down) an interface Example: - >>> from vyos.ifconfig import Interface >>> Interface('eth0').state 'up' """ - - state = '' - with open('/sys/class/net/{0}/operstate'.format(self._ifname), 'r') as f: - state = f.read().rstrip('\n') - return state + return self._read_sysfs('/sys/class/net/{0}/operstate'.format(self._ifname)) @state.setter - def state(self, state=None): + def state(self, state): """ Enable (up) / Disable (down) an interface Example: - >>> from vyos.ifconfig import Interface >>> Interface('eth0').state = 'down' >>> Interface('eth0').state 'down' """ - if state not in ['up', 'down']: raise ValueError('state must be "up" or "down"') @@ -403,7 +366,6 @@ class Interface: This is done using the netifaces and ipaddress python modules. Example: - >>> from vyos.ifconfig import Interface >>> Interface('eth0').get_addrs() ['172.16.33.30/24', 'fe80::20c:29ff:fe11:a174/64'] @@ -433,13 +395,12 @@ class Interface: return ipv4 + ipv6 - def add_addr(self, addr=None): + def add_addr(self, addr): """ Add IP address to interface. Address is only added if it yet not added to that interface. Example: - >>> from vyos.ifconfig import Interface >>> j = Interface('eth0') >>> j.add_addr('192.0.2.1/24') @@ -447,21 +408,16 @@ class Interface: >>> j.get_addr() ['192.0.2.1/24', '2001:db8::ffff/64'] """ - - if not addr: - raise ValueError('No IP address specified') - if not is_intf_addr_assigned(self._ifname, addr): cmd = 'sudo ip addr add "{}" dev "{}"'.format(addr, self._ifname) self._cmd(cmd) - def del_addr(self, addr=None): + def del_addr(self, addr): """ Remove IP address from interface. Example: - >>> from vyos.ifconfig import Interface >>> j = Interface('eth0') >>> j.add_addr('2001:db8::ffff/64') @@ -472,10 +428,6 @@ class Interface: >>> j.get_addr() ['2001:db8::ffff/64'] """ - - if not addr: - raise ValueError('No IP address specified') - if is_intf_addr_assigned(self._ifname, addr): cmd = 'ip addr del "{}" dev "{}"'.format(addr, self._ifname) self._cmd(cmd) @@ -493,7 +445,6 @@ class Interface: >>> j = Interface('eth0') >>> j.set_dhcp() """ - dhcp = { 'hostname': 'vyos', 'intf': self._ifname @@ -528,7 +479,6 @@ class Interface: >>> j = Interface('eth0') >>> j.del_dhcp() """ - pid = 0 if os.path.isfile(self._dhcp_pid_file): with open(self._dhcp_pid_file, 'r') as f: @@ -565,7 +515,6 @@ class Interface: >>> j = Interface('eth0') >>> j.set_dhcpv6() """ - dhcpv6 = { 'intf': self._ifname } @@ -604,7 +553,6 @@ class Interface: >>> j = Interface('eth0') >>> j.del_dhcpv6() """ - pid = 0 if os.path.isfile(self._dhcpv6_pid_file): with open(self._dhcpv6_pid_file, 'r') as f: @@ -635,177 +583,130 @@ class Interface: class LoopbackIf(Interface): - def __init__(self, ifname=None): + def __init__(self, ifname): super().__init__(ifname, type='loopback') class DummyIf(Interface): - def __init__(self, ifname=None): + def __init__(self, ifname): super().__init__(ifname, type='dummy') class BridgeIf(Interface): - def __init__(self, ifname=None): + def __init__(self, ifname): super().__init__(ifname, type='bridge') @property def ageing_time(self): """ - Get bridge aging time in seconds. + Return configured bridge interface MAC address aging time in seconds. + Internal kernel representation is in centiseconds, thus its converted + in the end. Kernel default is 300 seconds. Example: - >>> from vyos.ifconfig import Interface >>> BridgeIf('br0').aging_time - '3' + '300' """ - - time = 0 - with open('/sys/class/net/{0}/bridge/ageing_time'.format(self._ifname), 'r') as f: - time = int(f.read().rstrip('\n')) - - # kernel representation is in centiseconds - convert to seconds - return time/100 - + return (self._read_sysfs('/sys/class/net/{0}/bridge/ageing_time' + .format(self._ifname)) / 100) @ageing_time.setter - def ageing_time(self, time=None): + def ageing_time(self, time): """ - Set bridge aging time in seconds. + Set bridge interface MAC address aging time in seconds. Internal kernel + representation is in centiseconds. Kernel default is 300 seconds. Example: - >>> from vyos.ifconfig import Interface >>> BridgeIf('br0').ageing_time = 2 """ - - if not time: - raise ValueError() - - # kernel representation is in centiseconds - convert from seconds to centiseconds time = int(time) * 100 - - with open('/sys/class/net/{0}/bridge/ageing_time'.format(self._ifname), 'w') as f: - f.write(str(time)) + return self._write_sysfs('/sys/class/net/{0}/bridge/ageing_time' + .format(self._ifname), time) @property def forward_delay(self): """ - Get bridge forwarding delay in seconds. + Get bridge forwarding delay in seconds. Internal Kernel representation + is in centiseconds. Example: - >>> from vyos.ifconfig import Interface >>> BridgeIf('br0').ageing_time - '3""" - - time = 0 - with open('/sys/class/net/{0}/bridge/forward_delay'.format(self._ifname), 'r') as f: - time = int(f.read().rstrip('\n')) - - # kernel representation is in centiseconds - convert to seconds - return time/100 - + '3' + """ + return (self._read_sysfs('/sys/class/net/{0}/bridge/forward_delay' + .format(self._ifname)) / 100) @forward_delay.setter - def forward_delay(self, time=None): + def forward_delay(self, time): """ - Set bridge forwarding delay in seconds. + Set bridge forwarding delay in seconds. Internal Kernel representation + is in centiseconds. Example: - >>> from vyos.ifconfig import Interface >>> BridgeIf('br0').forward_delay = 15 """ - - if not time: - raise ValueError() - - # kernel representation is in centiseconds - convert from seconds to centiseconds - time = int(time) * 100 - - with open('/sys/class/net/{0}/bridge/forward_delay'.format(self._ifname), 'w') as f: - f.write(str(time)) + return self._write_sysfs('/sys/class/net/{0}/bridge/forward_delay' + .format(self._ifname), (int(time) * 100)) @property def hello_time(self): """ - Get bridge hello time in seconds. + Get bridge hello time in seconds. Internal Kernel representation + is in centiseconds. Example: - >>> from vyos.ifconfig import Interface >>> BridgeIf('br0').hello_time '2' """ - - time = 0 - with open('/sys/class/net/{0}/bridge/hello_time'.format(self._ifname), 'r') as f: - time = int(f.read().rstrip('\n')) - - # kernel representation is in centiseconds - convert to seconds - return time/100 + return (self._read_sysfs('/sys/class/net/{0}/bridge/hello_time' + .format(self._ifname)) / 100) @hello_time.setter - def hello_time(self, time=None): + def hello_time(self, time): """ - Set bridge hello time in seconds. + Set bridge hello time in seconds. Internal Kernel representation + is in centiseconds. Example: - >>> from vyos.ifconfig import Interface >>> BridgeIf('br0').hello_time = 2 """ - - if not time: - raise ValueError() - - # kernel representation is in centiseconds - convert from seconds to centiseconds - time = int(time) * 100 - - with open('/sys/class/net/{0}/bridge/hello_time'.format(self._ifname), 'w') as f: - f.write(str(time)) + return self._write_sysfs('/sys/class/net/{0}/bridge/hello_time' + .format(self._ifname), (int(time) * 100)) @property def max_age(self): """ - Get bridge max max message age in seconds. + Get bridge max max message age in seconds. Internal Kernel representation + is in centiseconds. Example: - >>> from vyos.ifconfig import Interface >>> BridgeIf('br0').max_age '20' """ - time = 0 - with open('/sys/class/net/{0}/bridge/max_age'.format(self._ifname), 'r') as f: - time = int(f.read().rstrip('\n')) - - # kernel representation is in centiseconds - convert to seconds - return time/100 - + return (self._read_sysfs('/sys/class/net/{0}/bridge/max_age' + .format(self._ifname)) / 100) @max_age.setter - def max_age(self, time=None): + def max_age(self, time): """ - Set bridge max message age in seconds. + Set bridge max message age in seconds. Internal Kernel representation + is in centiseconds. Example: - >>> from vyos.ifconfig import Interface >>> BridgeIf('br0').max_age = 30 """ - - if not time: - raise ValueError() - - # kernel representation is in centiseconds - convert from seconds to centiseconds - time = int(time) * 100 - - with open('/sys/class/net/{0}/bridge/max_age'.format(self._ifname), 'w') as f: - f.write(str(time)) + return self._write_sysfs('/sys/class/net/{0}/bridge/max_age' + .format(self._ifname), (int(time) * 100)) @property def priority(self): @@ -813,45 +714,31 @@ class BridgeIf(Interface): Get bridge max aging time in seconds. Example: - >>> from vyos.ifconfig import Interface >>> BridgeIf('br0').priority '32768' """ - - priority = 0 - with open('/sys/class/net/{0}/bridge/priority'.format(self._ifname), 'r') as f: - priority = int(f.read().rstrip('\n')) - - # kernel representation is in centiseconds - convert to seconds - return priority - + return self._read_sysfs('/sys/class/net/{0}/bridge/priority' + .format(self._ifname)) @priority.setter - def priority(self, priority=None): + def priority(self, priority): """ Set bridge max aging time in seconds. Example: - >>> from vyos.ifconfig import Interface >>> BridgeIf('br0').priority = 8192 """ - - if not priority: - raise ValueError() - - with open('/sys/class/net/{0}/bridge/priority'.format(self._ifname), 'w') as f: - f.write(str(priority)) - + return self._write_sysfs('/sys/class/net/{0}/bridge/priority' + .format(self._ifname), priority) @property def stp_state(self): """ - Get bridge STP state + Get current bridge STP (Spanning Tree) state. Example: - >>> from vyos.ifconfig import Interface >>> BridgeIf('br0').stp_state '0' @@ -865,23 +752,20 @@ class BridgeIf(Interface): @stp_state.setter - def stp_state(self, state=None): + def stp_state(self, state): """ - Set bridge STP state. - 0 -> STP disabled, 1 -> STP enabled + Set bridge STP (Spannign Tree) state. 0 -> STP disabled, 1 -> STP enabled Example: - >>> from vyos.ifconfig import Interface >>> BridgeIf('br0').stp_state = 1 """ if int(state) >= 0 and int(state) <= 1: - with open('/sys/class/net/{0}/bridge/stp_state'.format(self._ifname), 'w') as f: - f.write(str(state)) + return self._write_sysfs('/sys/class/net/{0}/bridge/stp_state' + .format(self._ifname), state) else: - raise ValueError() - + raise ValueError("Value out of range") @property def multicast_querier(self): @@ -889,21 +773,15 @@ class BridgeIf(Interface): Get bridge multicast querier membership state. Example: - >>> from vyos.ifconfig import Interface >>> BridgeIf('br0').multicast_querier '0' """ - - enable = 0 - with open('/sys/class/net/{0}/bridge/multicast_querier'.format(self._ifname), 'r') as f: - enable = int(f.read().rstrip('\n')) - - return enable - + return self._read_sysfs('/sys/class/net/{0}/bridge/multicast_querier' + .format(self._ifname)) @multicast_querier.setter - def multicast_querier(self, enable=None): + def multicast_querier(self, enable): """ Sets whether the bridge actively runs a multicast querier or not. When a bridge receives a 'multicast host membership' query from another network @@ -913,29 +791,25 @@ class BridgeIf(Interface): Use enable=1 to enable or enable=0 to disable Example: - >>> from vyos.ifconfig import Interface >>> BridgeIf('br0').multicast_querier = 1 """ - if int(enable) >= 0 and int(enable) <= 1: - with open('/sys/class/net/{0}/bridge/multicast_querier'.format(self._ifname), 'w') as f: - f.write(str(enable)) + return self._write_sysfs('/sys/class/net/{0}/bridge/multicast_querier' + .format(self._ifname), enable) else: - raise ValueError() + raise ValueError("Value out of range") - def add_port(self, interface=None): + def add_port(self, interface): """ - Add bridge member port + Add physical interface to bridge (member port) Example: - >>> from vyos.ifconfig import Interface >>> BridgeIf('br0').add_port('eth0') >>> BridgeIf('br0').add_port('eth1') """ - if not interface: raise ValueError('No interface address specified') @@ -943,7 +817,7 @@ class BridgeIf(Interface): self._cmd(cmd) - def del_port(self, interface=None): + def del_port(self, interface): """ Add bridge member port @@ -952,7 +826,6 @@ class BridgeIf(Interface): >>> from vyos.ifconfig import Interface >>> BridgeIf('br0').del_port('eth1') """ - if not interface: raise ValueError('No interface address specified') @@ -960,7 +833,7 @@ class BridgeIf(Interface): self._cmd(cmd) - def set_cost(self, interface=None, cost=None): + def set_cost(self, interface, cost): """ Set interface path cost, only relevant for STP enabled interfaces @@ -969,18 +842,11 @@ class BridgeIf(Interface): >>> from vyos.ifconfig import Interface >>> Interface('eth0').path_cost(4) """ + return self._write_sysfs('/sys/class/net/{}/brif/{}/path_cost' + .format(self._ifname, interface), cost) - if not interface: - raise ValueError('interface not specified') - - if not cost: - raise ValueError('cost not specified') - - with open('/sys/class/net/{}/brif/{}/path_cost'.format(self._ifname, interface), 'w') as f: - f.write(str(cost)) - - def set_priority(self, interface=None, priority=None): + def set_priority(self, interface, priority): """ Set interface path priority, only relevant for STP enabled interfaces @@ -989,12 +855,5 @@ class BridgeIf(Interface): >>> from vyos.ifconfig import Interface >>> Interface('eth0').priority(4) """ - - if not interface: - raise ValueError('interface not specified') - - if not priority: - raise ValueError('priority not specified') - - with open('/sys/class/net/{}/brif/{}/priority'.format(self._ifname, interface), 'w') as f: - f.write(str(priority)) + return self._write_sysfs('/sys/class/net/{}/brif/{}/priority' + .format(self._ifname, interface), priority) -- cgit v1.2.3 From e7711bf0693e8463731cdbc955b18311738f42a5 Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Sun, 1 Sep 2019 12:34:51 +0200 Subject: Python/ifconfig: T1557: add proxy_arp{_pvlan} functions --- python/vyos/ifconfig.py | 105 ++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 101 insertions(+), 4 deletions(-) (limited to 'python/vyos') diff --git a/python/vyos/ifconfig.py b/python/vyos/ifconfig.py index cdbebed49..71d608511 100644 --- a/python/vyos/ifconfig.py +++ b/python/vyos/ifconfig.py @@ -284,7 +284,8 @@ class Interface: >>> Interface('eth0').link_detect = '1' """ if link_filter >= 0 and link_filter <= 2: - return self._write_sysfs('/proc/sys/net/ipv4/conf/{0}/link_filter'.format(self._ifname), link_filter) + return self._write_sysfs('/proc/sys/net/ipv4/conf/{0}/link_filter' + .format(self._ifname), link_filter) else: raise ValueError() @@ -300,7 +301,8 @@ class Interface: >>> Interface('eth0').ifalias '' """ - return self._read_sysfs('/sys/class/net/{0}/ifalias'.format(self._ifname)) + return self._read_sysfs('/sys/class/net/{0}/ifalias' + .format(self._ifname)) @ifalias.setter @@ -324,7 +326,8 @@ class Interface: # clear interface alias ifalias = '\0' - self._write_sysfs('/sys/class/net/{0}/ifalias'.format(self._ifname), ifalias) + self._write_sysfs('/sys/class/net/{0}/ifalias' + .format(self._ifname), ifalias) @property @@ -337,7 +340,8 @@ class Interface: >>> Interface('eth0').state 'up' """ - return self._read_sysfs('/sys/class/net/{0}/operstate'.format(self._ifname)) + return self._read_sysfs('/sys/class/net/{0}/operstate' + .format(self._ifname)) @state.setter @@ -359,6 +363,99 @@ class Interface: cmd = 'ip link set dev "{}" "{}"'.format(self._ifname, state) self._cmd(cmd) + @property + def proxy_arp(self): + """ + Get current proxy ARP configuration from sysfs. Default: 0 + + Example: + >>> from vyos.ifconfig import Interface + >>> Interface('eth0').proxy_arp + '0' + """ + return self._read_sysfs('/proc/sys/net/ipv4/conf/{}/proxy_arp' + .format(self._ifname)) + + @proxy_arp.setter + def proxy_arp(self, enable): + """ + Set per interface proxy ARP configuration + + Example: + >>> from vyos.ifconfig import Interface + >>> Interface('eth0').proxy_arp = 1 + >>> Interface('eth0').proxy_arp + '1' + """ + if int(enable) >= 0 and int(enable) <= 1: + return self._write_sysfs('/proc/sys/net/ipv4/conf/{}/proxy_arp' + .format(self._ifname), enable) + else: + raise ValueError("Value out of range") + + + @property + def proxy_arp_pvlan(self): + """ + Private VLAN proxy arp. + Basically allow proxy arp replies back to the same interface + (from which the ARP request/solicitation was received). + + This is done to support (ethernet) switch features, like RFC + 3069, where the individual ports are NOT allowed to + communicate with each other, but they are allowed to talk to + the upstream router. As described in RFC 3069, it is possible + to allow these hosts to communicate through the upstream + router by proxy_arp'ing. Don't need to be used together with + proxy_arp. + + This technology is known by different names: + In RFC 3069 it is called VLAN Aggregation. + Cisco and Allied Telesyn call it Private VLAN. + Hewlett-Packard call it Source-Port filtering or port-isolation. + Ericsson call it MAC-Forced Forwarding (RFC Draft). + + Example: + >>> from vyos.ifconfig import Interface + >>> Interface('eth0').proxy_arp_pvlan + '0' + """ + return self._read_sysfs('/proc/sys/net/ipv4/conf/{}/proxy_arp_pvlan' + .format(self._ifname)) + + @proxy_arp_pvlan.setter + def proxy_arp_pvlan(self, enable): + """ + Private VLAN proxy arp. + Basically allow proxy arp replies back to the same interface + (from which the ARP request/solicitation was received). + + This is done to support (ethernet) switch features, like RFC + 3069, where the individual ports are NOT allowed to + communicate with each other, but they are allowed to talk to + the upstream router. As described in RFC 3069, it is possible + to allow these hosts to communicate through the upstream + router by proxy_arp'ing. Don't need to be used together with + proxy_arp. + + This technology is known by different names: + In RFC 3069 it is called VLAN Aggregation. + Cisco and Allied Telesyn call it Private VLAN. + Hewlett-Packard call it Source-Port filtering or port-isolation. + Ericsson call it MAC-Forced Forwarding (RFC Draft). + + Example: + >>> from vyos.ifconfig import Interface + >>> Interface('eth0').proxy_arp_pvlan = 1 + >>> Interface('eth0').proxy_arp_pvlan + '1' + """ + if int(enable) >= 0 and int(enable) <= 1: + return self._write_sysfs('/proc/sys/net/ipv4/conf/{}/proxy_arp_pvlan' + .format(self._ifname), enable) + else: + raise ValueError("Value out of range") + def get_addr(self): """ -- cgit v1.2.3 From 736b3eca504bf9f57f12166bef7f3bfb347cf522 Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Sun, 1 Sep 2019 19:48:09 +0200 Subject: Python/ifconfig: T1557: bonding: add xmit_hash_policy --- python/vyos/ifconfig.py | 44 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) (limited to 'python/vyos') diff --git a/python/vyos/ifconfig.py b/python/vyos/ifconfig.py index 71d608511..b03550626 100644 --- a/python/vyos/ifconfig.py +++ b/python/vyos/ifconfig.py @@ -954,3 +954,47 @@ class BridgeIf(Interface): """ return self._write_sysfs('/sys/class/net/{}/brif/{}/priority' .format(self._ifname, interface), priority) + + +class BondIf(Interface): + def __init__(self, ifname): + super().__init__(ifname, type='bond') + + @property + def xmit_hash_policy(self): + """ + Selects the transmit hash policy to use for slave selection in + balance-xor, 802.3ad, and tlb modes. Possible values are: layer2, + layer2+3, layer3+4, encap2+3, encap3+4. + + The default value is layer2 + + Example: + >>> from vyos.ifconfig import BondIf + >>> BondIf('bond0').xmit_hash_policy + 'layer3+4' + """ + # Linux Kernel appends has policy value to string, e.g. 'layer3+4 1', + # so remove the later part and only return the mode as string. + return self._read_sysfs('/sys/class/net/{}/bonding/xmit_hash_policy' + .format(self._ifname)).split(' ')[0] + + @xmit_hash_policy.setter + def xmit_hash_policy(self, mode): + """ + Selects the transmit hash policy to use for slave selection in + balance-xor, 802.3ad, and tlb modes. Possible values are: layer2, + layer2+3, layer3+4, encap2+3, encap3+4. + + The default value is layer2 + + Example: + >>> from vyos.ifconfig import Interface + >>> BondIf('bond0').xmit_hash_policy = 'layer2+3' + >>> BondIf('bond0').proxy_arp + '1' + """ + if not mode in ['layer2', 'layer2+3', 'layer3+4', 'encap2+3', 'encap3+4']: + raise ValueError() + return self._write_sysfs('/sys/class/net/{}/bonding/xmit_hash_policy' + .format(self._ifname), mode) -- cgit v1.2.3 From 534595f4da5dd0d1eefdf0b59d89143482fb1add Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Sun, 1 Sep 2019 19:48:36 +0200 Subject: Python/ifconfig: T1557: bonding: add arp_interval --- python/vyos/ifconfig.py | 42 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) (limited to 'python/vyos') diff --git a/python/vyos/ifconfig.py b/python/vyos/ifconfig.py index b03550626..86b812161 100644 --- a/python/vyos/ifconfig.py +++ b/python/vyos/ifconfig.py @@ -998,3 +998,45 @@ class BondIf(Interface): raise ValueError() return self._write_sysfs('/sys/class/net/{}/bonding/xmit_hash_policy' .format(self._ifname), mode) + + @property + def arp_interval(self): + """ + Specifies the ARP link monitoring frequency in milliseconds. + + The ARP monitor works by periodically checking the slave devices to + determine whether they have sent or received traffic recently (the + precise criteria depends upon the bonding mode, and the state of the + slave). Regular traffic is generated via ARP probes issued for the + addresses specified by the arp_ip_target option. + + The default value is 0. + + Example: + >>> from vyos.ifconfig import BondIf + >>> BondIf('bond0').arp_interval + '0' + """ + return self._read_sysfs('/sys/class/net/{}/bonding/arp_interval' + .format(self._ifname)) + + @arp_interval.setter + def arp_interval(self, time): + """ + Specifies the IP addresses to use as ARP monitoring peers when + arp_interval is > 0. These are the targets of the ARP request sent to + determine the health of the link to the targets. Specify these values + in ddd.ddd.ddd.ddd format. Multiple IP addresses must be separated by + a comma. At least one IP address must be given for ARP monitoring to + function. The maximum number of targets that can be specified is 16. + + The default value is no IP addresses. + + Example: + >>> from vyos.ifconfig import Interface + >>> BondIf('bond0').arp_interval = '100' + >>> BondIf('bond0').arp_interval + '100' + """ + return self._write_sysfs('/sys/class/net/{}/bonding/arp_interval' + .format(self._ifname), time) -- cgit v1.2.3 From 308886a3fc28aa7772c14452185fb06531ba9dfd Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Sun, 1 Sep 2019 19:48:47 +0200 Subject: Python/ifconfig: T1557: bonding: add arp_ip_target --- python/vyos/ifconfig.py | 42 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) (limited to 'python/vyos') diff --git a/python/vyos/ifconfig.py b/python/vyos/ifconfig.py index 86b812161..506004fa0 100644 --- a/python/vyos/ifconfig.py +++ b/python/vyos/ifconfig.py @@ -1040,3 +1040,45 @@ class BondIf(Interface): """ return self._write_sysfs('/sys/class/net/{}/bonding/arp_interval' .format(self._ifname), time) + + @property + def arp_ip_target(self): + """ + Specifies the IP addresses to use as ARP monitoring peers when + arp_interval is > 0. These are the targets of the ARP request sent to + determine the health of the link to the targets. Specify these values + in ddd.ddd.ddd.ddd format. Multiple IP addresses must be separated by + a comma. At least one IP address must be given for ARP monitoring to + function. The maximum number of targets that can be specified is 16. + + The default value is no IP addresses. + + Example: + >>> from vyos.ifconfig import BondIf + >>> BondIf('bond0').arp_ip_target + '192.0.2.1' + """ + return self._read_sysfs('/sys/class/net/{}/bonding/arp_ip_target' + .format(self._ifname)) + + @arp_ip_target.setter + def arp_ip_target(self, target): + """ + Specifies the IP addresses to use as ARP monitoring peers when + arp_interval is > 0. These are the targets of the ARP request sent to + determine the health of the link to the targets. Specify these values + in ddd.ddd.ddd.ddd format. Multiple IP addresses must be separated by + a comma. At least one IP address must be given for ARP monitoring to + function. The maximum number of targets that can be specified is 16. + + The default value is no IP addresses. + + Example: + >>> from vyos.ifconfig import Interface + >>> BondIf('bond0').arp_ip_target = '192.0.2.1' + >>> BondIf('bond0').arp_ip_target + '192.0.2.1' + """ + return self._write_sysfs('/sys/class/net/{}/bonding/arp_ip_target' + .format(self._ifname), mode) + -- cgit v1.2.3 From 3b5ce92db3a77cc29051a26ef12cad1ed52f7e69 Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Tue, 3 Sep 2019 12:02:06 +0200 Subject: Python/ifconfig: T1557: bonding: bugfix setting ARP IP target --- python/vyos/ifconfig.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'python/vyos') diff --git a/python/vyos/ifconfig.py b/python/vyos/ifconfig.py index 506004fa0..c69beb31a 100644 --- a/python/vyos/ifconfig.py +++ b/python/vyos/ifconfig.py @@ -1080,5 +1080,5 @@ class BondIf(Interface): '192.0.2.1' """ return self._write_sysfs('/sys/class/net/{}/bonding/arp_ip_target' - .format(self._ifname), mode) + .format(self._ifname), target) -- cgit v1.2.3 From 50ea3e0576dd18d98d7696fcbf0714b6fe86672d Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Tue, 3 Sep 2019 12:23:36 +0200 Subject: Python/ifconfig: T1557: bonding: bugfix read_sysfs when debug is enabled --- python/vyos/ifconfig.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) (limited to 'python/vyos') diff --git a/python/vyos/ifconfig.py b/python/vyos/ifconfig.py index c69beb31a..217f898fa 100644 --- a/python/vyos/ifconfig.py +++ b/python/vyos/ifconfig.py @@ -122,12 +122,12 @@ class Interface: """ Provide a single primitive w/ error checking for reading from sysfs. """ - var = None + value = None with open(filename, 'r') as f: - var = f.read().rstrip('\n') + value = f.read().rstrip('\n') self._debug_msg('read "{}" <- "{}"'.format(value, filename)) - return var + return value def _write_sysfs(self, filename, value): -- cgit v1.2.3 From 8b2e1f9a9c493b2d04513f83e154c092df66f761 Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Tue, 3 Sep 2019 12:33:32 +0200 Subject: Python/ifconfig: T1557: adjust debug message format * remove missleading " as first character with no closing quote * use single quotes in output messages (unclutter) * when writing changes, make output string copy/pasteable my changing '->' to '>' --- python/vyos/ifconfig.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) (limited to 'python/vyos') diff --git a/python/vyos/ifconfig.py b/python/vyos/ifconfig.py index 217f898fa..786753b8c 100644 --- a/python/vyos/ifconfig.py +++ b/python/vyos/ifconfig.py @@ -84,7 +84,7 @@ class Interface: def _debug_msg(self, msg): if self._debug: - print('"DEBUG/{}: {}'.format(self._ifname, msg)) + print('DEBUG/{}: {}'.format(self._ifname, msg)) def remove(self): @@ -126,7 +126,7 @@ class Interface: with open(filename, 'r') as f: value = f.read().rstrip('\n') - self._debug_msg('read "{}" <- "{}"'.format(value, filename)) + self._debug_msg("read '{}' < '{}'".format(value, filename)) return value @@ -134,7 +134,7 @@ class Interface: """ Provide a single primitive w/ error checking for writing to sysfs. """ - self._debug_msg('write "{}" -> "{}"'.format(value, filename)) + self._debug_msg("write '{}' > '{}'".format(value, filename)) with open(filename, 'w') as f: f.write(str(value)) -- cgit v1.2.3 From 144379775ae8efde4f3290fa08c528977824b285 Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Tue, 3 Sep 2019 12:49:33 +0200 Subject: Python/ifconfig: T1556: bridge: no need to manually generate an exception --- python/vyos/ifconfig.py | 6 ------ 1 file changed, 6 deletions(-) (limited to 'python/vyos') diff --git a/python/vyos/ifconfig.py b/python/vyos/ifconfig.py index 786753b8c..b25d84f26 100644 --- a/python/vyos/ifconfig.py +++ b/python/vyos/ifconfig.py @@ -907,9 +907,6 @@ class BridgeIf(Interface): >>> BridgeIf('br0').add_port('eth0') >>> BridgeIf('br0').add_port('eth1') """ - if not interface: - raise ValueError('No interface address specified') - cmd = 'ip link set dev "{}" master "{}"'.format(interface, self._ifname) self._cmd(cmd) @@ -923,9 +920,6 @@ class BridgeIf(Interface): >>> from vyos.ifconfig import Interface >>> BridgeIf('br0').del_port('eth1') """ - if not interface: - raise ValueError('No interface address specified') - cmd = 'ip link set dev "{}" nomaster'.format(interface) self._cmd(cmd) -- cgit v1.2.3 From cb9aa5447e79df7d83704a5e039937b72c77177d Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Tue, 3 Sep 2019 12:51:27 +0200 Subject: Python/ifconfig: T1557: add message to raised Exceptions --- python/vyos/ifconfig.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'python/vyos') diff --git a/python/vyos/ifconfig.py b/python/vyos/ifconfig.py index b25d84f26..10653c645 100644 --- a/python/vyos/ifconfig.py +++ b/python/vyos/ifconfig.py @@ -287,7 +287,7 @@ class Interface: return self._write_sysfs('/proc/sys/net/ipv4/conf/{0}/link_filter' .format(self._ifname), link_filter) else: - raise ValueError() + raise ValueError("Value out of range") @property @@ -989,7 +989,7 @@ class BondIf(Interface): '1' """ if not mode in ['layer2', 'layer2+3', 'layer3+4', 'encap2+3', 'encap3+4']: - raise ValueError() + raise ValueError("Value out of range") return self._write_sysfs('/sys/class/net/{}/bonding/xmit_hash_policy' .format(self._ifname), mode) -- cgit v1.2.3 From c25a04907f3088aa29c7ffdb7c338fa169d891f4 Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Tue, 3 Sep 2019 12:54:18 +0200 Subject: Python/ifconfig: T1557: bonding: add {add,del}_port for slave interface --- python/vyos/ifconfig.py | 26 ++++++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) (limited to 'python/vyos') diff --git a/python/vyos/ifconfig.py b/python/vyos/ifconfig.py index 10653c645..74b90d0c4 100644 --- a/python/vyos/ifconfig.py +++ b/python/vyos/ifconfig.py @@ -913,10 +913,9 @@ class BridgeIf(Interface): def del_port(self, interface): """ - Add bridge member port + Remove member port from bridge instance. Example: - >>> from vyos.ifconfig import Interface >>> BridgeIf('br0').del_port('eth1') """ @@ -1076,3 +1075,26 @@ class BondIf(Interface): return self._write_sysfs('/sys/class/net/{}/bonding/arp_ip_target' .format(self._ifname), target) + def add_port(self, interface): + """ + Enslave physical interface to bond + + Example: + >>> from vyos.ifconfig import Interface + >>> BondIf('bond0').add_port('eth0') + >>> BondIf('bond0').add_port('eth1') + """ + return self._write_sysfs('/sys/class/net/{}/bonding/slaves' + .format(self._ifname), '+' + target) + + def del_port(self, interface): + """ + Remove physical port from bond + + Example: + >>> from vyos.ifconfig import Interface + >>> BondIf('bond0').del_port('eth1') + """ + return self._write_sysfs('/sys/class/net/{}/bonding/slaves' + .format(self._ifname), '-' + target) + -- cgit v1.2.3 From 98e14d27dee4e8e0619abd479583aac472a5fc08 Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Tue, 3 Sep 2019 13:54:55 +0200 Subject: Python/ifconfig: T1557: adjust debug message format #2 --- python/vyos/ifconfig.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) (limited to 'python/vyos') diff --git a/python/vyos/ifconfig.py b/python/vyos/ifconfig.py index 74b90d0c4..5247ac3d7 100644 --- a/python/vyos/ifconfig.py +++ b/python/vyos/ifconfig.py @@ -84,7 +84,7 @@ class Interface: def _debug_msg(self, msg): if self._debug: - print('DEBUG/{}: {}'.format(self._ifname, msg)) + print('DEBUG/{:<6} {}'.format(self._ifname, msg)) def remove(self): @@ -109,7 +109,7 @@ class Interface: def _cmd(self, command): - self._debug_msg(command) + self._debug_msg("{:<6} '{}'".format('cmd', command)) process = subprocess.Popen(command,stdout=subprocess.PIPE, shell=True) proc_stdout = process.communicate()[0].strip() @@ -126,7 +126,7 @@ class Interface: with open(filename, 'r') as f: value = f.read().rstrip('\n') - self._debug_msg("read '{}' < '{}'".format(value, filename)) + self._debug_msg("{:<6} '{}' < '{}'".format('read', value, filename)) return value @@ -134,7 +134,7 @@ class Interface: """ Provide a single primitive w/ error checking for writing to sysfs. """ - self._debug_msg("write '{}' > '{}'".format(value, filename)) + self._debug_msg("{:<6} '{}' > '{}'".format('write', value, filename)) with open(filename, 'w') as f: f.write(str(value)) @@ -506,7 +506,7 @@ class Interface: ['192.0.2.1/24', '2001:db8::ffff/64'] """ if not is_intf_addr_assigned(self._ifname, addr): - cmd = 'sudo ip addr add "{}" dev "{}"'.format(addr, self._ifname) + cmd = 'ip addr add "{}" dev "{}"'.format(addr, self._ifname) self._cmd(cmd) -- cgit v1.2.3 From 90e58236898918a3bc27ce451e245647e712ad40 Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Tue, 3 Sep 2019 13:56:49 +0200 Subject: Python/ifconfig: T1557: bonding: disable interface prior enslaving them An interface can only be added to a bond if it is in 'down' state. If interface is in 'up' state, the following Kernel error will be thrown: > bond0: eth1 is up - this may be due to an out of date ifenslave. --- python/vyos/ifconfig.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) (limited to 'python/vyos') diff --git a/python/vyos/ifconfig.py b/python/vyos/ifconfig.py index 5247ac3d7..0dba21ab1 100644 --- a/python/vyos/ifconfig.py +++ b/python/vyos/ifconfig.py @@ -1077,15 +1077,20 @@ class BondIf(Interface): def add_port(self, interface): """ - Enslave physical interface to bond + Enslave physical interface to bond. Example: >>> from vyos.ifconfig import Interface >>> BondIf('bond0').add_port('eth0') >>> BondIf('bond0').add_port('eth1') """ + # An interface can only be added to a bond if it is in 'down' state. If + # interface is in 'up' state, the following Kernel error will be thrown: + # bond0: eth1 is up - this may be due to an out of date ifenslave. + Interface(interface).state = 'down' + return self._write_sysfs('/sys/class/net/{}/bonding/slaves' - .format(self._ifname), '+' + target) + .format(self._ifname), '+' + interface) def del_port(self, interface): """ @@ -1096,5 +1101,5 @@ class BondIf(Interface): >>> BondIf('bond0').del_port('eth1') """ return self._write_sysfs('/sys/class/net/{}/bonding/slaves' - .format(self._ifname), '-' + target) + .format(self._ifname), '-' + interface) -- cgit v1.2.3 From 74fcfa7fb48601febd0d9d0e4d53db20b28e4898 Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Tue, 3 Sep 2019 13:57:57 +0200 Subject: Python/ifconfig: T1557: bonding: add get_slaves() call --- python/vyos/ifconfig.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) (limited to 'python/vyos') diff --git a/python/vyos/ifconfig.py b/python/vyos/ifconfig.py index 0dba21ab1..e58c7e0bb 100644 --- a/python/vyos/ifconfig.py +++ b/python/vyos/ifconfig.py @@ -1103,3 +1103,15 @@ class BondIf(Interface): return self._write_sysfs('/sys/class/net/{}/bonding/slaves' .format(self._ifname), '-' + interface) + def get_slaves(self): + """ + Return a list with all configured slave interfaces on this bond. + + Example: + >>> from vyos.ifconfig import Interface + >>> BondIf('bond0').get_slaves() + ['eth1', 'eth2'] + """ + slaves = self._read_sysfs('/sys/class/net/{}/bonding/slaves' + .format(self._ifname)) + return list(map(str, slaves.split())) -- cgit v1.2.3 From 8a524a1ae182c7ad9b031c7b0d79273a3df13390 Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Tue, 3 Sep 2019 14:48:03 +0200 Subject: Python/ifconfig: T1557: bonding: add primary and mode property --- python/vyos/ifconfig.py | 89 ++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 88 insertions(+), 1 deletion(-) (limited to 'python/vyos') diff --git a/python/vyos/ifconfig.py b/python/vyos/ifconfig.py index e58c7e0bb..71587d045 100644 --- a/python/vyos/ifconfig.py +++ b/python/vyos/ifconfig.py @@ -970,7 +970,7 @@ class BondIf(Interface): # Linux Kernel appends has policy value to string, e.g. 'layer3+4 1', # so remove the later part and only return the mode as string. return self._read_sysfs('/sys/class/net/{}/bonding/xmit_hash_policy' - .format(self._ifname)).split(' ')[0] + .format(self._ifname)).split()[0] @xmit_hash_policy.setter def xmit_hash_policy(self, mode): @@ -1115,3 +1115,90 @@ class BondIf(Interface): slaves = self._read_sysfs('/sys/class/net/{}/bonding/slaves' .format(self._ifname)) return list(map(str, slaves.split())) + + @property + def primary(self): + """ + A string (eth0, eth2, etc) specifying which slave is the primary + device. The specified device will always be the active slave while it + is available. Only when the primary is off-line will alternate devices + be used. This is useful when one slave is preferred over another, e.g., + when one slave has higher throughput than another. + + The primary option is only valid for active-backup, balance-tlb and + balance-alb mode. + + Example: + >>> from vyos.ifconfig import BondIf + >>> BondIf('bond0').primary + 'eth1' + """ + return self._read_sysfs('/sys/class/net/{}/bonding/primary' + .format(self._ifname)) + + @primary.setter + def primary(self, interface): + """ + A string (eth0, eth2, etc) specifying which slave is the primary + device. The specified device will always be the active slave while it + is available. Only when the primary is off-line will alternate devices + be used. This is useful when one slave is preferred over another, e.g., + when one slave has higher throughput than another. + + The primary option is only valid for active-backup, balance-tlb and + balance-alb mode. + + Example: + >>> from vyos.ifconfig import Interface + >>> BondIf('bond0').primary = 'eth2' + >>> BondIf('bond0').primary + 'eth2' + """ + if not interface: + # reset primary interface + interface = '\0' + + return self._write_sysfs('/sys/class/net/{}/bonding/primary' + .format(self._ifname), interface) + + @property + def mode(self): + """ + Specifies one of the bonding policies. The default is balance-rr + (round robin). + + Possible values are: balance-rr (0), active-backup (1), balance-xor (2), + broadcast (3), 802.3ad (4), balance-tlb (5), balance-alb (6) + + Example: + >>> from vyos.ifconfig import BondIf + >>> BondIf('bond0').mode + 'balance-rr' + """ + return self._read_sysfs('/sys/class/net/{}/bonding/mode' + .format(self._ifname)).split()[0] + + @mode.setter + def mode(self, mode): + """ + Specifies one of the bonding policies. The default is balance-rr + (round robin). + + Possible values are: balance-rr, active-backup, balance-xor, + broadcast, 802.3ad, balance-tlb, balance-alb + + NOTE: the bonding mode can not be changed when the bond itself has + slaves + + Example: + >>> from vyos.ifconfig import Interface + >>> BondIf('bond0').mode = '802.3ad' + >>> BondIf('bond0').mode + '802.3ad' + """ + if not mode in ['balance-rr', 'active-backup', 'balance-xor', 'broadcast', + '802.3ad', 'balance-tlb', 'balance-alb']: + raise ValueError("Value out of range") + + return self._write_sysfs('/sys/class/net/{}/bonding/mode' + .format(self._ifname), mode) -- cgit v1.2.3 From 21bcf16ce23d3df7c3267508012e663e8b443ccd Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Tue, 3 Sep 2019 15:13:34 +0200 Subject: Python/ifconfig: T1557: add description for Interface classes --- python/vyos/ifconfig.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) (limited to 'python/vyos') diff --git a/python/vyos/ifconfig.py b/python/vyos/ifconfig.py index 71587d045..341a9770c 100644 --- a/python/vyos/ifconfig.py +++ b/python/vyos/ifconfig.py @@ -680,16 +680,33 @@ class Interface: class LoopbackIf(Interface): + """ + The loopback device is a special, virtual network interface that your router + uses to communicate with itself. + """ def __init__(self, ifname): super().__init__(ifname, type='loopback') class DummyIf(Interface): + """ + A dummy interface is entirely virtual like, for example, the loopback + interface. The purpose of a dummy interface is to provide a device to route + packets through without actually transmitting them. + """ def __init__(self, ifname): super().__init__(ifname, type='dummy') class BridgeIf(Interface): + """ + A bridge is a way to connect two Ethernet segments together in a protocol + independent way. Packets are forwarded based on Ethernet address, rather + than IP address (like a router). Since forwarding is done at Layer 2, all + protocols can go transparently through a bridge. + + The Linux bridge code implements a subset of the ANSI/IEEE 802.1d standard. + """ def __init__(self, ifname): super().__init__(ifname, type='bridge') @@ -950,6 +967,13 @@ class BridgeIf(Interface): class BondIf(Interface): + """ + The Linux bonding driver provides a method for aggregating multiple network + interfaces into a single logical "bonded" interface. The behavior of the + bonded interfaces depends upon the mode; generally speaking, modes provide + either hot standby or load balancing services. Additionally, link integrity + monitoring may be performed. + """ def __init__(self, ifname): super().__init__(ifname, type='bond') -- cgit v1.2.3 From 01938cec2c2e19c8f556c2e5b425fc79d038126f Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Tue, 3 Sep 2019 15:14:05 +0200 Subject: Python/ifconfig: T1557: derive BondIf from EthernetIf as we need VLANs --- python/vyos/ifconfig.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) (limited to 'python/vyos') diff --git a/python/vyos/ifconfig.py b/python/vyos/ifconfig.py index 341a9770c..8cf5e1645 100644 --- a/python/vyos/ifconfig.py +++ b/python/vyos/ifconfig.py @@ -966,7 +966,11 @@ class BridgeIf(Interface): .format(self._ifname, interface), priority) -class BondIf(Interface): +class EthernetIf(Interface): + def __init__(self, ifname, type=None): + super().__init__(ifname, type) + +class BondIf(EthernetIf): """ The Linux bonding driver provides a method for aggregating multiple network interfaces into a single logical "bonded" interface. The behavior of the -- cgit v1.2.3 From 35dcd0a1cb5d9a63dbebcb299eb9425b57258537 Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Tue, 3 Sep 2019 18:59:50 +0200 Subject: Python/ifconfig: T1557: cleanup __init__/debug --- python/vyos/ifconfig.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) (limited to 'python/vyos') diff --git a/python/vyos/ifconfig.py b/python/vyos/ifconfig.py index 8cf5e1645..1d03e4e74 100644 --- a/python/vyos/ifconfig.py +++ b/python/vyos/ifconfig.py @@ -57,18 +57,16 @@ class Interface: >>> from vyos.ifconfig import Interface >>> i = Interface('eth0') """ + self._ifname = str(ifname) if not os.path.exists('/sys/class/net/{}'.format(ifname)) and not type: - raise Exception('interface "{}" not found'.format(str(ifname))) - - self._ifname = str(ifname) - self._debug = False + raise Exception('interface "{}" not found'.format(self._ifname)) if os.path.isfile('/tmp/vyos.ifconfig.debug'): self._debug = True - if not os.path.exists('/sys/class/net/{}'.format(ifname)): - cmd = 'ip link add dev "{}" type "{}"'.format(ifname, type) + if not os.path.exists('/sys/class/net/{}'.format(self._ifname)): + cmd = 'ip link add dev "{}" type "{}"'.format(self._ifname, type) self._cmd(cmd) # per interface DHCP config files @@ -83,7 +81,7 @@ class Interface: def _debug_msg(self, msg): - if self._debug: + if os.path.isfile('/tmp/vyos.ifconfig.debug'): print('DEBUG/{:<6} {}'.format(self._ifname, msg)) -- cgit v1.2.3 From 1fc4c201d7771ac4164e9480d55a654b7674d098 Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Tue, 3 Sep 2019 18:58:27 +0200 Subject: bonding: T1614: T1557: add vif/vif-s VLAN interface support Support for vif-c interfaces is still missing --- python/vyos/ifconfig.py | 47 ++++++++++++++++- src/conf_mode/interface-bonding.py | 101 +++++++++++++++++++++++++++++++++---- 2 files changed, 136 insertions(+), 12 deletions(-) (limited to 'python/vyos') diff --git a/python/vyos/ifconfig.py b/python/vyos/ifconfig.py index 1d03e4e74..bb2d23d0d 100644 --- a/python/vyos/ifconfig.py +++ b/python/vyos/ifconfig.py @@ -87,7 +87,9 @@ class Interface: def remove(self): """ - Remove system interface + Remove interface from operating system. Removing the interface + deconfigures all assigned IP addresses and clear possible DHCP(v6) + client processes. Example: >>> from vyos.ifconfig import Interface @@ -964,10 +966,53 @@ class BridgeIf(Interface): .format(self._ifname, interface), priority) + class EthernetIf(Interface): def __init__(self, ifname, type=None): super().__init__(ifname, type) + def add_vlan(self, vlan_id, ethertype=''): + """ + A virtual LAN (VLAN) is any broadcast domain that is partitioned and + isolated in a computer network at the data link layer (OSI layer 2). + Use this function to create a new VLAN interface on a given physical + interface. + + This function creates both 802.1q and 802.1ad (Q-in-Q) interfaces. Proto + parameter is used to indicate VLAN type. + + A new object of type EthernetIf is returned once the interface has been + created. + """ + vlan_ifname = self._ifname + '.' + str(vlan_id) + if not os.path.exists('/sys/class/net/{}'.format(vlan_ifname)): + self._vlan_id = int(vlan_id) + + if ethertype: + self._ethertype = ethertype + ethertype='proto {}'.format(ethertype) + + # create interface in the system + cmd = 'ip link add link {intf} name {intf}.{vlan} type vlan {proto} id {vlan}'.format(intf=self._ifname, vlan=self._vlan_id, proto=ethertype) + self._cmd(cmd) + + # return new object mapping to the newly created interface + # we can now work on this object for e.g. IP address setting + # or interface description and so on + return EthernetIf(vlan_ifname) + + + def del_vlan(self, vlan_id): + """ + Remove VLAN interface from operating system. Removing the interface + deconfigures all assigned IP addresses and clear possible DHCP(v6) + client processes. + """ + vlan_ifname = self._ifname + '.' + str(vlan_id) + tmp = EthernetIf(vlan_ifname) + tmp.remove() + + class BondIf(EthernetIf): """ The Linux bonding driver provides a method for aggregating multiple network diff --git a/src/conf_mode/interface-bonding.py b/src/conf_mode/interface-bonding.py index a623ca524..9e7be19f5 100755 --- a/src/conf_mode/interface-bonding.py +++ b/src/conf_mode/interface-bonding.py @@ -21,7 +21,8 @@ import os from copy import deepcopy from sys import exit from netifaces import interfaces -from vyos.ifconfig import BondIf + +from vyos.ifconfig import BondIf, EthernetIf from vyos.config import Config from vyos import ConfigError @@ -49,7 +50,9 @@ default_config_data = { 'mtu': 1500, 'primary': '', 'vif_s': [], - 'vif': [] + 'vif_s_remove': [], + 'vif': [], + 'vif_remove': [] } def diff(first, second): @@ -74,6 +77,15 @@ def get_bond_mode(mode): else: raise ConfigError('invalid bond mode "{}"'.format(mode)) +def get_ethertype(ethertype_val): + if ethertype_val == '0x88A8': + return '802.1ad' + elif ethertype_val == '0x8100': + return '802.1q' + else: + raise ConfigError('invalid ethertype "{}"'.format(ethertype_val)) + + def vlan_to_dict(conf): """ Common used function which will extract VLAN related information from config @@ -82,7 +94,7 @@ def vlan_to_dict(conf): Function call's itself recursively if a vif-s/vif-c pair is detected. """ vlan = { - 'id': conf.get_level().split(' ')[-1], # get the '100' in 'interfaces bonding bond0 vif-s 100' + 'id': conf.get_level().split()[-1], # get the '100' in 'interfaces bonding bond0 vif-s 100' 'address': [], 'address_remove': [], 'description': '', @@ -133,9 +145,14 @@ def vlan_to_dict(conf): if conf.exists('disable'): vlan['disable'] = True - # ethertype (only on vif-s nodes) - if conf.exists('ethertype'): - vlan['ethertype'] = conf.return_value('ethertype') + # ethertype is mandatory on vif-s nodes and only exists here! + # check if this is a vif-s node at all: + if conf.get_level().split()[-2] == 'vif-s': + # ethertype uses a default of 0x88A8 + tmp = '0x88A8' + if conf.exists('ethertype'): + tmp = conf.return_value('ethertype') + vlan['ethertype'] = get_ethertype(tmp) # Media Access Control (MAC) address if conf.exists('mac'): @@ -158,6 +175,39 @@ def vlan_to_dict(conf): return vlan + +def apply_vlan_config(vlan, config): + """ + Generic function to apply a VLAN configuration from a dictionary + to a VLAN interface + """ + + if type(vlan) != type(EthernetIf("lo")): + raise TypeError() + + # Configure interface address(es) + for addr in config['address_remove']: + vlan.del_addr(addr) + for addr in config['address']: + vlan.add_addr(addr) + + # update interface description used e.g. within SNMP + vlan.ifalias = config['description'] + # ignore link state changes + vlan.link_detect = config['disable_link_detect'] + # Maximum Transmission Unit (MTU) + vlan.mtu = config['mtu'] + # Change VLAN interface MAC address + if config['mac']: + vlan.mac = config['mac'] + + # enable/disable VLAN interface + if config['disable']: + vlan.state = 'down' + else: + vlan.state = 'up' + + def get_config(): # initialize kernel module if not loaded if not os.path.isfile('/sys/class/net/bonding_masters'): @@ -271,6 +321,12 @@ def get_config(): # re-set configuration level and retrieve vif-s interfaces conf.set_level(cfg_base) + # Determine vif-s interfaces (currently effective) - to determine which + # vif-s interface is no longer present and needs to be removed + eff_intf = conf.list_effective_nodes('vif-s') + act_intf = conf.list_nodes('vif-s') + bond['vif_s_remove'] = diff(eff_intf, act_intf) + if conf.exists('vif-s'): for vif_s in conf.list_nodes('vif-s'): # set config level to vif-s interface @@ -279,6 +335,12 @@ def get_config(): # re-set configuration level and retrieve vif-s interfaces conf.set_level(cfg_base) + # Determine vif interfaces (currently effective) - to determine which + # vif interface is no longer present and needs to be removed + eff_intf = conf.list_effective_nodes('vif') + act_intf = conf.list_nodes('vif') + bond['vif_remove'] = diff(eff_intf, act_intf) + if conf.exists('vif'): for vif in conf.list_nodes('vif'): # set config level to vif interface @@ -287,6 +349,7 @@ def get_config(): return bond + def verify(bond): if len (bond['arp_mon_tgt']) > 16: raise ConfigError('The maximum number of targets that can be specified is 16') @@ -300,9 +363,11 @@ def verify(bond): return None + def generate(bond): return None + def apply(bond): b = BondIf(bond['intf']) @@ -378,13 +443,27 @@ def apply(bond): # Primary device interface b.primary = bond['primary'] - # - # VLAN config goes here - # - # Add (enslave) interfaces to bond for intf in bond['member']: - b.add_port(intf) + b.add_port(intf) + + # remove no longer required service VLAN interfaces (vif-s) + for vif_s in bond['vif_s_remove']: + b.del_vlan(vif_s) + + # create service VLAN interfaces (vif-s) + for vif_s in bond['vif_s']: + vlan = b.add_vlan(vif_s['id'], ethertype=vif_s['ethertype']) + apply_vlan_config(vlan, vif_s) + + # remove no longer required VLAN interfaces (vif) + for vif in bond['vif_remove']: + b.del_vlan(vif) + + # create VLAN interfaces (vif) + for vif in bond['vif']: + vlan = b.add_vlan(vif['id']) + apply_vlan_config(vlan, vif) # As the bond interface is always disabled first when changing # parameters we will only re-enable the interface if it is not -- cgit v1.2.3 From ca7e1b276878f503c63c1743c0f8aa3a5245ce31 Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Tue, 3 Sep 2019 20:43:24 +0200 Subject: Python/ifconfig: T1557: remove double quotes on iproute2 commands --- python/vyos/ifconfig.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) (limited to 'python/vyos') diff --git a/python/vyos/ifconfig.py b/python/vyos/ifconfig.py index bb2d23d0d..449923f09 100644 --- a/python/vyos/ifconfig.py +++ b/python/vyos/ifconfig.py @@ -66,7 +66,7 @@ class Interface: self._debug = True if not os.path.exists('/sys/class/net/{}'.format(self._ifname)): - cmd = 'ip link add dev "{}" type "{}"'.format(self._ifname, type) + cmd = 'ip link add dev {} type {}'.format(self._ifname, type) self._cmd(cmd) # per interface DHCP config files @@ -104,12 +104,12 @@ class Interface: # NOTE (Improvement): # after interface removal no other commands should be allowed # to be called and instead should raise an Exception: - cmd = 'ip link del dev "{}"'.format(self._ifname) + cmd = 'ip link del dev {}'.format(self._ifname) self._cmd(cmd) def _cmd(self, command): - self._debug_msg("{:<6} '{}'".format('cmd', command)) + self._debug_msg("cmd '{}'".format(command)) process = subprocess.Popen(command,stdout=subprocess.PIPE, shell=True) proc_stdout = process.communicate()[0].strip() @@ -126,7 +126,7 @@ class Interface: with open(filename, 'r') as f: value = f.read().rstrip('\n') - self._debug_msg("{:<6} '{}' < '{}'".format('read', value, filename)) + self._debug_msg("read '{}' < '{}'".format(value, filename)) return value @@ -134,7 +134,7 @@ class Interface: """ Provide a single primitive w/ error checking for writing to sysfs. """ - self._debug_msg("{:<6} '{}' > '{}'".format('write', value, filename)) + self._debug_msg("write '{}' > '{}'".format(value, filename)) with open(filename, 'w') as f: f.write(str(value)) @@ -216,7 +216,7 @@ class Interface: # Assemble command executed on system. Unfortunately there is no way # of altering the MAC address via sysfs - cmd = 'ip link set dev "{}" address "{}"'.format(self._ifname, mac) + cmd = 'ip link set dev {} address {}'.format(self._ifname, mac) self._cmd(cmd) @@ -360,7 +360,7 @@ class Interface: # Assemble command executed on system. Unfortunately there is no way # to up/down an interface via sysfs - cmd = 'ip link set dev "{}" "{}"'.format(self._ifname, state) + cmd = 'ip link set dev {} {}'.format(self._ifname, state) self._cmd(cmd) @property @@ -924,7 +924,7 @@ class BridgeIf(Interface): >>> BridgeIf('br0').add_port('eth0') >>> BridgeIf('br0').add_port('eth1') """ - cmd = 'ip link set dev "{}" master "{}"'.format(interface, self._ifname) + cmd = 'ip link set dev {} master {}'.format(interface, self._ifname) self._cmd(cmd) @@ -936,7 +936,7 @@ class BridgeIf(Interface): >>> from vyos.ifconfig import Interface >>> BridgeIf('br0').del_port('eth1') """ - cmd = 'ip link set dev "{}" nomaster'.format(interface) + cmd = 'ip link set dev {} nomaster'.format(interface) self._cmd(cmd) -- cgit v1.2.3 From ff34756f534bfc0f09a5ab6db0d36e1bf43546a8 Mon Sep 17 00:00:00 2001 From: hagbard Date: Wed, 4 Sep 2019 12:50:43 -0700 Subject: [wireguard] - T1628: Adopt WireGuard configuration script to new vyos.ifconfig class --- python/vyos/ifconfig.py | 70 ++++++ src/conf_mode/interface-wireguard.py | 447 +++++++++++++---------------------- 2 files changed, 232 insertions(+), 285 deletions(-) (limited to 'python/vyos') diff --git a/python/vyos/ifconfig.py b/python/vyos/ifconfig.py index 449923f09..ad3a066a8 100644 --- a/python/vyos/ifconfig.py +++ b/python/vyos/ifconfig.py @@ -1273,3 +1273,73 @@ class BondIf(EthernetIf): return self._write_sysfs('/sys/class/net/{}/bonding/mode' .format(self._ifname), mode) + +class WireGuardIf(Interface): + """ + Wireguard interface class, contains a comnfig dictionary since + wireguard VPN is being comnfigured via the wg command rather than + writing the config into a file. Otherwise if a pre-shared key is used + (symetric enryption key), it would we exposed within multiple files. + Currently it's only within the config.boot if the config was saved. + + Example: + >>> from vyos.ifconfig import WireGuardIf as wg_if + >>> wg_intfc = wg_if("wg01") + >>> print (wg_intfc.wg_config) + {'private-key': None, 'keepalive': 0, 'endpoint': None, 'port': 0, 'allowed-ips': [], 'pubkey': None, 'fwmark': 0, 'psk': '/dev/null'} + >>> wg_intfc.wg_config['keepalive'] = 100 + >>> print (wg_intfc.wg_config) + {'private-key': None, 'keepalive': 100, 'endpoint': None, 'port': 0, 'allowed-ips': [], 'pubkey': None, 'fwmark': 0, 'psk': '/dev/null'} + """ + def __init__(self, ifname): + super().__init__(ifname, type='wireguard') + self.wg_config = { + 'port' : 0, + 'private-key' : None, + 'pubkey' : None, + 'psk' : '/dev/null', + 'allowed-ips' : [], + 'fwmark' : 0x00, + 'endpoint' : None, + 'keepalive' : 0 + } + + def wg_update(self): + if not self.wg_config['private-key']: + raise ValueError("private key required") + else: + ### fmask permission check? + pass + + cmd = "wg set {} ".format(self._ifname) + cmd += "listen-port {} ".format(self.wg_config['port']) + cmd += "fwmark {} ".format(str(self.wg_config['fwmark'])) + cmd += "private-key {} ".format(self.wg_config['private-key']) + cmd += "peer {} ".format(self.wg_config['pubkey']) + cmd += " preshared-key {} ".format(self.wg_config['psk']) + cmd += " allowed-ips " + for aip in self.wg_config['allowed-ips']: + if aip != self.wg_config['allowed-ips'][-1]: + cmd += aip + "," + else: + cmd += aip + if self.wg_config['endpoint']: + cmd += " endpoint {}".format(self.wg_config['endpoint']) + cmd += " persistent-keepalive {}".format(self.wg_config['keepalive']) + + self._cmd(cmd) + + ### remove psk since it isn't required anymore and is saved in the cli config only !! + if self.wg_config['psk'] != '/dev/null': + if os.path.exists(self.wg_config['psk']): + os.remove(self.wg_config['psk']) + + """ + Remove a peer of an interface, peers are identified by their public key. + Giving it a readable name is a vyos feature, to remove a peer the pubkey + and the interface is needed, to remove the entry. + """ + def wg_remove_peer(self, peerkey): + cmd = "sudo wg set {0} peer {1} remove".format(self._ifname, str(peerkey)) + self._cmd(cmd) + diff --git a/src/conf_mode/interface-wireguard.py b/src/conf_mode/interface-wireguard.py index 8234fad0b..40356da51 100755 --- a/src/conf_mode/interface-wireguard.py +++ b/src/conf_mode/interface-wireguard.py @@ -24,11 +24,15 @@ import subprocess from vyos.config import Config from vyos import ConfigError +from vyos.ifconfig import WireGuardIf as wg_if + +ifname = str(os.environ['VYOS_TAGNODE_VALUE']) +wg_intfc = wg_if(ifname) dir = r'/config/auth/wireguard' pk = dir + '/private.key' pub = dir + '/public.key' -psk_file = r'/tmp/psk' +psk_file = dir + '/psk' def check_kmod(): if not os.path.exists('/sys/module/wireguard'): @@ -42,92 +46,61 @@ def get_config(): if not c.exists('interfaces wireguard'): return None - c.set_level('interfaces') - intfcs = c.list_nodes('wireguard') - intfcs_eff = c.list_effective_nodes('wireguard') - new_lst = list(set(intfcs) - set(intfcs_eff)) - del_lst = list(set(intfcs_eff) - set(intfcs)) - config_data = { - 'interfaces' : {} + ifname : { + 'addr' : '', + 'descr' : ifname, + 'lport' : None, + 'status' : 'exists', + 'state' : 'enabled', + 'fwmark' : 0x00, + 'mtu' : 1420, + 'peer' : {} + } } - ### setting defaults and determine status of the config - for intfc in intfcs: - cnf = 'wireguard ' + intfc - # default data struct - config_data['interfaces'].update( - { - intfc : { - 'addr' : '', - 'descr' : intfc, ## snmp ifAlias - 'lport' : '', - 'status' : 'exists', - 'state' : 'enabled', - 'fwmark' : 0x00, - 'mtu' : '1420', - 'peer' : {} - } - } - ) - - ### determine status either delete or create - for i in new_lst: - config_data['interfaces'][i]['status'] = 'create' - - for i in del_lst: - config_data['interfaces'].update( - { - i : { - 'status': 'delete' - } - } - ) - - ### based on the status, setup conf values - for intfc in intfcs: - cnf = 'wireguard ' + intfc - if config_data['interfaces'][intfc]['status'] != 'delete': - ### addresses - if c.exists(cnf + ' address'): - config_data['interfaces'][intfc]['addr'] = c.return_values(cnf + ' address') - ### interface up/down - if c.exists(cnf + ' disable'): - config_data['interfaces'][intfc]['state'] = 'disable' - ### listen port - if c.exists(cnf + ' port'): - config_data['interfaces'][intfc]['lport'] = c.return_value(cnf + ' port') - ### fwmark - if c.exists(cnf + ' fwmark'): - config_data['interfaces'][intfc]['fwmark'] = c.return_value(cnf + ' fwmark') - ### description - if c.exists(cnf + ' description'): - config_data['interfaces'][intfc]['descr'] = c.return_value(cnf + ' description') - ### mtu - if c.exists(cnf + ' mtu'): - config_data['interfaces'][intfc]['mtu'] = c.return_value(cnf + ' mtu') - ### peers - if c.exists(cnf + ' peer'): - for p in c.list_nodes(cnf + ' peer'): - if not c.exists(cnf + ' peer ' + p + ' disable'): - config_data['interfaces'][intfc]['peer'].update( - { - p : { - 'allowed-ips' : [], - 'endpoint' : '', - 'pubkey' : '' - } - } - ) - if c.exists(cnf + ' peer ' + p + ' pubkey'): - config_data['interfaces'][intfc]['peer'][p]['pubkey'] = c.return_value(cnf + ' peer ' + p + ' pubkey') - if c.exists(cnf + ' peer ' + p + ' allowed-ips'): - config_data['interfaces'][intfc]['peer'][p]['allowed-ips'] = c.return_values(cnf + ' peer ' + p + ' allowed-ips') - if c.exists(cnf + ' peer ' + p + ' endpoint'): - config_data['interfaces'][intfc]['peer'][p]['endpoint'] = c.return_value(cnf + ' peer ' + p + ' endpoint') - if c.exists(cnf + ' peer ' + p + ' persistent-keepalive'): - config_data['interfaces'][intfc]['peer'][p]['persistent-keepalive'] = c.return_value(cnf + ' peer ' + p + ' persistent-keepalive') - if c.exists(cnf + ' peer ' + p + ' preshared-key'): - config_data['interfaces'][intfc]['peer'][p]['psk'] = c.return_value(cnf + ' peer ' + p + ' preshared-key') + + c.set_level('interfaces wireguard') + if not c.exists_effective(ifname): + config_data[ifname]['status'] = 'create' + + if not c.exists(ifname) and c.exists_effective(ifname): + config_data[ifname]['status'] = 'delete' + + if config_data[ifname]['status'] != 'delete': + if c.exists(ifname + ' address'): + config_data[ifname]['addr'] = c.return_values(ifname + ' address') + if c.exists(ifname + ' disable'): + config_data[ifname]['state'] = 'disable' + if c.exists(ifname + ' port'): + config_data[ifname]['lport'] = c.return_value(ifname + ' port') + if c.exists(ifname + ' fwmark'): + config_data[ifname]['fwmark'] = c.return_value(ifname + ' fwmark') + if c.exists(ifname + ' description'): + config_data[ifname]['descr'] = c.return_value(ifname + ' description') + if c.exists(ifname + ' mtu'): + config_data[ifname]['mtu'] = c.return_value(ifname + ' mtu') + if c.exists(ifname + ' peer'): + for p in c.list_nodes(ifname + ' peer'): + if not c.exists(ifname + ' peer ' + p + ' disable'): + config_data[ifname]['peer'].update( + { + p : { + 'allowed-ips' : [], + 'endpoint' : '', + 'pubkey' : '' + } + } + ) + if c.exists(ifname + ' peer ' + p + ' pubkey'): + config_data[ifname]['peer'][p]['pubkey'] = c.return_value(ifname + ' peer ' + p + ' pubkey') + if c.exists(ifname + ' peer ' + p + ' allowed-ips'): + config_data[ifname]['peer'][p]['allowed-ips'] = c.return_values(ifname + ' peer ' + p + ' allowed-ips') + if c.exists(ifname + ' peer ' + p + ' endpoint'): + config_data[ifname]['peer'][p]['endpoint'] = c.return_value(ifname + ' peer ' + p + ' endpoint') + if c.exists(ifname + ' peer ' + p + ' persistent-keepalive'): + config_data[ifname]['peer'][p]['persistent-keepalive'] = c.return_value(ifname + ' peer ' + p + ' persistent-keepalive') + if c.exists(ifname + ' peer ' + p + ' preshared-key'): + config_data[ifname]['peer'][p]['psk'] = c.return_value(ifname + ' peer ' + p + ' preshared-key') return config_data @@ -135,22 +108,22 @@ def verify(c): if not c: return None - for i in c['interfaces']: - if c['interfaces'][i]['status'] != 'delete': - if not c['interfaces'][i]['addr']: - raise ConfigError("address required for interface " + i) - if not c['interfaces'][i]['peer']: - raise ConfigError("peer required on interface " + i) - - for p in c['interfaces'][i]['peer']: - if not c['interfaces'][i]['peer'][p]['allowed-ips']: - raise ConfigError("allowed-ips required on interface " + i + " for peer " + p) - if not c['interfaces'][i]['peer'][p]['pubkey']: - raise ConfigError("pubkey from your peer is mandatory on " + i + " for peer " + p) + if not os.path.exists(pk): + raise ConfigError("No keys found, generate them by executing: \'run generate wireguard keypair\'") + if c[ifname]['status'] != 'delete': + if not c[ifname]['addr']: + raise ConfigError("ERROR: IP address required") + if not c[ifname]['peer']: + raise ConfigError("ERROR: peer required") + for p in c[ifname]['peer']: + if not c[ifname]['peer'][p]['allowed-ips']: + raise ConfigError("ERROR: allowed-ips required for peer " + p) + if not c[ifname]['peer'][p]['pubkey']: + raise ConfigError("peer pubkey required for peer " + p) def apply(c): - ### no wg config left, delete all wireguard devices on the os + ### no wg config left, delete all wireguard devices, if any if not c: net_devs = os.listdir('/sys/class/net/') for dev in net_devs: @@ -162,205 +135,109 @@ def apply(c): subprocess.call(['ip l d dev ' + wg_intf + ' >/dev/null'], shell=True) return None - ### - ## find the diffs between effective config an new config - ### + ### interface removal + if c[ifname]['status'] == 'delete': + sl.syslog(sl.LOG_NOTICE, "removing interface " + ifname) + wg_intfc.remove() + return None + c_eff = Config() c_eff.set_level('interfaces wireguard') - ### link status up/down aka interface disable - - for intf in c['interfaces']: - if not c['interfaces'][intf]['status'] == 'delete': - if c['interfaces'][intf]['state'] == 'disable': - sl.syslog(sl.LOG_NOTICE, "disable interface " + intf) - subprocess.call(['ip l s dev ' + intf + ' down ' + ' &>/dev/null'], shell=True) - else: - sl.syslog(sl.LOG_NOTICE, "enable interface " + intf) - subprocess.call(['ip l s dev ' + intf + ' up ' + ' &>/dev/null'], shell=True) - - ### deletion of a specific interface - for intf in c['interfaces']: - if c['interfaces'][intf]['status'] == 'delete': - sl.syslog(sl.LOG_NOTICE, "removing interface " + intf) - subprocess.call(['ip l d dev ' + intf + ' &>/dev/null'], shell=True) - - ### peer deletion - peer_eff = c_eff.list_effective_nodes( intf + ' peer') - peer_cnf = [] - try: - for p in c['interfaces'][intf]['peer']: - peer_cnf.append(p) - except KeyError: - pass - - peer_rem = list(set(peer_eff) - set(peer_cnf)) - for p in peer_rem: - pkey = c_eff.return_effective_value( intf + ' peer ' + p +' pubkey') - remove_peer(intf, pkey) - - ### peer pubkey update - ### wg identifies peers by its pubky, so we have to remove the peer first - ### it will recreated it then below with the new key from the cli config - for p in peer_eff: - if p in peer_cnf: - ekey = c_eff.return_effective_value( intf + ' peer ' + p +' pubkey') - nkey = c['interfaces'][intf]['peer'][p]['pubkey'] - if nkey != ekey: - sl.syslog(sl.LOG_NOTICE, "peer " + p + ' changed pubkey from ' + ekey + 'to key ' + nkey + ' on interface ' + intf) - remove_peer(intf, ekey) - - ### new config - if c['interfaces'][intf]['status'] == 'create': - if not os.path.exists(pk): - raise ConfigError("No keys found, generate them by executing: \'run generate wireguard keypair\'") - - subprocess.call(['ip l a dev ' + intf + ' type wireguard 2>/dev/null'], shell=True) - for addr in c['interfaces'][intf]['addr']: - add_addr(intf, addr) - - subprocess.call(['ip l set up dev ' + intf + ' mtu ' + c['interfaces'][intf]['mtu'] + ' &>/dev/null'], shell=True) - configure_interface(c, intf) - - ### config updates - if c['interfaces'][intf]['status'] == 'exists': - ### IP address change - addr_eff = c_eff.return_effective_values(intf + ' address') - addr_rem = list(set(addr_eff) - set(c['interfaces'][intf]['addr'])) - addr_add = list(set(c['interfaces'][intf]['addr']) - set(addr_eff)) - - if len(addr_rem) != 0: - for addr in addr_rem: - del_addr(intf, addr) - - if len(addr_add) != 0: - for addr in addr_add: - add_addr(intf, addr) - - ## mtu update - mtu = c['interfaces'][intf]['mtu'] - if mtu != 1420: - sl.syslog(sl.LOG_NOTICE, "setting mtu to " + mtu + " on " + intf) - subprocess.call(['ip l set mtu ' + mtu + ' dev ' + intf + ' &>/dev/null'], shell=True) - - - ### persistent-keepalive - for p in c['interfaces'][intf]['peer']: - val_eff = "" - val = "" - - try: - val = c['interfaces'][intf]['peer'][p]['persistent-keepalive'] - except KeyError: - pass - - if c_eff.exists_effective(intf + ' peer ' + p + ' persistent-keepalive'): - val_eff = c_eff.return_effective_value(intf + ' peer ' + p + ' persistent-keepalive') - - ### disable keepalive - if val_eff and not val: - c['interfaces'][intf]['peer'][p]['persistent-keepalive'] = 0 - - ### set new keepalive value - if not val_eff and val: - c['interfaces'][intf]['peer'][p]['persistent-keepalive'] = val - - ## wg command call - configure_interface(c, intf) - - ### ifalias for snmp from description - if c['interfaces'][intf]['status'] != 'delete': - descr_eff = c_eff.return_effective_value(intf + ' description') - cnf_descr = c['interfaces'][intf]['descr'] - if descr_eff != cnf_descr: - with open('/sys/class/net/' + str(intf) + '/ifalias', 'w') as fh: - fh.write(str(cnf_descr)) - -def configure_interface(c, intf): - for p in c['interfaces'][intf]['peer']: - ## config init for wg call - wg_config = { - 'interface' : intf, - 'port' : 0, - 'private-key' : pk, - 'pubkey' : '', - 'psk' : '/dev/null', - 'allowed-ips' : [], - 'fwmark' : 0x00, - 'endpoint' : None, - 'keepalive' : 0 - } - - ## mandatory settings - wg_config['pubkey'] = c['interfaces'][intf]['peer'][p]['pubkey'] - wg_config['allowed-ips'] = c['interfaces'][intf]['peer'][p]['allowed-ips'] + ## interface state + if c[ifname]['state'] == 'disable': + sl.syslog(sl.LOG_NOTICE, "disable interface " + ifname) + wg_intfc.state = 'down' + else: + if not wg_intfc.state == 'up': + sl.syslog(sl.LOG_NOTICE, "enable interface " + ifname) + wg_intfc.state = 'up' + + ## IP address + if not c_eff.exists_effective(ifname + ' address'): + for ip in c[ifname]['addr']: + wg_intfc.add_addr(ip) + else: + addr_eff = c_eff.return_effective_values(ifname + ' address') + addr_rem = list(set(addr_eff) - set(c[ifname]['addr'])) + addr_add = list(set(c[ifname]['addr']) - set(addr_eff)) + + if len(addr_rem) !=0: + for ip in addr_rem: + sl.syslog(sl.LOG_NOTICE, "remove IP address {0} from {1}".format(ip,ifname)) + wg_intfc.del_addr(ip) + + if len(addr_add) !=0: + for ip in addr_add: + sl.syslog(sl.LOG_NOTICE, "add IP address {0} to {1}".format(ip,ifname)) + wg_intfc.add_addr(ip) + + ## interface MTU + if c[ifname]['mtu'] != 1420: + wg_intfc.mtu = int(c[ifname]['mtu']) + else: + ## default is set to 1420 in config_data + wg_intfc.mtu = int(c[ifname]['mtu']) + + ## ifalias for snmp from description + descr_eff = c_eff.return_effective_value(ifname + ' description') + if descr_eff != c[ifname]['descr']: + wg_intfc.ifalias = str(c[ifname]['descr']) + + ## peer deletion + peer_eff = c_eff.list_effective_nodes(ifname + ' peer') + peer_cnf = [] - ## optional settings - # listen-port - if c['interfaces'][intf]['lport']: - wg_config['port'] = c['interfaces'][intf]['lport'] + try: + for p in c[ifname]['peer']: + peer_cnf.append(p) + except KeyError: + pass + + peer_rem = list(set(peer_eff) - set(peer_cnf)) + for p in peer_rem: + pkey = c_eff.return_effective_value( ifname + ' peer ' + p +' pubkey') + wg_intfc.wg_remove_peer(pkey) + + ## peer key update + for p in peer_eff: + if p in peer_cnf: + ekey = c_eff.return_effective_value( ifname + ' peer ' + p +' pubkey') + nkey = c[ifname]['peer'][p]['pubkey'] + if nkey != ekey: + sl.syslog(sl.LOG_NOTICE, "peer {0} pubkey changed from {1} to {2} on interface {3}".format(p, ekey, nkey, ifname)) + print ("peer {0} pubkey changed from {1} to {2} on interface {3}".format(p, ekey, nkey, ifname)) + wg_intfc.wg_remove_peer(ekey) + + wg_intfc.wg_config['private-key'] = pk + for p in c[ifname]['peer']: + wg_intfc.wg_config['pubkey'] = str(c[ifname]['peer'][p]['pubkey']) + wg_intfc.wg_config['allowed-ips'] = (c[ifname]['peer'][p]['allowed-ips']) + + ## listen-port + if c[ifname]['lport']: + wg_intfc.wg_config['port'] = c[ifname]['lport'] ## fwmark - if c['interfaces'][intf]['fwmark']: - wg_config['fwmark'] = c['interfaces'][intf]['fwmark'] - + if c[ifname]['fwmark']: + wg_intfc.wg_config['fwmark'] = c[ifname]['fwmark'] + ## endpoint - if c['interfaces'][intf]['peer'][p]['endpoint']: - wg_config['endpoint'] = c['interfaces'][intf]['peer'][p]['endpoint'] + if c[ifname]['peer'][p]['endpoint']: + wg_intfc.wg_config['endpoint'] = c[ifname]['peer'][p]['endpoint'] ## persistent-keepalive - if 'persistent-keepalive' in c['interfaces'][intf]['peer'][p]: - wg_config['keepalive'] = c['interfaces'][intf]['peer'][p]['persistent-keepalive'] + if 'persistent-keepalive' in c[ifname]['peer'][p]: + wg_intfc.wg_config['keepalive'] = c[ifname]['peer'][p]['persistent-keepalive'] - ## preshared-key - is only read from a file, it's called via sudo redirection doesn't work either - if 'psk' in c['interfaces'][intf]['peer'][p]: + ## preshared-key - needs to be read from a file + if 'psk' in c[ifname]['peer'][p]: old_umask = os.umask(0o077) - open(psk_file, 'w').write(str(c['interfaces'][intf]['peer'][p]['psk'])) + open(psk_file, 'w').write(str(c[ifname]['peer'][p]['psk'])) os.umask(old_umask) - wg_config['psk'] = psk_file - - ### assemble wg command - cmd = "sudo wg set " + intf - cmd += " listen-port " + str(wg_config['port']) - cmd += " fwmark " + str(wg_config['fwmark']) - cmd += " private-key " + wg_config['private-key'] - cmd += " peer " + wg_config['pubkey'] - cmd += " preshared-key " + wg_config['psk'] - cmd += " allowed-ips " - for ap in wg_config['allowed-ips']: - if ap != wg_config['allowed-ips'][-1]: - cmd += ap + "," - else: - cmd += ap - - if wg_config['endpoint']: - cmd += " endpoint " + wg_config['endpoint'] - - if wg_config['keepalive'] != 0: - cmd += " persistent-keepalive " + wg_config['keepalive'] - else: - cmd += " persistent-keepalive 0" - - sl.syslog(sl.LOG_NOTICE, cmd) - #print (cmd) - subprocess.call([cmd], shell=True) - """ remove psk_file """ - if os.path.exists(psk_file): - os.remove(psk_file) - -def add_addr(intf, addr): - # see https://phabricator.vyos.net/T949 - ret = subprocess.call(['ip a a dev ' + intf + ' ' + addr + ' &>/dev/null'], shell=True) - sl.syslog(sl.LOG_NOTICE, "ip a a dev " + intf + " " + addr) - -def del_addr(intf, addr): - ret = subprocess.call(['ip a d dev ' + intf + ' ' + addr + ' &>/dev/null'], shell=True) - sl.syslog(sl.LOG_NOTICE, "ip a d dev " + intf + " " + addr) - -def remove_peer(intf, peer_key): - cmd = 'sudo wg set ' + str(intf) + ' peer ' + peer_key + ' remove &>/dev/null' - ret = subprocess.call([cmd], shell=True) - sl.syslog(sl.LOG_NOTICE, "peer " + peer_key + " removed from " + intf) + wg_intfc.wg_config['psk'] = psk_file + + wg_intfc.wg_update() if __name__ == '__main__': try: -- cgit v1.2.3 From 64d58eda4c1ebaa9d346d65d606d2a75694467ee Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Wed, 4 Sep 2019 21:50:04 +0200 Subject: Python/configdict: add list_diff function to compare two lists A list containing only unique elements not part of the other list is returned. This is usefull to check e.g. which IP addresses need to be removed from the OS. --- python/vyos/configdict.py | 8 ++++++++ src/conf_mode/interface-bonding.py | 11 +++++------ src/conf_mode/interface-bridge.py | 12 +++++------- src/conf_mode/interface-dummy.py | 10 ++++------ src/conf_mode/interface-loopback.py | 7 +++---- 5 files changed, 25 insertions(+), 23 deletions(-) (limited to 'python/vyos') diff --git a/python/vyos/configdict.py b/python/vyos/configdict.py index 157011839..a723c5322 100644 --- a/python/vyos/configdict.py +++ b/python/vyos/configdict.py @@ -78,3 +78,11 @@ def retrieve_config(path_hash, base_path, config): config_hash[k][node] = retrieve_config(inner_hash, path + [node], config) return config_hash + + +def list_diff(first, second): + """ + Diff two dictionaries and return only unique items + """ + second = set(second) + return [item for item in first if item not in second] diff --git a/src/conf_mode/interface-bonding.py b/src/conf_mode/interface-bonding.py index 03d28954d..ba4577900 100755 --- a/src/conf_mode/interface-bonding.py +++ b/src/conf_mode/interface-bonding.py @@ -23,6 +23,7 @@ from sys import exit from netifaces import interfaces from vyos.ifconfig import BondIf, EthernetIf +from vyos.configdict import list_diff from vyos.config import Config from vyos import ConfigError @@ -55,9 +56,6 @@ default_config_data = { 'vif_remove': [] } -def diff(first, second): - second = set(second) - return [item for item in first if item not in second] def get_bond_mode(mode): if mode == 'round-robin': @@ -77,6 +75,7 @@ def get_bond_mode(mode): else: raise ConfigError('invalid bond mode "{}"'.format(mode)) + def get_ethertype(ethertype_val): if ethertype_val == '0x88A8': return '802.1ad' @@ -321,7 +320,7 @@ def get_config(): # address is no longer valid and needs to be removed from the bond eff_addr = conf.return_effective_values('address') act_addr = conf.return_values('address') - bond['address_remove'] = diff(eff_addr, act_addr) + bond['address_remove'] = list_diff(eff_addr, act_addr) # Primary device interface if conf.exists('primary'): @@ -333,7 +332,7 @@ def get_config(): # interface is no longer present and needs to be removed eff_intf = conf.list_effective_nodes('vif-s') act_intf = conf.list_nodes('vif-s') - bond['vif_s_remove'] = diff(eff_intf, act_intf) + bond['vif_s_remove'] = list_diff(eff_intf, act_intf) if conf.exists('vif-s'): for vif_s in conf.list_nodes('vif-s'): @@ -347,7 +346,7 @@ def get_config(): # vif interface is no longer present and needs to be removed eff_intf = conf.list_effective_nodes('vif') act_intf = conf.list_nodes('vif') - bond['vif_remove'] = diff(eff_intf, act_intf) + bond['vif_remove'] = list_diff(eff_intf, act_intf) if conf.exists('vif'): for vif in conf.list_nodes('vif'): diff --git a/src/conf_mode/interface-bridge.py b/src/conf_mode/interface-bridge.py index 1d3587114..b165428ee 100755 --- a/src/conf_mode/interface-bridge.py +++ b/src/conf_mode/interface-bridge.py @@ -21,8 +21,10 @@ import os from copy import deepcopy from sys import exit from netifaces import interfaces -from vyos.config import Config + from vyos.ifconfig import BridgeIf, Interface +from vyos.configdict import list_diff +from vyos.config import Config from vyos import ConfigError default_config_data = { @@ -46,10 +48,6 @@ default_config_data = { 'stp': 0 } -def diff(first, second): - second = set(second) - return [item for item in first if item not in second] - def get_config(): bridge = deepcopy(default_config_data) conf = Config() @@ -137,13 +135,13 @@ def get_config(): # interfaces is no longer assigend to the bridge and thus can be removed eff_intf = conf.list_effective_nodes('member interface') act_intf = conf.list_nodes('member interface') - bridge['member_remove'] = diff(eff_intf, act_intf) + bridge['member_remove'] = list_diff(eff_intf, act_intf) # Determine interface addresses (currently effective) - to determine which # address is no longer valid and needs to be removed from the bridge eff_addr = conf.return_effective_values('address') act_addr = conf.return_values('address') - bridge['address_remove'] = diff(eff_addr, act_addr) + bridge['address_remove'] = list_diff(eff_addr, act_addr) # Priority for this bridge if conf.exists('priority'): diff --git a/src/conf_mode/interface-dummy.py b/src/conf_mode/interface-dummy.py index 03afdc668..4a1179672 100755 --- a/src/conf_mode/interface-dummy.py +++ b/src/conf_mode/interface-dummy.py @@ -19,8 +19,10 @@ from os import environ from copy import deepcopy from sys import exit -from vyos.config import Config + from vyos.ifconfig import DummyIf +from vyos.configdict import list_diff +from vyos.config import Config from vyos import ConfigError default_config_data = { @@ -32,10 +34,6 @@ default_config_data = { 'intf': '' } -def diff(first, second): - second = set(second) - return [item for item in first if item not in second] - def get_config(): dummy = deepcopy(default_config_data) conf = Config() @@ -70,7 +68,7 @@ def get_config(): # address is no longer valid and needs to be removed from the interface eff_addr = conf.return_effective_values('address') act_addr = conf.return_values('address') - dummy['address_remove'] = diff(eff_addr, act_addr) + dummy['address_remove'] = list_diff(eff_addr, act_addr) return dummy diff --git a/src/conf_mode/interface-loopback.py b/src/conf_mode/interface-loopback.py index be47324c1..e2df37655 100755 --- a/src/conf_mode/interface-loopback.py +++ b/src/conf_mode/interface-loopback.py @@ -18,7 +18,9 @@ from os import environ from sys import exit from copy import deepcopy + from vyos.ifconfig import LoopbackIf +from vyos.configdict import list_diff from vyos.config import Config from vyos import ConfigError @@ -29,9 +31,6 @@ default_config_data = { 'description': '', } -def diff(first, second): - second = set(second) - return [item for item in first if item not in second] def get_config(): loopback = deepcopy(default_config_data) @@ -62,7 +61,7 @@ def get_config(): # address is no longer valid and needs to be removed from the interface eff_addr = conf.return_effective_values('address') act_addr = conf.return_values('address') - loopback['address_remove'] = diff(eff_addr, act_addr) + loopback['address_remove'] = list_diff(eff_addr, act_addr) return loopback -- cgit v1.2.3 From 952871200ecee584e7ed1bcb37bdaa06111e3a72 Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Wed, 4 Sep 2019 21:50:46 +0200 Subject: Python/configdict: add function vlan_to_dict A generic function which can parse the VLAN (vif, vif-s, cif-c) nodes in a config session. A dictionary describing the VLAN is returned. A good example will be the interface-bonding.py script used to generate bond interfaces in the system. It is used as follows: if conf.exists('vif'): for vif in conf.list_nodes('vif'): # set config level to vif interface conf.set_level(cfg_base + ' vif ' + vif) bond['vif'].append(vlan_to_dict(conf)) --- python/vyos/configdict.py | 108 ++++++++++++++++++++++++++++++++++++ src/conf_mode/interface-bonding.py | 109 +------------------------------------ 2 files changed, 109 insertions(+), 108 deletions(-) (limited to 'python/vyos') diff --git a/python/vyos/configdict.py b/python/vyos/configdict.py index a723c5322..4bc8863bb 100644 --- a/python/vyos/configdict.py +++ b/python/vyos/configdict.py @@ -18,6 +18,7 @@ A library for retrieving value dicts from VyOS configs in a declarative fashion. """ +from vyos import ConfigError def retrieve_config(path_hash, base_path, config): """ @@ -86,3 +87,110 @@ def list_diff(first, second): """ second = set(second) return [item for item in first if item not in second] + + +def get_ethertype(ethertype_val): + if ethertype_val == '0x88A8': + return '802.1ad' + elif ethertype_val == '0x8100': + return '802.1q' + else: + raise ConfigError('invalid ethertype "{}"'.format(ethertype_val)) + + +def vlan_to_dict(conf): + """ + Common used function which will extract VLAN related information from config + and represent the result as Python dictionary. + + Function call's itself recursively if a vif-s/vif-c pair is detected. + """ + vlan = { + 'id': conf.get_level().split()[-1], # get the '100' in 'interfaces bonding bond0 vif-s 100' + 'address': [], + 'address_remove': [], + 'description': '', + 'dhcp_client_id': '', + 'dhcp_hostname': '', + 'dhcpv6_prm_only': False, + 'dhcpv6_temporary': False, + 'disable': False, + 'disable_link_detect': 1, + 'mac': '', + 'mtu': 1500 + } + # retrieve configured interface addresses + if conf.exists('address'): + vlan['address'] = conf.return_values('address') + + # Determine interface addresses (currently effective) - to determine which + # address is no longer valid and needs to be removed from the bond + eff_addr = conf.return_effective_values('address') + act_addr = conf.return_values('address') + vlan['address_remove'] = list_diff(eff_addr, act_addr) + + # retrieve interface description + if conf.exists('description'): + vlan['description'] = conf.return_value('description') + + # get DHCP client identifier + if conf.exists('dhcp-options client-id'): + vlan['dhcp_client_id'] = conf.return_value('dhcp-options client-id') + + # DHCP client host name (overrides the system host name) + if conf.exists('dhcp-options host-name'): + vlan['dhcp_hostname'] = conf.return_value('dhcp-options host-name') + + # DHCPv6 only acquire config parameters, no address + if conf.exists('dhcpv6-options parameters-only'): + vlan['dhcpv6_prm_only'] = conf.return_value('dhcpv6-options parameters-only') + + # DHCPv6 temporary IPv6 address + if conf.exists('dhcpv6-options temporary'): + vlan['dhcpv6_temporary'] = conf.return_value('dhcpv6-options temporary') + + # ignore link state changes + if conf.exists('disable-link-detect'): + vlan['disable_link_detect'] = 2 + + # disable bond interface + if conf.exists('disable'): + vlan['disable'] = True + + # Media Access Control (MAC) address + if conf.exists('mac'): + vlan['mac'] = conf.return_value('mac') + + # Maximum Transmission Unit (MTU) + if conf.exists('mtu'): + vlan['mtu'] = int(conf.return_value('mtu')) + + # ethertype is mandatory on vif-s nodes and only exists here! + # check if this is a vif-s node at all: + if conf.get_level().split()[-2] == 'vif-s': + vlan['vif_c'] = [] + vlan['vif_c_remove'] = [] + + # ethertype uses a default of 0x88A8 + tmp = '0x88A8' + if conf.exists('ethertype'): + tmp = conf.return_value('ethertype') + vlan['ethertype'] = get_ethertype(tmp) + + # get vif-c interfaces (currently effective) - to determine which vif-c + # interface is no longer present and needs to be removed + eff_intf = conf.list_effective_nodes('vif-c') + act_intf = conf.list_nodes('vif-c') + vlan['vif_c_remove'] = list_diff(eff_intf, act_intf) + + # check if there is a Q-in-Q vlan customer interface + # and call this function recursively + if conf.exists('vif-c'): + cfg_level = conf.get_level() + # add new key (vif-c) to dictionary + for vif in conf.list_nodes('vif-c'): + # set config level to vif interface + conf.set_level(cfg_level + ' vif-c ' + vif) + vlan['vif_c'].append(vlan_to_dict(conf)) + + return vlan diff --git a/src/conf_mode/interface-bonding.py b/src/conf_mode/interface-bonding.py index ba4577900..2ec764965 100755 --- a/src/conf_mode/interface-bonding.py +++ b/src/conf_mode/interface-bonding.py @@ -23,7 +23,7 @@ from sys import exit from netifaces import interfaces from vyos.ifconfig import BondIf, EthernetIf -from vyos.configdict import list_diff +from vyos.configdict import list_diff, vlan_to_dict from vyos.config import Config from vyos import ConfigError @@ -76,113 +76,6 @@ def get_bond_mode(mode): raise ConfigError('invalid bond mode "{}"'.format(mode)) -def get_ethertype(ethertype_val): - if ethertype_val == '0x88A8': - return '802.1ad' - elif ethertype_val == '0x8100': - return '802.1q' - else: - raise ConfigError('invalid ethertype "{}"'.format(ethertype_val)) - - -def vlan_to_dict(conf): - """ - Common used function which will extract VLAN related information from config - and represent the result as Python dictionary. - - Function call's itself recursively if a vif-s/vif-c pair is detected. - """ - vlan = { - 'id': conf.get_level().split()[-1], # get the '100' in 'interfaces bonding bond0 vif-s 100' - 'address': [], - 'address_remove': [], - 'description': '', - 'dhcp_client_id': '', - 'dhcp_hostname': '', - 'dhcpv6_prm_only': False, - 'dhcpv6_temporary': False, - 'disable': False, - 'disable_link_detect': 1, - 'mac': '', - 'mtu': 1500 - } - # retrieve configured interface addresses - if conf.exists('address'): - vlan['address'] = conf.return_values('address') - - # Determine interface addresses (currently effective) - to determine which - # address is no longer valid and needs to be removed from the bond - eff_addr = conf.return_effective_values('address') - act_addr = conf.return_values('address') - vlan['address_remove'] = diff(eff_addr, act_addr) - - # retrieve interface description - if conf.exists('description'): - vlan['description'] = conf.return_value('description') - - # get DHCP client identifier - if conf.exists('dhcp-options client-id'): - vlan['dhcp_client_id'] = conf.return_value('dhcp-options client-id') - - # DHCP client host name (overrides the system host name) - if conf.exists('dhcp-options host-name'): - vlan['dhcp_hostname'] = conf.return_value('dhcp-options host-name') - - # DHCPv6 only acquire config parameters, no address - if conf.exists('dhcpv6-options parameters-only'): - vlan['dhcpv6_prm_only'] = conf.return_value('dhcpv6-options parameters-only') - - # DHCPv6 temporary IPv6 address - if conf.exists('dhcpv6-options temporary'): - vlan['dhcpv6_temporary'] = conf.return_value('dhcpv6-options temporary') - - # ignore link state changes - if conf.exists('disable-link-detect'): - vlan['disable_link_detect'] = 2 - - # disable bond interface - if conf.exists('disable'): - vlan['disable'] = True - - # Media Access Control (MAC) address - if conf.exists('mac'): - vlan['mac'] = conf.return_value('mac') - - # Maximum Transmission Unit (MTU) - if conf.exists('mtu'): - vlan['mtu'] = int(conf.return_value('mtu')) - - # ethertype is mandatory on vif-s nodes and only exists here! - # check if this is a vif-s node at all: - if conf.get_level().split()[-2] == 'vif-s': - vlan['vif_c'] = [] - vlan['vif_c_remove'] = [] - - # ethertype uses a default of 0x88A8 - tmp = '0x88A8' - if conf.exists('ethertype'): - tmp = conf.return_value('ethertype') - vlan['ethertype'] = get_ethertype(tmp) - - # get vif-c interfaces (currently effective) - to determine which vif-c - # interface is no longer present and needs to be removed - eff_intf = conf.list_effective_nodes('vif-c') - act_intf = conf.list_nodes('vif-c') - vlan['vif_c_remove'] = diff(eff_intf, act_intf) - - # check if there is a Q-in-Q vlan customer interface - # and call this function recursively - if conf.exists('vif-c'): - cfg_level = conf.get_level() - # add new key (vif-c) to dictionary - for vif in conf.list_nodes('vif-c'): - # set config level to vif interface - conf.set_level(cfg_level + ' vif-c ' + vif) - vlan['vif_c'].append(vlan_to_dict(conf)) - - return vlan - - def apply_vlan_config(vlan, config): """ Generic function to apply a VLAN configuration from a dictionary -- cgit v1.2.3 From a4f34dfe7e003510a4e6263b012b79221a5a786f Mon Sep 17 00:00:00 2001 From: hagbard Date: Wed, 4 Sep 2019 13:17:09 -0700 Subject: [wireguard] - T1628: fixing comment indent --- python/vyos/ifconfig.py | 173 ++++++++++++++++++++++++------------------------ 1 file changed, 85 insertions(+), 88 deletions(-) (limited to 'python/vyos') diff --git a/python/vyos/ifconfig.py b/python/vyos/ifconfig.py index ad3a066a8..0bc4eff17 100644 --- a/python/vyos/ifconfig.py +++ b/python/vyos/ifconfig.py @@ -40,7 +40,9 @@ interface "{{ intf }}" { dhclient_base = r'/var/lib/dhcp/dhclient_' + class Interface: + def __init__(self, ifname, type=None): """ This is the base interface class which supports basic IP/MAC address @@ -79,12 +81,10 @@ class Interface: self._dhcpv6_pid_file = dhclient_base + self._ifname + '.v6pid' self._dhcpv6_lease_file = dhclient_base + self._ifname + '.v6leases' - def _debug_msg(self, msg): if os.path.isfile('/tmp/vyos.ifconfig.debug'): print('DEBUG/{:<6} {}'.format(self._ifname, msg)) - def remove(self): """ Remove interface from operating system. Removing the interface @@ -107,17 +107,15 @@ class Interface: cmd = 'ip link del dev {}'.format(self._ifname) self._cmd(cmd) - def _cmd(self, command): self._debug_msg("cmd '{}'".format(command)) - process = subprocess.Popen(command,stdout=subprocess.PIPE, shell=True) + process = subprocess.Popen(command, stdout=subprocess.PIPE, shell=True) proc_stdout = process.communicate()[0].strip() # add exception handling code pass - def _read_sysfs(self, filename): """ Provide a single primitive w/ error checking for reading from sysfs. @@ -129,7 +127,6 @@ class Interface: self._debug_msg("read '{}' < '{}'".format(value, filename)) return value - def _write_sysfs(self, filename, value): """ Provide a single primitive w/ error checking for writing to sysfs. @@ -140,7 +137,6 @@ class Interface: return None - @property def mtu(self): """ @@ -154,7 +150,6 @@ class Interface: return self._read_sysfs('/sys/class/net/{0}/mtu' .format(self._ifname)) - @mtu.setter def mtu(self, mtu): """ @@ -185,7 +180,6 @@ class Interface: return self._read_sysfs('/sys/class/net/{0}/address' .format(self._ifname)) - @mac.setter def mac(self, mac): """ @@ -202,7 +196,8 @@ class Interface: if octets != 6: raise ValueError('wrong number of MAC octets: {} '.format(octets)) - # validate against the first mac address byte if it's a multicast address + # validate against the first mac address byte if it's a multicast + # address if int(mac.split(':')[0]) & 1: raise ValueError('{} is a multicast MAC address'.format(mac)) @@ -219,7 +214,6 @@ class Interface: cmd = 'ip link set dev {} address {}'.format(self._ifname, mac) self._cmd(cmd) - @property def arp_cache_tmo(self): """ @@ -234,7 +228,6 @@ class Interface: return (self._read_sysfs('/proc/sys/net/ipv4/neigh/{0}/base_reachable_time_ms' .format(self._ifname)) / 1000) - @arp_cache_tmo.setter def arp_cache_tmo(self, tmo): """ @@ -261,7 +254,6 @@ class Interface: return self._read_sysfs('/proc/sys/net/ipv4/conf/{0}/link_filter' .format(self._ifname)) - @link_detect.setter def link_detect(self, link_filter): """ @@ -289,7 +281,6 @@ class Interface: else: raise ValueError("Value out of range") - @property def ifalias(self): """ @@ -304,7 +295,6 @@ class Interface: return self._read_sysfs('/sys/class/net/{0}/ifalias' .format(self._ifname)) - @ifalias.setter def ifalias(self, ifalias=None): """ @@ -329,7 +319,6 @@ class Interface: self._write_sysfs('/sys/class/net/{0}/ifalias' .format(self._ifname), ifalias) - @property def state(self): """ @@ -343,7 +332,6 @@ class Interface: return self._read_sysfs('/sys/class/net/{0}/operstate' .format(self._ifname)) - @state.setter def state(self, state): """ @@ -393,7 +381,6 @@ class Interface: else: raise ValueError("Value out of range") - @property def proxy_arp_pvlan(self): """ @@ -456,7 +443,6 @@ class Interface: else: raise ValueError("Value out of range") - def get_addr(self): """ Retrieve assigned IPv4 and IPv6 addresses from given interface. @@ -474,24 +460,26 @@ class Interface: if AF_INET in ifaddresses(self._ifname).keys(): for v4_addr in ifaddresses(self._ifname)[AF_INET]: # we need to manually assemble a list of IPv4 address/prefix - prefix = '/' + str(IPv4Network('0.0.0.0/' + v4_addr['netmask']).prefixlen) - ipv4.append( v4_addr['addr'] + prefix ) + prefix = '/' + \ + str(IPv4Network('0.0.0.0/' + v4_addr['netmask']).prefixlen) + ipv4.append(v4_addr['addr'] + prefix) if AF_INET6 in ifaddresses(self._ifname).keys(): for v6_addr in ifaddresses(self._ifname)[AF_INET6]: # Note that currently expanded netmasks are not supported. That means # 2001:db00::0/24 is a valid argument while 2001:db00::0/ffff:ff00:: not. # see https://docs.python.org/3/library/ipaddress.html - bits = bin( int(v6_addr['netmask'].replace(':',''), 16) ).count('1') + bits = bin( + int(v6_addr['netmask'].replace(':', ''), 16)).count('1') prefix = '/' + str(bits) - # we alsoneed to remove the interface suffix on link local addresses + # we alsoneed to remove the interface suffix on link local + # addresses v6_addr['addr'] = v6_addr['addr'].split('%')[0] - ipv6.append( v6_addr['addr'] + prefix ) + ipv6.append(v6_addr['addr'] + prefix) return ipv4 + ipv6 - def add_addr(self, addr): """ Add IP address to interface. Address is only added if it yet not added @@ -509,7 +497,6 @@ class Interface: cmd = 'ip addr add "{}" dev "{}"'.format(addr, self._ifname) self._cmd(cmd) - def del_addr(self, addr): """ Remove IP address from interface. @@ -529,7 +516,6 @@ class Interface: cmd = 'ip addr del "{}" dev "{}"'.format(addr, self._ifname) self._cmd(cmd) - # replace dhcpv4/v6 with systemd.networkd? def set_dhcp(self): """ @@ -558,13 +544,14 @@ class Interface: with open(self._dhcp_cfg_file, 'w') as f: f.write(dhcp_text) - cmd = 'start-stop-daemon --start --quiet --pidfile ' + self._dhcp_pid_file + cmd = 'start-stop-daemon --start --quiet --pidfile ' + \ + self._dhcp_pid_file cmd += ' --exec /sbin/dhclient --' # now pass arguments to dhclient binary - cmd += ' -4 -nw -cf {} -pf {} -lf {} {}'.format(self._dhcp_cfg_file, self._dhcp_pid_file, self._dhcp_lease_file, self._ifname) + cmd += ' -4 -nw -cf {} -pf {} -lf {} {}'.format( + self._dhcp_cfg_file, self._dhcp_pid_file, self._dhcp_lease_file, self._ifname) self._cmd(cmd) - def del_dhcp(self): """ De-configure interface as DHCP clinet. All auto generated files like @@ -585,7 +572,8 @@ class Interface: return None # stop dhclient - cmd = 'start-stop-daemon --stop --quiet --pidfile {}'.format(self._dhcp_pid_file) + cmd = 'start-stop-daemon --stop --quiet --pidfile {}'.format( + self._dhcp_pid_file) self._cmd(cmd) # cleanup old config file @@ -600,7 +588,6 @@ class Interface: if os.path.isfile(self._dhcp_lease_file): os.remove(self._dhcp_lease_file) - def set_dhcpv6(self): """ Configure interface as DHCPv6 client. The dhclient binary is automatically @@ -632,13 +619,14 @@ class Interface: cmd = 'sysctl -q -w net.ipv6.conf.{}.accept_ra=0'.format(self._ifname) self._cmd(cmd) - cmd = 'start-stop-daemon --start --quiet --pidfile ' + self._dhcpv6_pid_file + cmd = 'start-stop-daemon --start --quiet --pidfile ' + \ + self._dhcpv6_pid_file cmd += ' --exec /sbin/dhclient --' # now pass arguments to dhclient binary - cmd += ' -6 -nw -cf {} -pf {} -lf {} {}'.format(self._dhcpv6_cfg_file, self._dhcpv6_pid_file, self._dhcpv6_lease_file, self._ifname) + cmd += ' -6 -nw -cf {} -pf {} -lf {} {}'.format( + self._dhcpv6_cfg_file, self._dhcpv6_pid_file, self._dhcpv6_lease_file, self._ifname) self._cmd(cmd) - def del_dhcpv6(self): """ De-configure interface as DHCPv6 clinet. All auto generated files like @@ -659,7 +647,8 @@ class Interface: return None # stop dhclient - cmd = 'start-stop-daemon --stop --quiet --pidfile {}'.format(self._dhcpv6_pid_file) + cmd = 'start-stop-daemon --stop --quiet --pidfile {}'.format( + self._dhcpv6_pid_file) self._cmd(cmd) # accept router announcements on this interface @@ -680,25 +669,30 @@ class Interface: class LoopbackIf(Interface): + """ The loopback device is a special, virtual network interface that your router uses to communicate with itself. """ + def __init__(self, ifname): super().__init__(ifname, type='loopback') class DummyIf(Interface): + """ A dummy interface is entirely virtual like, for example, the loopback interface. The purpose of a dummy interface is to provide a device to route packets through without actually transmitting them. """ + def __init__(self, ifname): super().__init__(ifname, type='dummy') class BridgeIf(Interface): + """ A bridge is a way to connect two Ethernet segments together in a protocol independent way. Packets are forwarded based on Ethernet address, rather @@ -707,6 +701,7 @@ class BridgeIf(Interface): The Linux bridge code implements a subset of the ANSI/IEEE 802.1d standard. """ + def __init__(self, ifname): super().__init__(ifname, type='bridge') @@ -780,7 +775,6 @@ class BridgeIf(Interface): return (self._read_sysfs('/sys/class/net/{0}/bridge/hello_time' .format(self._ifname)) / 100) - @hello_time.setter def hello_time(self, time): """ @@ -864,7 +858,6 @@ class BridgeIf(Interface): return state - @stp_state.setter def stp_state(self, state): """ @@ -914,7 +907,6 @@ class BridgeIf(Interface): else: raise ValueError("Value out of range") - def add_port(self, interface): """ Add physical interface to bridge (member port) @@ -927,7 +919,6 @@ class BridgeIf(Interface): cmd = 'ip link set dev {} master {}'.format(interface, self._ifname) self._cmd(cmd) - def del_port(self, interface): """ Remove member port from bridge instance. @@ -939,7 +930,6 @@ class BridgeIf(Interface): cmd = 'ip link set dev {} nomaster'.format(interface) self._cmd(cmd) - def set_cost(self, interface, cost): """ Set interface path cost, only relevant for STP enabled interfaces @@ -952,7 +942,6 @@ class BridgeIf(Interface): return self._write_sysfs('/sys/class/net/{}/brif/{}/path_cost' .format(self._ifname, interface), cost) - def set_priority(self, interface, priority): """ Set interface path priority, only relevant for STP enabled interfaces @@ -966,8 +955,8 @@ class BridgeIf(Interface): .format(self._ifname, interface), priority) - class EthernetIf(Interface): + def __init__(self, ifname, type=None): super().__init__(ifname, type) @@ -990,10 +979,11 @@ class EthernetIf(Interface): if ethertype: self._ethertype = ethertype - ethertype='proto {}'.format(ethertype) + ethertype = 'proto {}'.format(ethertype) # create interface in the system - cmd = 'ip link add link {intf} name {intf}.{vlan} type vlan {proto} id {vlan}'.format(intf=self._ifname, vlan=self._vlan_id, proto=ethertype) + cmd = 'ip link add link {intf} name {intf}.{vlan} type vlan {proto} id {vlan}'.format( + intf=self._ifname, vlan=self._vlan_id, proto=ethertype) self._cmd(cmd) # return new object mapping to the newly created interface @@ -1001,7 +991,6 @@ class EthernetIf(Interface): # or interface description and so on return EthernetIf(vlan_ifname) - def del_vlan(self, vlan_id): """ Remove VLAN interface from operating system. Removing the interface @@ -1014,6 +1003,7 @@ class EthernetIf(Interface): class BondIf(EthernetIf): + """ The Linux bonding driver provides a method for aggregating multiple network interfaces into a single logical "bonded" interface. The behavior of the @@ -1021,6 +1011,7 @@ class BondIf(EthernetIf): either hot standby or load balancing services. Additionally, link integrity monitoring may be performed. """ + def __init__(self, ifname): super().__init__(ifname, type='bond') @@ -1184,7 +1175,7 @@ class BondIf(EthernetIf): ['eth1', 'eth2'] """ slaves = self._read_sysfs('/sys/class/net/{}/bonding/slaves' - .format(self._ifname)) + .format(self._ifname)) return list(map(str, slaves.split())) @property @@ -1267,14 +1258,17 @@ class BondIf(EthernetIf): >>> BondIf('bond0').mode '802.3ad' """ - if not mode in ['balance-rr', 'active-backup', 'balance-xor', 'broadcast', + if not mode in [ + 'balance-rr', 'active-backup', 'balance-xor', 'broadcast', '802.3ad', 'balance-tlb', 'balance-alb']: raise ValueError("Value out of range") return self._write_sysfs('/sys/class/net/{}/bonding/mode' .format(self._ifname), mode) + class WireGuardIf(Interface): + """ Wireguard interface class, contains a comnfig dictionary since wireguard VPN is being comnfigured via the wg command rather than @@ -1289,57 +1283,60 @@ class WireGuardIf(Interface): {'private-key': None, 'keepalive': 0, 'endpoint': None, 'port': 0, 'allowed-ips': [], 'pubkey': None, 'fwmark': 0, 'psk': '/dev/null'} >>> wg_intfc.wg_config['keepalive'] = 100 >>> print (wg_intfc.wg_config) - {'private-key': None, 'keepalive': 100, 'endpoint': None, 'port': 0, 'allowed-ips': [], 'pubkey': None, 'fwmark': 0, 'psk': '/dev/null'} + {'private-key': None, 'keepalive': 100, 'endpoint': None, 'port': 0, 'allowed-ips': [], 'pubkey': None, 'fwmark': 0, 'psk': '/dev/null'} """ + def __init__(self, ifname): - super().__init__(ifname, type='wireguard') - self.wg_config = { - 'port' : 0, - 'private-key' : None, - 'pubkey' : None, - 'psk' : '/dev/null', - 'allowed-ips' : [], - 'fwmark' : 0x00, - 'endpoint' : None, - 'keepalive' : 0 - } + super().__init__(ifname, type='wireguard') + self.wg_config = { + 'port': 0, + 'private-key': None, + 'pubkey': None, + 'psk': '/dev/null', + 'allowed-ips': [], + 'fwmark': 0x00, + 'endpoint': None, + 'keepalive': 0 + } def wg_update(self): - if not self.wg_config['private-key']: - raise ValueError("private key required") - else: - ### fmask permission check? - pass - - cmd = "wg set {} ".format(self._ifname) - cmd += "listen-port {} ".format(self.wg_config['port']) - cmd += "fwmark {} ".format(str(self.wg_config['fwmark'])) - cmd += "private-key {} ".format(self.wg_config['private-key']) - cmd += "peer {} ".format(self.wg_config['pubkey']) - cmd += " preshared-key {} ".format(self.wg_config['psk']) - cmd += " allowed-ips " - for aip in self.wg_config['allowed-ips']: - if aip != self.wg_config['allowed-ips'][-1]: - cmd += aip + "," + if not self.wg_config['private-key']: + raise ValueError("private key required") else: - cmd += aip - if self.wg_config['endpoint']: - cmd += " endpoint {}".format(self.wg_config['endpoint']) - cmd += " persistent-keepalive {}".format(self.wg_config['keepalive']) + # fmask permission check? + pass + + cmd = "wg set {} ".format(self._ifname) + cmd += "listen-port {} ".format(self.wg_config['port']) + cmd += "fwmark {} ".format(str(self.wg_config['fwmark'])) + cmd += "private-key {} ".format(self.wg_config['private-key']) + cmd += "peer {} ".format(self.wg_config['pubkey']) + cmd += " preshared-key {} ".format(self.wg_config['psk']) + cmd += " allowed-ips " + for aip in self.wg_config['allowed-ips']: + if aip != self.wg_config['allowed-ips'][-1]: + cmd += aip + "," + else: + cmd += aip + if self.wg_config['endpoint']: + cmd += " endpoint {}".format(self.wg_config['endpoint']) + cmd += " persistent-keepalive {}".format(self.wg_config['keepalive']) - self._cmd(cmd) + self._cmd(cmd) - ### remove psk since it isn't required anymore and is saved in the cli config only !! - if self.wg_config['psk'] != '/dev/null': - if os.path.exists(self.wg_config['psk']): - os.remove(self.wg_config['psk']) + # remove psk since it isn't required anymore and is saved in the cli + # config only !! + if self.wg_config['psk'] != '/dev/null': + if os.path.exists(self.wg_config['psk']): + os.remove(self.wg_config['psk']) """ Remove a peer of an interface, peers are identified by their public key. Giving it a readable name is a vyos feature, to remove a peer the pubkey and the interface is needed, to remove the entry. """ - def wg_remove_peer(self, peerkey): - cmd = "sudo wg set {0} peer {1} remove".format(self._ifname, str(peerkey)) - self._cmd(cmd) + def wg_remove_peer(self, peerkey): + cmd = "sudo wg set {0} peer {1} remove".format( + self._ifname, str(peerkey)) + self._cmd(cmd) -- cgit v1.2.3 From 4778f7d2f771b09df1705cea5c7c00e798a7f776 Mon Sep 17 00:00:00 2001 From: hagbard Date: Wed, 4 Sep 2019 14:37:15 -0700 Subject: [wireguard] - T1628: renaming member functions, removing wg_ prefix --- python/vyos/ifconfig.py | 34 +-- src/conf_mode/interface-wireguard.py | 427 ++++++++++++++++++----------------- 2 files changed, 240 insertions(+), 221 deletions(-) (limited to 'python/vyos') diff --git a/python/vyos/ifconfig.py b/python/vyos/ifconfig.py index 0bc4eff17..30bfa5735 100644 --- a/python/vyos/ifconfig.py +++ b/python/vyos/ifconfig.py @@ -1288,7 +1288,7 @@ class WireGuardIf(Interface): def __init__(self, ifname): super().__init__(ifname, type='wireguard') - self.wg_config = { + self.config = { 'port': 0, 'private-key': None, 'pubkey': None, @@ -1299,36 +1299,36 @@ class WireGuardIf(Interface): 'keepalive': 0 } - def wg_update(self): - if not self.wg_config['private-key']: + def update(self): + if not self.config['private-key']: raise ValueError("private key required") else: # fmask permission check? pass cmd = "wg set {} ".format(self._ifname) - cmd += "listen-port {} ".format(self.wg_config['port']) - cmd += "fwmark {} ".format(str(self.wg_config['fwmark'])) - cmd += "private-key {} ".format(self.wg_config['private-key']) - cmd += "peer {} ".format(self.wg_config['pubkey']) - cmd += " preshared-key {} ".format(self.wg_config['psk']) + cmd += "listen-port {} ".format(self.config['port']) + cmd += "fwmark {} ".format(str(self.config['fwmark'])) + cmd += "private-key {} ".format(self.config['private-key']) + cmd += "peer {} ".format(self.config['pubkey']) + cmd += " preshared-key {} ".format(self.config['psk']) cmd += " allowed-ips " - for aip in self.wg_config['allowed-ips']: - if aip != self.wg_config['allowed-ips'][-1]: + for aip in self.config['allowed-ips']: + if aip != self.config['allowed-ips'][-1]: cmd += aip + "," else: cmd += aip - if self.wg_config['endpoint']: - cmd += " endpoint {}".format(self.wg_config['endpoint']) - cmd += " persistent-keepalive {}".format(self.wg_config['keepalive']) + if self.config['endpoint']: + cmd += " endpoint {}".format(self.config['endpoint']) + cmd += " persistent-keepalive {}".format(self.config['keepalive']) self._cmd(cmd) # remove psk since it isn't required anymore and is saved in the cli # config only !! - if self.wg_config['psk'] != '/dev/null': - if os.path.exists(self.wg_config['psk']): - os.remove(self.wg_config['psk']) + if self.config['psk'] != '/dev/null': + if os.path.exists(self.config['psk']): + os.remove(self.config['psk']) """ Remove a peer of an interface, peers are identified by their public key. @@ -1336,7 +1336,7 @@ class WireGuardIf(Interface): and the interface is needed, to remove the entry. """ - def wg_remove_peer(self, peerkey): + def remove_peer(self, peerkey): cmd = "sudo wg set {0} peer {1} remove".format( self._ifname, str(peerkey)) self._cmd(cmd) diff --git a/src/conf_mode/interface-wireguard.py b/src/conf_mode/interface-wireguard.py index 40356da51..265491993 100755 --- a/src/conf_mode/interface-wireguard.py +++ b/src/conf_mode/interface-wireguard.py @@ -24,227 +24,246 @@ import subprocess from vyos.config import Config from vyos import ConfigError -from vyos.ifconfig import WireGuardIf as wg_if +from vyos.ifconfig import WireGuardIf ifname = str(os.environ['VYOS_TAGNODE_VALUE']) -wg_intfc = wg_if(ifname) +intfc = WireGuardIf(ifname) dir = r'/config/auth/wireguard' pk = dir + '/private.key' pub = dir + '/public.key' psk_file = dir + '/psk' + def check_kmod(): - if not os.path.exists('/sys/module/wireguard'): - sl.syslog(sl.LOG_NOTICE, "loading wirguard kmod") - if os.system('sudo modprobe wireguard') != 0: - sl.syslog(sl.LOG_NOTICE, "modprobe wireguard failed") - raise ConfigError("modprobe wireguard failed") + if not os.path.exists('/sys/module/wireguard'): + sl.syslog(sl.LOG_NOTICE, "loading wirguard kmod") + if os.system('sudo modprobe wireguard') != 0: + sl.syslog(sl.LOG_NOTICE, "modprobe wireguard failed") + raise ConfigError("modprobe wireguard failed") + def get_config(): - c = Config() - if not c.exists('interfaces wireguard'): - return None - - config_data = { - ifname : { - 'addr' : '', - 'descr' : ifname, - 'lport' : None, - 'status' : 'exists', - 'state' : 'enabled', - 'fwmark' : 0x00, - 'mtu' : 1420, - 'peer' : {} - } - } - - c.set_level('interfaces wireguard') - if not c.exists_effective(ifname): - config_data[ifname]['status'] = 'create' - - if not c.exists(ifname) and c.exists_effective(ifname): - config_data[ifname]['status'] = 'delete' - - if config_data[ifname]['status'] != 'delete': - if c.exists(ifname + ' address'): - config_data[ifname]['addr'] = c.return_values(ifname + ' address') - if c.exists(ifname + ' disable'): - config_data[ifname]['state'] = 'disable' - if c.exists(ifname + ' port'): - config_data[ifname]['lport'] = c.return_value(ifname + ' port') - if c.exists(ifname + ' fwmark'): - config_data[ifname]['fwmark'] = c.return_value(ifname + ' fwmark') - if c.exists(ifname + ' description'): - config_data[ifname]['descr'] = c.return_value(ifname + ' description') - if c.exists(ifname + ' mtu'): - config_data[ifname]['mtu'] = c.return_value(ifname + ' mtu') - if c.exists(ifname + ' peer'): - for p in c.list_nodes(ifname + ' peer'): - if not c.exists(ifname + ' peer ' + p + ' disable'): - config_data[ifname]['peer'].update( - { - p : { - 'allowed-ips' : [], - 'endpoint' : '', - 'pubkey' : '' - } - } - ) - if c.exists(ifname + ' peer ' + p + ' pubkey'): - config_data[ifname]['peer'][p]['pubkey'] = c.return_value(ifname + ' peer ' + p + ' pubkey') - if c.exists(ifname + ' peer ' + p + ' allowed-ips'): - config_data[ifname]['peer'][p]['allowed-ips'] = c.return_values(ifname + ' peer ' + p + ' allowed-ips') - if c.exists(ifname + ' peer ' + p + ' endpoint'): - config_data[ifname]['peer'][p]['endpoint'] = c.return_value(ifname + ' peer ' + p + ' endpoint') - if c.exists(ifname + ' peer ' + p + ' persistent-keepalive'): - config_data[ifname]['peer'][p]['persistent-keepalive'] = c.return_value(ifname + ' peer ' + p + ' persistent-keepalive') - if c.exists(ifname + ' peer ' + p + ' preshared-key'): - config_data[ifname]['peer'][p]['psk'] = c.return_value(ifname + ' peer ' + p + ' preshared-key') - - return config_data + c = Config() + if not c.exists('interfaces wireguard'): + return None + + config_data = { + ifname: { + 'addr': '', + 'descr': ifname, + 'lport': None, + 'status': 'exists', + 'state': 'enabled', + 'fwmark': 0x00, + 'mtu': 1420, + 'peer': {} + } + } + + c.set_level('interfaces wireguard') + if not c.exists_effective(ifname): + config_data[ifname]['status'] = 'create' + + if not c.exists(ifname) and c.exists_effective(ifname): + config_data[ifname]['status'] = 'delete' + + if config_data[ifname]['status'] != 'delete': + if c.exists(ifname + ' address'): + config_data[ifname]['addr'] = c.return_values(ifname + ' address') + if c.exists(ifname + ' disable'): + config_data[ifname]['state'] = 'disable' + if c.exists(ifname + ' port'): + config_data[ifname]['lport'] = c.return_value(ifname + ' port') + if c.exists(ifname + ' fwmark'): + config_data[ifname]['fwmark'] = c.return_value(ifname + ' fwmark') + if c.exists(ifname + ' description'): + config_data[ifname]['descr'] = c.return_value( + ifname + ' description') + if c.exists(ifname + ' mtu'): + config_data[ifname]['mtu'] = c.return_value(ifname + ' mtu') + if c.exists(ifname + ' peer'): + for p in c.list_nodes(ifname + ' peer'): + if not c.exists(ifname + ' peer ' + p + ' disable'): + config_data[ifname]['peer'].update( + { + p: { + 'allowed-ips': [], + 'endpoint': '', + 'pubkey': '' + } + } + ) + if c.exists(ifname + ' peer ' + p + ' pubkey'): + config_data[ifname]['peer'][p]['pubkey'] = c.return_value( + ifname + ' peer ' + p + ' pubkey') + if c.exists(ifname + ' peer ' + p + ' allowed-ips'): + config_data[ifname]['peer'][p]['allowed-ips'] = c.return_values( + ifname + ' peer ' + p + ' allowed-ips') + if c.exists(ifname + ' peer ' + p + ' endpoint'): + config_data[ifname]['peer'][p]['endpoint'] = c.return_value( + ifname + ' peer ' + p + ' endpoint') + if c.exists(ifname + ' peer ' + p + ' persistent-keepalive'): + config_data[ifname]['peer'][p]['persistent-keepalive'] = c.return_value( + ifname + ' peer ' + p + ' persistent-keepalive') + if c.exists(ifname + ' peer ' + p + ' preshared-key'): + config_data[ifname]['peer'][p]['psk'] = c.return_value( + ifname + ' peer ' + p + ' preshared-key') + + return config_data + def verify(c): - if not c: - return None + if not c: + return None - if not os.path.exists(pk): - raise ConfigError("No keys found, generate them by executing: \'run generate wireguard keypair\'") + if not os.path.exists(pk): + raise ConfigError( + "No keys found, generate them by executing: \'run generate wireguard keypair\'") + + if c[ifname]['status'] != 'delete': + if not c[ifname]['addr']: + raise ConfigError("ERROR: IP address required") + if not c[ifname]['peer']: + raise ConfigError("ERROR: peer required") + for p in c[ifname]['peer']: + if not c[ifname]['peer'][p]['allowed-ips']: + raise ConfigError("ERROR: allowed-ips required for peer " + p) + if not c[ifname]['peer'][p]['pubkey']: + raise ConfigError("peer pubkey required for peer " + p) - if c[ifname]['status'] != 'delete': - if not c[ifname]['addr']: - raise ConfigError("ERROR: IP address required") - if not c[ifname]['peer']: - raise ConfigError("ERROR: peer required") - for p in c[ifname]['peer']: - if not c[ifname]['peer'][p]['allowed-ips']: - raise ConfigError("ERROR: allowed-ips required for peer " + p) - if not c[ifname]['peer'][p]['pubkey']: - raise ConfigError("peer pubkey required for peer " + p) def apply(c): - ### no wg config left, delete all wireguard devices, if any - if not c: - net_devs = os.listdir('/sys/class/net/') - for dev in net_devs: - if os.path.isdir('/sys/class/net/' + dev): - buf = open('/sys/class/net/' + dev + '/uevent', 'r').read() - if re.search("DEVTYPE=wireguard", buf, re.I|re.M): - wg_intf = re.sub("INTERFACE=", "", re.search("INTERFACE=.*", buf, re.I|re.M).group(0)) - sl.syslog(sl.LOG_NOTICE, "removing interface " + wg_intf) - subprocess.call(['ip l d dev ' + wg_intf + ' >/dev/null'], shell=True) - return None - - ### interface removal - if c[ifname]['status'] == 'delete': - sl.syslog(sl.LOG_NOTICE, "removing interface " + ifname) - wg_intfc.remove() - return None - - c_eff = Config() - c_eff.set_level('interfaces wireguard') - - ## interface state - if c[ifname]['state'] == 'disable': - sl.syslog(sl.LOG_NOTICE, "disable interface " + ifname) - wg_intfc.state = 'down' - else: - if not wg_intfc.state == 'up': - sl.syslog(sl.LOG_NOTICE, "enable interface " + ifname) - wg_intfc.state = 'up' - - ## IP address - if not c_eff.exists_effective(ifname + ' address'): - for ip in c[ifname]['addr']: - wg_intfc.add_addr(ip) - else: - addr_eff = c_eff.return_effective_values(ifname + ' address') - addr_rem = list(set(addr_eff) - set(c[ifname]['addr'])) - addr_add = list(set(c[ifname]['addr']) - set(addr_eff)) - - if len(addr_rem) !=0: - for ip in addr_rem: - sl.syslog(sl.LOG_NOTICE, "remove IP address {0} from {1}".format(ip,ifname)) - wg_intfc.del_addr(ip) - - if len(addr_add) !=0: - for ip in addr_add: - sl.syslog(sl.LOG_NOTICE, "add IP address {0} to {1}".format(ip,ifname)) - wg_intfc.add_addr(ip) - - ## interface MTU - if c[ifname]['mtu'] != 1420: - wg_intfc.mtu = int(c[ifname]['mtu']) - else: - ## default is set to 1420 in config_data - wg_intfc.mtu = int(c[ifname]['mtu']) - - ## ifalias for snmp from description - descr_eff = c_eff.return_effective_value(ifname + ' description') - if descr_eff != c[ifname]['descr']: - wg_intfc.ifalias = str(c[ifname]['descr']) - - ## peer deletion - peer_eff = c_eff.list_effective_nodes(ifname + ' peer') - peer_cnf = [] - - try: + # no wg config left, delete all wireguard devices, if any + if not c: + net_devs = os.listdir('/sys/class/net/') + for dev in net_devs: + if os.path.isdir('/sys/class/net/' + dev): + buf = open('/sys/class/net/' + dev + '/uevent', 'r').read() + if re.search("DEVTYPE=wireguard", buf, re.I | re.M): + wg_intf = re.sub("INTERFACE=", "", re.search( + "INTERFACE=.*", buf, re.I | re.M).group(0)) + sl.syslog(sl.LOG_NOTICE, "removing interface " + wg_intf) + subprocess.call( + ['ip l d dev ' + wg_intf + ' >/dev/null'], shell=True) + return None + + # interface removal + if c[ifname]['status'] == 'delete': + sl.syslog(sl.LOG_NOTICE, "removing interface " + ifname) + intfc.remove() + return None + + c_eff = Config() + c_eff.set_level('interfaces wireguard') + + # interface state + if c[ifname]['state'] == 'disable': + sl.syslog(sl.LOG_NOTICE, "disable interface " + ifname) + intfc.state = 'down' + else: + if not intfc.state == 'up': + sl.syslog(sl.LOG_NOTICE, "enable interface " + ifname) + intfc.state = 'up' + + # IP address + if not c_eff.exists_effective(ifname + ' address'): + for ip in c[ifname]['addr']: + intfc.add_addr(ip) + else: + addr_eff = c_eff.return_effective_values(ifname + ' address') + addr_rem = list(set(addr_eff) - set(c[ifname]['addr'])) + addr_add = list(set(c[ifname]['addr']) - set(addr_eff)) + + if len(addr_rem) != 0: + for ip in addr_rem: + sl.syslog( + sl.LOG_NOTICE, "remove IP address {0} from {1}".format(ip, ifname)) + intfc.del_addr(ip) + + if len(addr_add) != 0: + for ip in addr_add: + sl.syslog( + sl.LOG_NOTICE, "add IP address {0} to {1}".format(ip, ifname)) + intfc.add_addr(ip) + + # interface MTU + if c[ifname]['mtu'] != 1420: + intfc.mtu = int(c[ifname]['mtu']) + else: + # default is set to 1420 in config_data + intfc.mtu = int(c[ifname]['mtu']) + + # ifalias for snmp from description + descr_eff = c_eff.return_effective_value(ifname + ' description') + if descr_eff != c[ifname]['descr']: + intfc.ifalias = str(c[ifname]['descr']) + + # peer deletion + peer_eff = c_eff.list_effective_nodes(ifname + ' peer') + peer_cnf = [] + + try: + for p in c[ifname]['peer']: + peer_cnf.append(p) + except KeyError: + pass + + peer_rem = list(set(peer_eff) - set(peer_cnf)) + for p in peer_rem: + pkey = c_eff.return_effective_value(ifname + ' peer ' + p + ' pubkey') + intfc.remove_peer(pkey) + + # peer key update + for p in peer_eff: + if p in peer_cnf: + ekey = c_eff.return_effective_value( + ifname + ' peer ' + p + ' pubkey') + nkey = c[ifname]['peer'][p]['pubkey'] + if nkey != ekey: + sl.syslog( + sl.LOG_NOTICE, "peer {0} pubkey changed from {1} to {2} on interface {3}".format(p, ekey, nkey, ifname)) + print ( + "peer {0} pubkey changed from {1} to {2} on interface {3}".format(p, ekey, nkey, ifname)) + intfc.remove_peer(ekey) + + intfc.config['private-key'] = pk for p in c[ifname]['peer']: - peer_cnf.append(p) - except KeyError: - pass - - peer_rem = list(set(peer_eff) - set(peer_cnf)) - for p in peer_rem: - pkey = c_eff.return_effective_value( ifname + ' peer ' + p +' pubkey') - wg_intfc.wg_remove_peer(pkey) - - ## peer key update - for p in peer_eff: - if p in peer_cnf: - ekey = c_eff.return_effective_value( ifname + ' peer ' + p +' pubkey') - nkey = c[ifname]['peer'][p]['pubkey'] - if nkey != ekey: - sl.syslog(sl.LOG_NOTICE, "peer {0} pubkey changed from {1} to {2} on interface {3}".format(p, ekey, nkey, ifname)) - print ("peer {0} pubkey changed from {1} to {2} on interface {3}".format(p, ekey, nkey, ifname)) - wg_intfc.wg_remove_peer(ekey) - - wg_intfc.wg_config['private-key'] = pk - for p in c[ifname]['peer']: - wg_intfc.wg_config['pubkey'] = str(c[ifname]['peer'][p]['pubkey']) - wg_intfc.wg_config['allowed-ips'] = (c[ifname]['peer'][p]['allowed-ips']) - - ## listen-port - if c[ifname]['lport']: - wg_intfc.wg_config['port'] = c[ifname]['lport'] - - ## fwmark - if c[ifname]['fwmark']: - wg_intfc.wg_config['fwmark'] = c[ifname]['fwmark'] - - ## endpoint - if c[ifname]['peer'][p]['endpoint']: - wg_intfc.wg_config['endpoint'] = c[ifname]['peer'][p]['endpoint'] - - ## persistent-keepalive - if 'persistent-keepalive' in c[ifname]['peer'][p]: - wg_intfc.wg_config['keepalive'] = c[ifname]['peer'][p]['persistent-keepalive'] - - ## preshared-key - needs to be read from a file - if 'psk' in c[ifname]['peer'][p]: - old_umask = os.umask(0o077) - open(psk_file, 'w').write(str(c[ifname]['peer'][p]['psk'])) - os.umask(old_umask) - wg_intfc.wg_config['psk'] = psk_file - - wg_intfc.wg_update() + intfc.config['pubkey'] = str(c[ifname]['peer'][p]['pubkey']) + intfc.config['allowed-ips'] = (c[ifname]['peer'][p]['allowed-ips']) + + # listen-port + if c[ifname]['lport']: + intfc.config['port'] = c[ifname]['lport'] + + # fwmark + if c[ifname]['fwmark']: + intfc.config['fwmark'] = c[ifname]['fwmark'] + + # endpoint + if c[ifname]['peer'][p]['endpoint']: + intfc.config['endpoint'] = c[ifname]['peer'][p]['endpoint'] + + # persistent-keepalive + if 'persistent-keepalive' in c[ifname]['peer'][p]: + intfc.config['keepalive'] = c[ifname][ + 'peer'][p]['persistent-keepalive'] + + # preshared-key - needs to be read from a file + if 'psk' in c[ifname]['peer'][p]: + old_umask = os.umask(0o077) + open(psk_file, 'w').write(str(c[ifname]['peer'][p]['psk'])) + os.umask(old_umask) + intfc.config['psk'] = psk_file + + intfc.update() if __name__ == '__main__': - try: - check_kmod() - c = get_config() - verify(c) - apply(c) - except ConfigError as e: - print(e) - sys.exit(1) + try: + check_kmod() + c = get_config() + verify(c) + apply(c) + except ConfigError as e: + print(e) + sys.exit(1) -- cgit v1.2.3 From 1bac4362d007135f91daffc4b0d51783658c10cf Mon Sep 17 00:00:00 2001 From: hagbard Date: Wed, 4 Sep 2019 14:54:42 -0700 Subject: [wireguard] - T1628: line break in coment added --- python/vyos/ifconfig.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) (limited to 'python/vyos') diff --git a/python/vyos/ifconfig.py b/python/vyos/ifconfig.py index 30bfa5735..084339408 100644 --- a/python/vyos/ifconfig.py +++ b/python/vyos/ifconfig.py @@ -1280,10 +1280,12 @@ class WireGuardIf(Interface): >>> from vyos.ifconfig import WireGuardIf as wg_if >>> wg_intfc = wg_if("wg01") >>> print (wg_intfc.wg_config) - {'private-key': None, 'keepalive': 0, 'endpoint': None, 'port': 0, 'allowed-ips': [], 'pubkey': None, 'fwmark': 0, 'psk': '/dev/null'} + {'private-key': None, 'keepalive': 0, 'endpoint': None, 'port': 0, + 'allowed-ips': [], 'pubkey': None, 'fwmark': 0, 'psk': '/dev/null'} >>> wg_intfc.wg_config['keepalive'] = 100 >>> print (wg_intfc.wg_config) - {'private-key': None, 'keepalive': 100, 'endpoint': None, 'port': 0, 'allowed-ips': [], 'pubkey': None, 'fwmark': 0, 'psk': '/dev/null'} + {'private-key': None, 'keepalive': 100, 'endpoint': None, 'port': 0, + 'allowed-ips': [], 'pubkey': None, 'fwmark': 0, 'psk': '/dev/null'} """ def __init__(self, ifname): -- cgit v1.2.3 From fca8166ba0837add065d3756d4e0919f5a159a4d Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Fri, 6 Sep 2019 10:06:00 +0200 Subject: Python/ifconfig: T1557: recursively delete VLAN interfaces on remove() --- python/vyos/ifconfig.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) (limited to 'python/vyos') diff --git a/python/vyos/ifconfig.py b/python/vyos/ifconfig.py index 084339408..c5d3e447b 100644 --- a/python/vyos/ifconfig.py +++ b/python/vyos/ifconfig.py @@ -14,6 +14,7 @@ # License along with this library. If not, see . import os +import re import subprocess import jinja2 @@ -97,6 +98,18 @@ class Interface: >>> i.remove() """ + # do we have sub interfaces (VLANs)? + # we apply a regex matching subinterfaces (indicated by a .) of a + # parent interface. 'bond0(?:\.\d+){1,2}' will match vif and vif-s/vif-c + # subinterfaces + vlan_ifs = [f for f in os.listdir(r'/sys/class/net') \ + if re.match(self._ifname + r'(?:\.\d+){1,2}', f)] + + for vlan in vlan_ifs: + Interface(vlan).remove() + + # All subinterfaces are now removed, continue on the physical interface + # stop DHCP(v6) if running self.del_dhcp() self.del_dhcpv6() -- cgit v1.2.3 From 35c7d66165da2102ee8986c3999fe9fea16c38da Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Fri, 6 Sep 2019 10:10:11 +0200 Subject: Python/ifconfig: T1557: {add,del}_addr() now supports dhcp/dhcpv6 Instead of manually starting DHCP/DHCPv6 for every interface and have an identical if/elif/else statement checking for dhcp/dhcpv6 rather move this repeating stement into add_addr()/del_addr(). Single source is always preferred. --- python/vyos/ifconfig.py | 111 ++++++++++++++++++++++++------------- src/conf_mode/interface-bonding.py | 56 ++++++++++--------- src/conf_mode/interface-bridge.py | 35 ++++-------- 3 files changed, 112 insertions(+), 90 deletions(-) (limited to 'python/vyos') diff --git a/python/vyos/ifconfig.py b/python/vyos/ifconfig.py index c5d3e447b..1886addfc 100644 --- a/python/vyos/ifconfig.py +++ b/python/vyos/ifconfig.py @@ -61,6 +61,7 @@ class Interface: >>> i = Interface('eth0') """ self._ifname = str(ifname) + self._state = 'down' if not os.path.exists('/sys/class/net/{}'.format(ifname)) and not type: raise Exception('interface "{}" not found'.format(self._ifname)) @@ -111,8 +112,8 @@ class Interface: # All subinterfaces are now removed, continue on the physical interface # stop DHCP(v6) if running - self.del_dhcp() - self.del_dhcpv6() + self._del_dhcp() + self._del_dhcpv6() # NOTE (Improvement): # after interface removal no other commands should be allowed @@ -359,6 +360,8 @@ class Interface: if state not in ['up', 'down']: raise ValueError('state must be "up" or "down"') + self._state = state + # Assemble command executed on system. Unfortunately there is no way # to up/down an interface via sysfs cmd = 'ip link set dev {} {}'.format(self._ifname, state) @@ -495,8 +498,14 @@ class Interface: def add_addr(self, addr): """ - Add IP address to interface. Address is only added if it yet not added - to that interface. + Add IP(v6) address to interface. Address is only added if it is not + already assigned to that interface. + + addr: can be an IPv4 address, IPv6 address, dhcp or dhcpv6! + IPv4: add IPv4 address to interface + IPv6: add IPv6 address to interface + dhcp: start dhclient (IPv4) on interface + dhcpv6: start dhclient (IPv6) on interface Example: >>> from vyos.ifconfig import Interface @@ -506,13 +515,25 @@ class Interface: >>> j.get_addr() ['192.0.2.1/24', '2001:db8::ffff/64'] """ - if not is_intf_addr_assigned(self._ifname, addr): - cmd = 'ip addr add "{}" dev "{}"'.format(addr, self._ifname) - self._cmd(cmd) + if addr == 'dhcp': + self._set_dhcp() + elif addr == 'dhcpv6': + self._set_dhcpv6() + else: + if not is_intf_addr_assigned(self._ifname, addr): + cmd = 'ip addr add "{}" dev "{}"'.format(addr, self._ifname) + self._cmd(cmd) def del_addr(self, addr): """ - Remove IP address from interface. + Delete IP(v6) address to interface. Address is only added if it is + assigned to that interface. + + addr: can be an IPv4 address, IPv6 address, dhcp or dhcpv6! + IPv4: delete IPv4 address from interface + IPv6: delete IPv6 address from interface + dhcp: stop dhclient (IPv4) on interface + dhcpv6: stop dhclient (IPv6) on interface Example: >>> from vyos.ifconfig import Interface @@ -525,12 +546,17 @@ class Interface: >>> j.get_addr() ['2001:db8::ffff/64'] """ - if is_intf_addr_assigned(self._ifname, addr): - cmd = 'ip addr del "{}" dev "{}"'.format(addr, self._ifname) - self._cmd(cmd) + if addr == 'dhcp': + self._del_dhcp() + elif addr == 'dhcpv6': + self._del_dhcpv6() + else: + if is_intf_addr_assigned(self._ifname, addr): + cmd = 'ip addr del "{}" dev "{}"'.format(addr, self._ifname) + self._cmd(cmd) # replace dhcpv4/v6 with systemd.networkd? - def set_dhcp(self): + def _set_dhcp(self): """ Configure interface as DHCP client. The dhclient binary is automatically started in background! @@ -557,15 +583,17 @@ class Interface: with open(self._dhcp_cfg_file, 'w') as f: f.write(dhcp_text) - cmd = 'start-stop-daemon --start --quiet --pidfile ' + \ - self._dhcp_pid_file - cmd += ' --exec /sbin/dhclient --' - # now pass arguments to dhclient binary - cmd += ' -4 -nw -cf {} -pf {} -lf {} {}'.format( - self._dhcp_cfg_file, self._dhcp_pid_file, self._dhcp_lease_file, self._ifname) - self._cmd(cmd) + if self._state == 'up': + cmd = 'start-stop-daemon --start --quiet --pidfile ' + \ + self._dhcp_pid_file + cmd += ' --exec /sbin/dhclient --' + # now pass arguments to dhclient binary + cmd += ' -4 -nw -cf {} -pf {} -lf {} {}'.format( + self._dhcp_cfg_file, self._dhcp_pid_file, self._dhcp_lease_file, self._ifname) + self._cmd(cmd) - def del_dhcp(self): + + def _del_dhcp(self): """ De-configure interface as DHCP clinet. All auto generated files like pid, config and lease will be removed. @@ -601,7 +629,8 @@ class Interface: if os.path.isfile(self._dhcp_lease_file): os.remove(self._dhcp_lease_file) - def set_dhcpv6(self): + + def _set_dhcpv6(self): """ Configure interface as DHCPv6 client. The dhclient binary is automatically started in background! @@ -622,25 +651,28 @@ class Interface: with open(self._dhcpv6_cfg_file, 'w') as f: f.write(dhcpv6_text) - # https://bugs.launchpad.net/ubuntu/+source/ifupdown/+bug/1447715 - # - # wee need to wait for IPv6 DAD to finish once and interface is added - # this suxx :-( - sleep(5) + if self._state == 'up': + # https://bugs.launchpad.net/ubuntu/+source/ifupdown/+bug/1447715 + # + # wee need to wait for IPv6 DAD to finish once and interface is added + # this suxx :-( + sleep(5) - # no longer accept router announcements on this interface - cmd = 'sysctl -q -w net.ipv6.conf.{}.accept_ra=0'.format(self._ifname) - self._cmd(cmd) + # no longer accept router announcements on this interface + cmd = 'sysctl -q -w net.ipv6.conf.{}.accept_ra=0'.format(self._ifname) + self._cmd(cmd) - cmd = 'start-stop-daemon --start --quiet --pidfile ' + \ - self._dhcpv6_pid_file - cmd += ' --exec /sbin/dhclient --' - # now pass arguments to dhclient binary - cmd += ' -6 -nw -cf {} -pf {} -lf {} {}'.format( - self._dhcpv6_cfg_file, self._dhcpv6_pid_file, self._dhcpv6_lease_file, self._ifname) - self._cmd(cmd) + # assemble command-line to start DHCPv6 client (dhclient) + cmd = 'start-stop-daemon --start --quiet --pidfile ' + \ + self._dhcpv6_pid_file + cmd += ' --exec /sbin/dhclient --' + # now pass arguments to dhclient binary + cmd += ' -6 -nw -cf {} -pf {} -lf {} {}'.format( + self._dhcpv6_cfg_file, self._dhcpv6_pid_file, self._dhcpv6_lease_file, self._ifname) + self._cmd(cmd) - def del_dhcpv6(self): + + def _del_dhcpv6(self): """ De-configure interface as DHCPv6 clinet. All auto generated files like pid, config and lease will be removed. @@ -1281,7 +1313,6 @@ class BondIf(EthernetIf): class WireGuardIf(Interface): - """ Wireguard interface class, contains a comnfig dictionary since wireguard VPN is being comnfigured via the wg command rather than @@ -1293,11 +1324,11 @@ class WireGuardIf(Interface): >>> from vyos.ifconfig import WireGuardIf as wg_if >>> wg_intfc = wg_if("wg01") >>> print (wg_intfc.wg_config) - {'private-key': None, 'keepalive': 0, 'endpoint': None, 'port': 0, + {'private-key': None, 'keepalive': 0, 'endpoint': None, 'port': 0, 'allowed-ips': [], 'pubkey': None, 'fwmark': 0, 'psk': '/dev/null'} >>> wg_intfc.wg_config['keepalive'] = 100 >>> print (wg_intfc.wg_config) - {'private-key': None, 'keepalive': 100, 'endpoint': None, 'port': 0, + {'private-key': None, 'keepalive': 100, 'endpoint': None, 'port': 0, 'allowed-ips': [], 'pubkey': None, 'fwmark': 0, 'psk': '/dev/null'} """ diff --git a/src/conf_mode/interface-bonding.py b/src/conf_mode/interface-bonding.py index 2ec764965..81667289d 100755 --- a/src/conf_mode/interface-bonding.py +++ b/src/conf_mode/interface-bonding.py @@ -85,12 +85,6 @@ def apply_vlan_config(vlan, config): if type(vlan) != type(EthernetIf("lo")): raise TypeError() - # Configure interface address(es) - for addr in config['address_remove']: - vlan.del_addr(addr) - for addr in config['address']: - vlan.add_addr(addr) - # update interface description used e.g. within SNMP vlan.ifalias = config['description'] # ignore link state changes @@ -107,6 +101,14 @@ def apply_vlan_config(vlan, config): else: vlan.state = 'up' + # Configure interface address(es) + # - not longer required addresses get removed first + # - newly addresses will be added second + for addr in config['address_remove']: + vlan.del_addr(addr) + for addr in config['address']: + vlan.add_addr(addr) + def get_config(): # initialize kernel module if not loaded @@ -139,6 +141,11 @@ def get_config(): if conf.exists('address'): bond['address'] = conf.return_values('address') + # get interface addresses (currently effective) - to determine which + # address is no longer valid and needs to be removed from the bond + eff_addr = conf.return_effective_values('address') + bond['address_remove'] = list_diff(eff_addr, bond['address']) + # ARP link monitoring frequency in milliseconds if conf.exists('arp-monitor interval'): bond['arp_mon_intvl'] = int(conf.return_value('arp-monitor interval')) @@ -209,12 +216,6 @@ def get_config(): if conf.exists('member interface'): bond['member'] = conf.return_values('member interface') - # get interface addresses (currently effective) - to determine which - # address is no longer valid and needs to be removed from the bond - eff_addr = conf.return_effective_values('address') - act_addr = conf.return_values('address') - bond['address_remove'] = list_diff(eff_addr, act_addr) - # Primary device interface if conf.exists('primary'): bond['primary'] = conf.return_value('primary') @@ -314,6 +315,7 @@ def apply(bond): b = BondIf(bond['intf']) if bond['deleted']: + # # delete bonding interface b.remove() else: @@ -326,12 +328,6 @@ def apply(bond): for intf in b.get_slaves(): b.del_port(intf) - # Configure interface address(es) - for addr in bond['address_remove']: - b.del_addr(addr) - for addr in bond['address']: - b.add_addr(addr) - # ARP link monitoring frequency b.arp_interval = bond['arp_mon_intvl'] # reset miimon on arp-montior deletion @@ -348,8 +344,8 @@ def apply(bond): # from the kernel side this looks valid to me. We won't run into an error # when a user added manual adresses which would result in having more # then 16 adresses in total. - cur_addr = list(map(str, b.arp_ip_target.split())) - for addr in cur_addr: + arp_tgt_addr = list(map(str, b.arp_ip_target.split())) + for addr in arp_tgt_addr: b.arp_ip_target = '-' + addr # Add configured ARP target addresses @@ -391,6 +387,20 @@ def apply(bond): for intf in bond['member']: b.add_port(intf) + # As the bond interface is always disabled first when changing + # parameters we will only re-enable the interface if it is not + # administratively disabled + if not bond['disable']: + b.state = 'up' + + # Configure interface address(es) + # - not longer required addresses get removed first + # - newly addresses will be added second + for addr in bond['address_remove']: + b.del_addr(addr) + for addr in bond['address']: + b.add_addr(addr) + # remove no longer required service VLAN interfaces (vif-s) for vif_s in bond['vif_s_remove']: b.del_vlan(vif_s) @@ -420,12 +430,6 @@ def apply(bond): vlan = b.add_vlan(vif['id']) apply_vlan_config(vlan, vif) - # As the bond interface is always disabled first when changing - # parameters we will only re-enable the interface if it is not - # administratively disabled - if not bond['disable']: - b.state = 'up' - return None if __name__ == '__main__': diff --git a/src/conf_mode/interface-bridge.py b/src/conf_mode/interface-bridge.py index b165428ee..8996fceec 100755 --- a/src/conf_mode/interface-bridge.py +++ b/src/conf_mode/interface-bridge.py @@ -61,9 +61,7 @@ def get_config(): # Check if bridge has been removed if not conf.exists('interfaces bridge ' + bridge['intf']): bridge['deleted'] = True - # we should not bail out early here b/c we should - # find possible DHCP interfaces later on. - # DHCP interfaces invoke dhclient which should be stopped, too + return bridge # set new configuration level conf.set_level('interfaces bridge ' + bridge['intf']) @@ -72,6 +70,11 @@ def get_config(): if conf.exists('address'): bridge['address'] = conf.return_values('address') + # Determine interface addresses (currently effective) - to determine which + # address is no longer valid and needs to be removed from the bridge + eff_addr = conf.return_effective_values('address') + bridge['address_remove'] = list_diff(eff_addr, bridge['address']) + # retrieve aging - how long addresses are retained if conf.exists('aging'): bridge['aging'] = int(conf.return_value('aging')) @@ -137,12 +140,6 @@ def get_config(): act_intf = conf.list_nodes('member interface') bridge['member_remove'] = list_diff(eff_intf, act_intf) - # Determine interface addresses (currently effective) - to determine which - # address is no longer valid and needs to be removed from the bridge - eff_addr = conf.return_effective_values('address') - act_addr = conf.return_values('address') - bridge['address_remove'] = list_diff(eff_addr, act_addr) - # Priority for this bridge if conf.exists('priority'): bridge['priority'] = int(conf.return_value('priority')) @@ -225,23 +222,13 @@ def apply(bridge): if bridge['disable']: br.state = 'down' - # remove configured network interface addresses/DHCP(v6) configuration + # Configure interface address(es) + # - not longer required addresses get removed first + # - newly addresses will be added second for addr in bridge['address_remove']: - if addr == 'dhcp': - br.del_dhcp() - elif addr == 'dhcpv6': - br.del_dhcpv6() - else: - br.del_addr(addr) - - # add configured network interface addresses/DHCP(v6) configuration + br.del_addr(addr) for addr in bridge['address']: - if addr == 'dhcp': - br.set_dhcp() - elif addr == 'dhcpv6': - br.set_dhcpv6() - else: - br.add_addr(addr) + br.add_addr(addr) # configure additional bridge member options for member in bridge['member']: -- cgit v1.2.3 From b4eaa216e7dc44d950bc68adad330779dd1f71f6 Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Fri, 6 Sep 2019 10:27:27 +0200 Subject: Python/ifconfig: T1557: fix remove_peer commend in WireGuardIf --- python/vyos/ifconfig.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) (limited to 'python/vyos') diff --git a/python/vyos/ifconfig.py b/python/vyos/ifconfig.py index 1886addfc..7593f2c91 100644 --- a/python/vyos/ifconfig.py +++ b/python/vyos/ifconfig.py @@ -1376,13 +1376,13 @@ class WireGuardIf(Interface): if os.path.exists(self.config['psk']): os.remove(self.config['psk']) - """ - Remove a peer of an interface, peers are identified by their public key. - Giving it a readable name is a vyos feature, to remove a peer the pubkey - and the interface is needed, to remove the entry. - """ def remove_peer(self, peerkey): + """ + Remove a peer of an interface, peers are identified by their public key. + Giving it a readable name is a vyos feature, to remove a peer the pubkey + and the interface is needed, to remove the entry. + """ cmd = "sudo wg set {0} peer {1} remove".format( self._ifname, str(peerkey)) self._cmd(cmd) -- cgit v1.2.3 From dcde45826501302fd5fc2fbfcc1c376c2d51ea3a Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Thu, 5 Sep 2019 19:35:22 +0200 Subject: Python/ifconfig: T1557: vxlan: initial support via VXLANIf --- interface-definitions/interfaces-vxlan.xml | 102 +++++++++++++++++++++++++++++ python/vyos/ifconfig.py | 56 +++++++++++++++- 2 files changed, 155 insertions(+), 3 deletions(-) create mode 100644 interface-definitions/interfaces-vxlan.xml (limited to 'python/vyos') diff --git a/interface-definitions/interfaces-vxlan.xml b/interface-definitions/interfaces-vxlan.xml new file mode 100644 index 000000000..35a43f92c --- /dev/null +++ b/interface-definitions/interfaces-vxlan.xml @@ -0,0 +1,102 @@ + + + + + + + Virtual extensible LAN interface (VXLAN) + 460 + + vxlan[0-9]+$ + + VXLAN interface must be named vxlanN + + vxlanN + VXLAN interface name + + + + + + IP address + + ipv4net + IPv4 address and prefix length + + + ipv6net + IPv6 address and prefix length + + + + + + + + + + Interface description + + ^.{1,256}$ + + Interface description too long (limit 256 characters) + + + + + Disable interface + + + + + + Multicast group address for VXLAN interface + + ipv4 + Multicast group address + + + + + + + + + + + ARP cache entry timeout in seconds + + 1-86400 + ARP cache entry timout in seconds (default 30) + + + + + ARP cache entry timeout must be between 1 and 86400 seconds + + + + + Enable proxy-arp on this interface + + + + + + + + Underlay device of VXLAN interface + + interface + Interface used for VXLAN underlay + + + + + + + + + + + diff --git a/python/vyos/ifconfig.py b/python/vyos/ifconfig.py index 7593f2c91..bc22478a6 100644 --- a/python/vyos/ifconfig.py +++ b/python/vyos/ifconfig.py @@ -66,9 +66,6 @@ class Interface: if not os.path.exists('/sys/class/net/{}'.format(ifname)) and not type: raise Exception('interface "{}" not found'.format(self._ifname)) - if os.path.isfile('/tmp/vyos.ifconfig.debug'): - self._debug = True - if not os.path.exists('/sys/class/net/{}'.format(self._ifname)): cmd = 'ip link add dev {} type {}'.format(self._ifname, type) self._cmd(cmd) @@ -1386,3 +1383,56 @@ class WireGuardIf(Interface): cmd = "sudo wg set {0} peer {1} remove".format( self._ifname, str(peerkey)) self._cmd(cmd) + + +class VXLANIf(Interface, ): + """ + The VXLAN protocol is a tunnelling protocol designed to solve the + problem of limited VLAN IDs (4096) in IEEE 802.1q. With VXLAN the + size of the identifier is expanded to 24 bits (16777216). + + VXLAN is described by IETF RFC 7348, and has been implemented by a + number of vendors. The protocol runs over UDP using a single + destination port. This document describes the Linux kernel tunnel + device, there is also a separate implementation of VXLAN for + Openvswitch. + + Unlike most tunnels, a VXLAN is a 1 to N network, not just point to + point. A VXLAN device can learn the IP address of the other endpoint + either dynamically in a manner similar to a learning bridge, or make + use of statically-configured forwarding entries. + + For more information please refer to: + https://www.kernel.org/doc/Documentation/networking/vxlan.txt + """ + def __init__(self, ifname, config=''): + if config: + if not os.path.exists('/sys/class/net/{}'.format(self._ifname)): + # we assume that by default a multicast interface is created + group = 'group {}'.format(config['group']) + # if remote host is specified we ignore the multicast address + if config['remote']: + group = 'remote {}'.format(config['remote']) + # an underlay device is not always specified + dev = '' + if config['dev']: + dev = 'dev'.format(config['dev']) + + cmd = 'ip link add dev {intf} type vxlan id {vni} {group} {dev} {port}' + .format(intf=self._ifname, config['vni'], group=group, dev=dev, port=config['port']) + self._cmd(cmd) + + super().__init__(ifname, type='vxlan') + + + @staticmethod + def get_config(): + config = { + 'vni': 0, + 'dev': '', + 'group': '', + 'port': 8472 # The Linux implementation of VXLAN pre-dates + # the IANA's selection of a standard destination port + 'remote': '', + 'ttl': 16 + } -- cgit v1.2.3 From 98aafc8f704ef54b6ece514c038b6aea414df734 Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Thu, 5 Sep 2019 19:35:43 +0200 Subject: vxlan: T1636: initial rewrite with XML and Python Tested using: Site 1 (VyOS 1.2.2) ------------------- set interfaces vxlan vxlan100 address '10.10.10.2/24' set interfaces vxlan vxlan100 remote '172.18.201.10' set interfaces vxlan vxlan100 vni '100' Site 2 (rewrite) ---------------- set interfaces vxlan vxlan100 address '10.10.10.1/24' set interfaces vxlan vxlan100 description 'VyOS VXLAN' set interfaces vxlan vxlan100 remote '172.18.202.10' set interfaces vxlan vxlan100 vni '100' --- Makefile | 1 + interface-definitions/interfaces-vxlan.xml | 49 +++++++ python/vyos/ifconfig.py | 37 +++-- src/conf_mode/interface-vxlan.py | 208 +++++++++++++++++++++++++++++ 4 files changed, 282 insertions(+), 13 deletions(-) create mode 100755 src/conf_mode/interface-vxlan.py (limited to 'python/vyos') diff --git a/Makefile b/Makefile index 03b4712fc..d7b3f047d 100644 --- a/Makefile +++ b/Makefile @@ -13,6 +13,7 @@ interface_definitions: rm -f $(TMPL_DIR)/interfaces/node.def rm -f $(TMPL_DIR)/interfaces/bridge/node.tag/ip/node.def rm -f $(TMPL_DIR)/interfaces/bonding/node.tag/ip/node.def + rm -f $(TMPL_DIR)/interfaces/vxlan/node.tag/ip/node.def rm -f $(TMPL_DIR)/protocols/node.def rm -f $(TMPL_DIR)/protocols/static/node.def rm -f $(TMPL_DIR)/system/node.def diff --git a/interface-definitions/interfaces-vxlan.xml b/interface-definitions/interfaces-vxlan.xml index 35a43f92c..b06c2860c 100644 --- a/interface-definitions/interfaces-vxlan.xml +++ b/interface-definitions/interfaces-vxlan.xml @@ -95,6 +95,55 @@ + + + Maximum Transmission Unit (MTU) + + 1450-9000 + Maximum Transmission Unit + + + + + MTU must be between 1450 and 9000 + + + + + Remote address of VXLAN tunnel + + ipv4 + Remote address of VXLAN tunnel + + + + + + + + + Destination port of VXLAN tunnel (default: 8472) + + 1-65535 + Numeric IP port + + + + + + + + + Virtual Network Identifier + + 0-16777214 + VXLAN virtual network identifier + + + + + + diff --git a/python/vyos/ifconfig.py b/python/vyos/ifconfig.py index bc22478a6..0479e3672 100644 --- a/python/vyos/ifconfig.py +++ b/python/vyos/ifconfig.py @@ -1407,32 +1407,43 @@ class VXLANIf(Interface, ): """ def __init__(self, ifname, config=''): if config: + self._ifname = ifname + if not os.path.exists('/sys/class/net/{}'.format(self._ifname)): # we assume that by default a multicast interface is created group = 'group {}'.format(config['group']) + # if remote host is specified we ignore the multicast address if config['remote']: group = 'remote {}'.format(config['remote']) + # an underlay device is not always specified dev = '' if config['dev']: - dev = 'dev'.format(config['dev']) + dev = 'dev {}'.format(config['dev']) - cmd = 'ip link add dev {intf} type vxlan id {vni} {group} {dev} {port}' - .format(intf=self._ifname, config['vni'], group=group, dev=dev, port=config['port']) + cmd = 'ip link add {intf} type vxlan id {vni} {grp_rem} {dev} dstport {port}' \ + .format(intf=self._ifname, vni=config['vni'], grp_rem=group, dev=dev, port=config['port']) self._cmd(cmd) super().__init__(ifname, type='vxlan') + @staticmethod + def get_config(): + """ + VXLAN interfaces require a configuration when they are added using + iproute2. This static method will provide the configuration dictionary + used by this class. - @staticmethod - def get_config(): - config = { - 'vni': 0, - 'dev': '', - 'group': '', - 'port': 8472 # The Linux implementation of VXLAN pre-dates + Example: + >> dict = VXLANIf().get_config() + """ + config = { + 'vni': 0, + 'dev': '', + 'group': '', + 'port': 8472, # The Linux implementation of VXLAN pre-dates # the IANA's selection of a standard destination port - 'remote': '', - 'ttl': 16 - } + 'remote': '' + } + return config diff --git a/src/conf_mode/interface-vxlan.py b/src/conf_mode/interface-vxlan.py new file mode 100755 index 000000000..59022238e --- /dev/null +++ b/src/conf_mode/interface-vxlan.py @@ -0,0 +1,208 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2019 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# + +from os import environ +from sys import exit +from copy import deepcopy + +from vyos.configdict import list_diff +from vyos.config import Config +from vyos.ifconfig import VXLANIf, Interface +from vyos.interfaces import get_type_of_interface +from vyos import ConfigError +from netifaces import interfaces + +default_config_data = { + 'address': [], + 'address_remove': [], + 'deleted': False, + 'description': '', + 'disable': False, + 'group': '', + 'intf': '', + 'ip_arp_cache_tmo': 30, + 'ip_proxy_arp': 0, + 'link': '', + 'mtu': 1450, + 'remote': '', + 'remote_port': 8472 # The Linux implementation of VXLAN pre-dates + # the IANA's selection of a standard destination port +} + + +def get_config(): + vxlan = deepcopy(default_config_data) + conf = Config() + + # determine tagNode instance + try: + vxlan['intf'] = environ['VYOS_TAGNODE_VALUE'] + except KeyError as E: + print("Interface not specified") + + # Check if interface has been removed + if not conf.exists('interfaces vxlan ' + vxlan['intf']): + vxlan['deleted'] = True + return vxlan + + # set new configuration level + conf.set_level('interfaces vxlan ' + vxlan['intf']) + + # retrieve configured interface addresses + if conf.exists('address'): + vxlan['address'] = conf.return_values('address') + + # Determine interface addresses (currently effective) - to determine which + # address is no longer valid and needs to be removed from the interface + eff_addr = conf.return_effective_values('address') + act_addr = conf.return_values('address') + vxlan['address_remove'] = list_diff(eff_addr, act_addr) + + # retrieve interface description + if conf.exists('description'): + vxlan['description'] = conf.return_value('description') + + # Disable this interface + if conf.exists('disable'): + vxlan['disable'] = True + + # VXLAN multicast grou + if conf.exists('group'): + vxlan['group'] = conf.return_value('group') + + # ARP cache entry timeout in seconds + if conf.exists('ip arp-cache-timeout'): + vxlan['ip_arp_cache_tmo'] = int(conf.return_value('ip arp-cache-timeout')) + + # Enable proxy-arp on this interface + if conf.exists('ip enable-proxy-arp'): + vxlan['ip_proxy_arp'] = 1 + + # VXLAN underlay interface + if conf.exists('link'): + vxlan['link'] = conf.return_value('link') + + # Maximum Transmission Unit (MTU) + if conf.exists('mtu'): + vxlan['mtu'] = int(conf.return_value('mtu')) + + # Remote address of VXLAN tunnel + if conf.exists('remote'): + vxlan['remote'] = conf.return_value('remote') + + # Remote port of VXLAN tunnel + if conf.exists('port'): + vxlan['remote_port'] = int(conf.return_value('port')) + + # Virtual Network Identifier + if conf.exists('vni'): + vxlan['vni'] = conf.return_value('vni') + + return vxlan + + +def verify(vxlan): + if vxlan['deleted']: + # bail out early + return None + + if vxlan['mtu'] < 1500: + print('WARNING: RFC7348 recommends VXLAN tunnels preserve a 1500 byte MTU') + + if vxlan['group'] and not vxlan['link']: + raise ConfigError('Multicast VXLAN requires an underlaying interface ') + + if not (vxlan['group'] or vxlan['remote']): + raise ConfigError('Group or remote must be configured') + + if not vxlan['vni']: + raise ConfigError('Must configure VNI for VXLAN') + + if vxlan['link']: + # VXLAN adds a 50 byte overhead - we need to check the underlaying MTU + # if our configured MTU is at least 50 bytes less + underlay_mtu = int(Interface(vxlan['link']).mtu) + if underlay_mtu < (vxlan['mtu'] + 50): + raise ConfigError('VXLAN has a 50 byte overhead, underlaying device ' \ + 'MTU is to small ({})'.format(underlay_mtu)) + + return None + + +def generate(vxlan): + return None + + +def apply(vxlan): + # Check if the VXLAN interface already exists + if vxlan['intf'] in interfaces(): + v = VXLANIf(vxlan['intf']) + # VXLAN is super picky and the tunnel always needs to be recreated, + # thus we can simply always delete it first. + v.remove() + + if not vxlan['deleted']: + # VXLAN interface needs to be created on-block + # instead of passing a ton of arguments, I just use a dict + # that is managed by vyos.ifconfig + conf = deepcopy(VXLANIf.get_config()) + + # Assign VXLAN instance configuration parameters to config dict + conf['vni'] = vxlan['vni'] + conf['group'] = vxlan['group'] + conf['dev'] = vxlan['link'] + conf['remote'] = vxlan['remote'] + conf['port'] = vxlan['remote_port'] + + # Finally create the new interface + v = VXLANIf(vxlan['intf'], config=conf) + # update interface description used e.g. by SNMP + v.ifalias = vxlan['description'] + # Maximum Transfer Unit (MTU) + v.mtu = vxlan['mtu'] + + # configure ARP cache timeout in milliseconds + v.arp_cache_tmp = vxlan['ip_arp_cache_tmo'] + # Enable proxy-arp on this interface + v.proxy_arp = vxlan['ip_proxy_arp'] + + # Configure interface address(es) + # - not longer required addresses get removed first + # - newly addresses will be added second + for addr in vxlan['address_remove']: + v.del_addr(addr) + for addr in vxlan['address']: + v.add_addr(addr) + + # As the bond interface is always disabled first when changing + # parameters we will only re-enable the interface if it is not + # administratively disabled + if not vxlan['disable']: + v.state='up' + + return None + + +if __name__ == '__main__': + try: + c = get_config() + verify(c) + generate(c) + apply(c) + except ConfigError as e: + print(e) + exit(1) -- cgit v1.2.3 From 189ae4f7096abf7ca7100a4a31e038ce9e3e19c2 Mon Sep 17 00:00:00 2001 From: hagbard Date: Fri, 6 Sep 2019 14:46:43 -0700 Subject: [wireguard] - T1639: wireguard pubkey change error - sudo added to wg call - debug print removed when pubkey changes --- python/vyos/ifconfig.py | 2 +- src/conf_mode/interface-wireguard.py | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) (limited to 'python/vyos') diff --git a/python/vyos/ifconfig.py b/python/vyos/ifconfig.py index 0479e3672..5b1c11a47 100644 --- a/python/vyos/ifconfig.py +++ b/python/vyos/ifconfig.py @@ -1349,7 +1349,7 @@ class WireGuardIf(Interface): # fmask permission check? pass - cmd = "wg set {} ".format(self._ifname) + cmd = "sudo wg set {} ".format(self._ifname) cmd += "listen-port {} ".format(self.config['port']) cmd += "fwmark {} ".format(str(self.config['fwmark'])) cmd += "private-key {} ".format(self.config['private-key']) diff --git a/src/conf_mode/interface-wireguard.py b/src/conf_mode/interface-wireguard.py index 265491993..e7b9a267f 100755 --- a/src/conf_mode/interface-wireguard.py +++ b/src/conf_mode/interface-wireguard.py @@ -223,8 +223,6 @@ def apply(c): if nkey != ekey: sl.syslog( sl.LOG_NOTICE, "peer {0} pubkey changed from {1} to {2} on interface {3}".format(p, ekey, nkey, ifname)) - print ( - "peer {0} pubkey changed from {1} to {2} on interface {3}".format(p, ekey, nkey, ifname)) intfc.remove_peer(ekey) intfc.config['private-key'] = pk -- cgit v1.2.3 From f7456361b5b94f3c69f8fa0f34f8bff0ef68f9aa Mon Sep 17 00:00:00 2001 From: hagbard Date: Mon, 9 Sep 2019 09:32:57 -0700 Subject: [wireguard] - T1639: wireguard pubkey change error - removed sudo as is already runs as root - set privte key as variable in preparation to support multiple pk's --- python/vyos/ifconfig.py | 4 ++-- src/conf_mode/interface-wireguard.py | 15 +++++---------- 2 files changed, 7 insertions(+), 12 deletions(-) (limited to 'python/vyos') diff --git a/python/vyos/ifconfig.py b/python/vyos/ifconfig.py index 5b1c11a47..62bf94d79 100644 --- a/python/vyos/ifconfig.py +++ b/python/vyos/ifconfig.py @@ -1349,7 +1349,7 @@ class WireGuardIf(Interface): # fmask permission check? pass - cmd = "sudo wg set {} ".format(self._ifname) + cmd = "wg set {} ".format(self._ifname) cmd += "listen-port {} ".format(self.config['port']) cmd += "fwmark {} ".format(str(self.config['fwmark'])) cmd += "private-key {} ".format(self.config['private-key']) @@ -1380,7 +1380,7 @@ class WireGuardIf(Interface): Giving it a readable name is a vyos feature, to remove a peer the pubkey and the interface is needed, to remove the entry. """ - cmd = "sudo wg set {0} peer {1} remove".format( + cmd = "wg set {0} peer {1} remove".format( self._ifname, str(peerkey)) self._cmd(cmd) diff --git a/src/conf_mode/interface-wireguard.py b/src/conf_mode/interface-wireguard.py index e7b9a267f..4c0e90ca6 100755 --- a/src/conf_mode/interface-wireguard.py +++ b/src/conf_mode/interface-wireguard.py @@ -29,12 +29,6 @@ from vyos.ifconfig import WireGuardIf ifname = str(os.environ['VYOS_TAGNODE_VALUE']) intfc = WireGuardIf(ifname) -dir = r'/config/auth/wireguard' -pk = dir + '/private.key' -pub = dir + '/public.key' -psk_file = dir + '/psk' - - def check_kmod(): if not os.path.exists('/sys/module/wireguard'): sl.syslog(sl.LOG_NOTICE, "loading wirguard kmod") @@ -57,7 +51,8 @@ def get_config(): 'state': 'enabled', 'fwmark': 0x00, 'mtu': 1420, - 'peer': {} + 'peer': {}, + 'pk' : '/config/auth/wireguard/private.key' } } @@ -112,12 +107,11 @@ def get_config(): return config_data - def verify(c): if not c: return None - if not os.path.exists(pk): + if not os.path.exists(c[ifname]['pk']): raise ConfigError( "No keys found, generate them by executing: \'run generate wireguard keypair\'") @@ -225,7 +219,7 @@ def apply(c): sl.LOG_NOTICE, "peer {0} pubkey changed from {1} to {2} on interface {3}".format(p, ekey, nkey, ifname)) intfc.remove_peer(ekey) - intfc.config['private-key'] = pk + intfc.config['private-key'] = c[ifname]['pk'] for p in c[ifname]['peer']: intfc.config['pubkey'] = str(c[ifname]['peer'][p]['pubkey']) intfc.config['allowed-ips'] = (c[ifname]['peer'][p]['allowed-ips']) @@ -249,6 +243,7 @@ def apply(c): # preshared-key - needs to be read from a file if 'psk' in c[ifname]['peer'][p]: + psk_file = '/config/auth/wireguard/psk' old_umask = os.umask(0o077) open(psk_file, 'w').write(str(c[ifname]['peer'][p]['psk'])) os.umask(old_umask) -- cgit v1.2.3