From f16633f0ef2dd074c24387cbd2b9d5541297509c Mon Sep 17 00:00:00 2001 From: Dmytro Aleksandrov Date: Mon, 19 Aug 2019 18:53:33 +0300 Subject: T1596 rewrite 'telnet' and 'traceroute' operations to xml style --- debian/control | 3 +++ op-mode-definitions/telnet.xml | 30 +++++++++++++++++++++ op-mode-definitions/traceroute.xml | 53 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 86 insertions(+) create mode 100644 op-mode-definitions/telnet.xml create mode 100644 op-mode-definitions/traceroute.xml diff --git a/debian/control b/debian/control index a65d0158e..41b402fc1 100644 --- a/debian/control +++ b/debian/control @@ -61,6 +61,9 @@ Depends: python3, openvpn, openvpn-auth-ldap, openvpn-auth-radius, + mtr-tiny, + telnet, + traceroute, ${shlibs:Depends}, ${misc:Depends} Description: VyOS configuration scripts and data diff --git a/op-mode-definitions/telnet.xml b/op-mode-definitions/telnet.xml new file mode 100644 index 000000000..7ec75be88 --- /dev/null +++ b/op-mode-definitions/telnet.xml @@ -0,0 +1,30 @@ + + + + + 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 + + + + Telnet to a host:port + + <0-65535> + + + /usr/bin/telnet $2 $3 + + + + + + diff --git a/op-mode-definitions/traceroute.xml b/op-mode-definitions/traceroute.xml new file mode 100644 index 000000000..85f6047c1 --- /dev/null +++ b/op-mode-definitions/traceroute.xml @@ -0,0 +1,53 @@ + + + + + Track network path to node + + + + + Track network path to specified node + + <hostname> <x.x.x.x> <h:h:h:h:h:h:h:h> + + + /usr/bin/traceroute $2 + + + + + Track network path to <hostname|IPv4 address> + + <hostname> <x.x.x.x> + + + /usr/bin/traceroute -4 $3 + + + + + Track network path to <hostname|IPv6 address> + + <hostname> <h:h:h:h:h:h:h:h> + + + /usr/bin/traceroute -6 $3 + + + + + + + + + Monitor the path to a destination in realtime + + <hostname> <x.x.x.x> <h:h:h:h:h:h:h:h> + + + /usr/bin/mtr $3 + + + + -- cgit v1.2.3 From 5d858f0e6ad05b032c88c88a08c15d0876c44e8b Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Mon, 19 Aug 2019 22:49:11 +0200 Subject: openvpn: T1548: remove authy 2fa provider According to https://github.com/twilio/authy-openvpn commit 3e5dc73: > This plugin is no longer actively maintained. If you're interested in becoming a maintainer, we welcome forks of this project. In addition this plugin was always missing in the current branch ov VyOS and did not make it into VyOS 1.2 (crux) If 2FA for OpenVPN is required we should probably opt for Google Authenticator or if possible a U2F device. --- interface-definitions/interfaces-openvpn.xml | 48 ---------------------------- src/conf_mode/interface-openvpn.py | 31 ------------------ 2 files changed, 79 deletions(-) diff --git a/interface-definitions/interfaces-openvpn.xml b/interface-definitions/interfaces-openvpn.xml index d4e903c48..bb5c5a965 100644 --- a/interface-definitions/interfaces-openvpn.xml +++ b/interface-definitions/interfaces-openvpn.xml @@ -361,54 +361,6 @@ Server-mode options - - - Two Factor Authentication providers - - - - - Authy Two Factor Authentication providers - - - - - Authy api key - - - - - Authy users (must be email address) - - [A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,4}$ - - Invalid email address - - - - - Country calling codes - - [0-9]+$ - - Invalid Country Calling Code - - - - - Mobile phone number - - [0-9]+$ - - Invalid Phone Number - - - - - - - - Client-specific settings diff --git a/src/conf_mode/interface-openvpn.py b/src/conf_mode/interface-openvpn.py index e4bde7bb0..a8313378b 100755 --- a/src/conf_mode/interface-openvpn.py +++ b/src/conf_mode/interface-openvpn.py @@ -219,10 +219,6 @@ client-config-dir /opt/vyatta/etc/openvpn/ccd/{{ intf }} {% for option in options -%} {{ option }} {% endfor -%} - -{%- if server_2fa_authy_key %} -plugin /usr/lib/authy/authy-openvpn.so https://api.authy.com/protected/json {{ server_2fa_authy_key }} nopam -{% endif %} """ client_tmpl = """ @@ -269,8 +265,6 @@ default_config_data = { 'remote_address': '', 'remote_host': [], 'remote_port': '', - 'server_2fa_authy_key': '', - 'server_2fa_authy': [], 'client': [], 'server_domain': '', 'server_max_conn': '', @@ -453,31 +447,6 @@ def get_config(): if conf.exists('replace-default-route local'): openvpn['redirect_gateway'] = 'local def1' - # Two Factor Authentication providers - # currently limited to authy - if conf.exists('2-factor-authentication authy api-key'): - openvpn['server_2fa_authy_key'] = conf.return_value('2-factor-authentication authy api-key') - - # Authy users (must be email address) - for user in conf.list_nodes('server 2-factor-authentication authy user'): - # set configuration level - conf.set_level('interfaces openvpn ' + openvpn['intf'] + ' 2-factor-authentication authy user ' + user) - data = { - 'user': user, - 'country_code': '', - 'mobile_number': '' - } - - # Country calling codes - if conf.exists('country-calling-code'): - data['country_code'] = conf.return_value('country-calling-code') - - # Mobile phone number - if conf.exists('phone-number'): - data['mobile_number'] = conf.return_value('phone-number') - - openvpn['server_2fa_authy'].append(data) - # Topology for clients if conf.exists('server topology'): openvpn['server_topology'] = conf.return_value('server topology') -- cgit v1.2.3 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(+) 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 80ee4d8c545879d2422e7027ad5245bee484ee3c Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Mon, 19 Aug 2019 23:43:08 +0200 Subject: dummy: T1580: rewrite in new style XML/Python --- interface-definitions/interfaces-dummy.xml | 51 ++++++++++++ src/conf_mode/interface-dummy.py | 122 +++++++++++++++++++++++++++++ 2 files changed, 173 insertions(+) create mode 100644 interface-definitions/interfaces-dummy.xml create mode 100755 src/conf_mode/interface-dummy.py diff --git a/interface-definitions/interfaces-dummy.xml b/interface-definitions/interfaces-dummy.xml new file mode 100644 index 000000000..7c425480a --- /dev/null +++ b/interface-definitions/interfaces-dummy.xml @@ -0,0 +1,51 @@ + + + + + + + Dummy interface name + 300 + + dum[0-9]+$ + + Dummy interface must be named dumN + + dumN + Dummy 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 + + + + + + + diff --git a/src/conf_mode/interface-dummy.py b/src/conf_mode/interface-dummy.py new file mode 100755 index 000000000..668e4acc7 --- /dev/null +++ b/src/conf_mode/interface-dummy.py @@ -0,0 +1,122 @@ +#!/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 copy + +import vyos.configinterface as VyIfconfig + +from vyos.config import Config +from vyos import ConfigError + +default_config_data = { + 'address': [], + 'address_remove': [], + 'deleted': False, + 'description': '', + 'disable': False, + 'intf': '' +} + +def diff(first, second): + second = set(second) + return [item for item in first if item not in second] + +def get_config(): + dummy = copy.deepcopy(default_config_data) + conf = Config() + + # determine tagNode instance + try: + dummy['intf'] = os.environ['VYOS_TAGNODE_VALUE'] + except KeyError as E: + print("Interface not specified") + + # Check if interface has been removed + if not conf.exists('interfaces dummy ' + dummy['intf']): + dummy['deleted'] = True + return dummy + + # set new configuration level + conf.set_level('interfaces dummy ' + dummy['intf']) + + # retrieve configured interface addresses + if conf.exists('address'): + dummy['address'] = conf.return_values('address') + + # retrieve interface description + if conf.exists('description'): + dummy['description'] = conf.return_value('description') + + # Disable this interface + if conf.exists('disable'): + dummy['disable'] = True + + # 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') + dummy['address_remove'] = diff(eff_addr, act_addr) + + return dummy + +def verify(dummy): + if dummy is None: + return None + + return None + +def generate(dummy): + if dummy is None: + return None + + return None + +def apply(dummy): + if dummy is None: + return None + + # Remove dummy interface + if dummy['deleted']: + VyIfconfig.remove_interface(dummy['intf']) + else: + # Interface will only be added if it yet does not exist + VyIfconfig.add_interface('dummy', dummy['intf']) + + # update interface description used e.g. within SNMP + VyIfconfig.set_description(dummy['intf'], dummy['description']) + + # Configure interface address(es) + for addr in dummy['address_remove']: + VyIfconfig.remove_interface_address(dummy['intf'], addr) + + for addr in dummy['address']: + VyIfconfig.add_interface_address(dummy['intf'], addr) + + return None + +if __name__ == '__main__': + try: + c = get_config() + verify(c) + generate(c) + apply(c) + except ConfigError as e: + print(e) + sys.exit(1) -- 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(-) 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(+) 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 dc0f641956d002fa8588ef8d1213791cf36e92f2 Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Tue, 20 Aug 2019 11:50:55 +0200 Subject: powerdns: T1524: support setting allow-from network Netmasks (both IPv4 and IPv6) that are allowed to use the server. The default allows access only from RFC 1918 private IP addresses. Due to the aggressive nature of the internet these days, it is highly recommended to not open up the recursor for the entire internet. Questions from IP addresses not listed here are ignored and do not get an answer. https://docs.powerdns.com/recursor/settings.html#allow-from Imagine an ISP network with non RFC1918 IP adresses - they can't make use of PowerDNS recursor. As of now VyOS hat allow-from set to 0.0.0.0/0 and ::/0 which created an open resolver. If there is no allow-from statement a config-migrator will add the appropriate nodes to the configuration, resulting in: service { dns { forwarding { allow-from 0.0.0.0/0 allow-from ::/0 cache-size 0 ignore-hosts-file listen-address 192.0.2.1 } } } --- interface-definitions/dns-forwarding.xml | 17 ++++++++++ src/conf_mode/dns_forwarding.py | 21 ++++++------ src/migration-scripts/dns-forwarding/0-to-1 | 50 +++++++++++++++++++++++++++++ 3 files changed, 79 insertions(+), 9 deletions(-) create mode 100755 src/migration-scripts/dns-forwarding/0-to-1 diff --git a/interface-definitions/dns-forwarding.xml b/interface-definitions/dns-forwarding.xml index 56820608c..08a221ebe 100644 --- a/interface-definitions/dns-forwarding.xml +++ b/interface-definitions/dns-forwarding.xml @@ -97,6 +97,23 @@ + + + Networks allowed to query this server + + ipv4net + IP address and prefix length + + + ipv6net + IPv6 address and prefix length + + + + + + + Addresses to listen for DNS queries [REQUIRED] diff --git a/src/conf_mode/dns_forwarding.py b/src/conf_mode/dns_forwarding.py index 3ca77adee..86683e72c 100755 --- a/src/conf_mode/dns_forwarding.py +++ b/src/conf_mode/dns_forwarding.py @@ -44,7 +44,7 @@ config_tmpl = """ # Non-configurable defaults daemon=yes threads=1 -allow-from=0.0.0.0/0, ::/0 +allow-from={{ allow_from | join(',') }} log-common-errors=yes non-local-bind=yes query-local-address=0.0.0.0 @@ -83,6 +83,7 @@ dnssec={{ dnssec }} """ default_config_data = { + 'allow_from': [], 'cache_size': 10000, 'export_hosts_file': 'yes', 'listen_on': [], @@ -121,6 +122,9 @@ def get_config(arguments): conf.set_level('service dns forwarding') + if conf.exists('allow-from'): + dns['allow_from'] = conf.return_values('allow-from') + if conf.exists('cache-size'): cache_size = conf.return_value('cache-size') dns['cache_size'] = cache_size @@ -216,12 +220,10 @@ def get_config(arguments): return dns - def bracketize_ipv6_addrs(addrs): """Wraps each IPv6 addr in addrs in [], leaving IPv4 addrs untouched.""" return ['[{0}]'.format(a) if a.count(':') > 1 else a for a in addrs] - def verify(dns): # bail out early - looks like removal from running config if dns is None: @@ -231,6 +233,10 @@ def verify(dns): raise ConfigError( "Error: DNS forwarding requires either a listen-address (preferred) or a listen-on option") + if not dns['allow_from']: + raise ConfigError( + "Error: DNS forwarding requires an allow-from network") + if dns['domains']: for domain in dns['domains']: if not domain['servers']: @@ -239,7 +245,6 @@ def verify(dns): return None - def generate(dns): # bail out early - looks like removal from running config if dns is None: @@ -251,16 +256,14 @@ def generate(dns): f.write(config_text) return None - def apply(dns): - if dns is not None: - os.system("systemctl restart pdns-recursor") - else: + if dns is None: # DNS forwarding is removed in the commit os.system("systemctl stop pdns-recursor") if os.path.isfile(config_file): os.unlink(config_file) - + else: + os.system("systemctl restart pdns-recursor") if __name__ == '__main__': args = parser.parse_args() diff --git a/src/migration-scripts/dns-forwarding/0-to-1 b/src/migration-scripts/dns-forwarding/0-to-1 new file mode 100755 index 000000000..6e8720eef --- /dev/null +++ b/src/migration-scripts/dns-forwarding/0-to-1 @@ -0,0 +1,50 @@ +#!/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 . +# + +# This migration script will check if there is a allow-from directive configured +# for the dns forwarding service - if not, the node will be created with the old +# default values of 0.0.0.0/0 and ::/0 + +import sys +from vyos.configtree import ConfigTree + +if (len(sys.argv) < 1): + print("Must specify file name!") + sys.exit(1) + +file_name = sys.argv[1] + +with open(file_name, 'r') as f: + config_file = f.read() + +config = ConfigTree(config_file) + +base = ['service', 'dns', 'forwarding'] +if not config.exists(base): + # Nothing to do + sys.exit(0) +else: + if not config.exists(base + ['allow-from']): + config.set(base + ['allow-from'], value='0.0.0.0/0', replace=False) + config.set(base + ['allow-from'], value='::/0', replace=False) + + try: + with open(file_name, 'w') as f: + f.write(config.to_string()) + except OSError as e: + print("Failed to save the modified config: {}".format(e)) + sys.exit(1) -- 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(+) 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 3f225b56f576d30bd163f975c821e8baf2be6d28 Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Tue, 20 Aug 2019 11:56:08 +0200 Subject: powerdns: T1595: add config migrator to remove 'listen-on' --- src/migration-scripts/dns-forwarding/1-to-2 | 78 +++++++++++++++++++++++++++++ 1 file changed, 78 insertions(+) create mode 100755 src/migration-scripts/dns-forwarding/1-to-2 diff --git a/src/migration-scripts/dns-forwarding/1-to-2 b/src/migration-scripts/dns-forwarding/1-to-2 new file mode 100755 index 000000000..31ba5573f --- /dev/null +++ b/src/migration-scripts/dns-forwarding/1-to-2 @@ -0,0 +1,78 @@ +#!/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 . +# + +# This migration script will remove the deprecated 'listen-on' statement +# from the dns forwarding service and will add the corresponding +# listen-address nodes instead. This is required as PowerDNS can only listen +# on interface addresses and not on interface names. + +import sys + +from ipaddress import ip_interface +from vyos.configtree import ConfigTree +from vyos.interfaces import get_type_of_interface + +if (len(sys.argv) < 1): + print("Must specify file name!") + sys.exit(1) + +file_name = sys.argv[1] + +with open(file_name, 'r') as f: + config_file = f.read() + +config = ConfigTree(config_file) + +base = ['service', 'dns', 'forwarding'] +if not config.exists(base): + # Nothing to do + sys.exit(0) +else: + if config.exists(base + ['listen-on']): + listen_intf = config.return_values(base + ['listen-on']) + # Delete node with abandoned command + config.delete(base + ['listen-on']) + + # retrieve interface addresses for every configured listen-on interface + listen_addr = [] + for intf in listen_intf: + # we need to treat vif and vif-s interfaces differently, + # both "real interfaces" use dots for vlan identifiers - those + # need to be exchanged with vif and vif-s identifiers + if intf.count('.') == 1: + # this is a regular VLAN interface + intf = intf.split('.')[0] + ' vif ' + intf.split('.')[1] + elif intf.count('.') == 2: + # this is a QinQ VLAN interface + intf = intf.split('.')[0] + ' vif-s ' + intf.split('.')[1] + ' vif-c ' + intf.split('.')[2] + + path = ['interfaces', get_type_of_interface(intf), intf, 'address'] + + # retrieve corresponding interface addresses in CIDR format + # those need to be converted in pure IP addresses without network information + for addr in config.return_values(path): + listen_addr.append( ip_interface(addr).ip ) + + for addr in listen_addr: + config.set(base + ['listen-address'], value=addr, replace=False) + + try: + with open(file_name, 'w') as f: + f.write(config.to_string()) + except OSError as e: + print("Failed to save the modified config: {}".format(e)) + sys.exit(1) -- cgit v1.2.3 From dbdd50e96f5af8f59d884f03df1cdeed9bac39d1 Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Tue, 20 Aug 2019 12:02:49 +0200 Subject: powerdns: T1595: remove 'listen-on' CLI option --- interface-definitions/dns-forwarding.xml | 9 ------- src/conf_mode/dns_forwarding.py | 41 -------------------------------- 2 files changed, 50 deletions(-) diff --git a/interface-definitions/dns-forwarding.xml b/interface-definitions/dns-forwarding.xml index 08a221ebe..a88c174e3 100644 --- a/interface-definitions/dns-forwarding.xml +++ b/interface-definitions/dns-forwarding.xml @@ -132,15 +132,6 @@ - - - Interface to listen for DNS queries [DEPRECATED] - - - - - - Maximum amount of time negative entries are cached diff --git a/src/conf_mode/dns_forwarding.py b/src/conf_mode/dns_forwarding.py index 86683e72c..9e81c7294 100755 --- a/src/conf_mode/dns_forwarding.py +++ b/src/conf_mode/dns_forwarding.py @@ -87,7 +87,6 @@ default_config_data = { 'cache_size': 10000, 'export_hosts_file': 'yes', 'listen_on': [], - 'interfaces': [], 'name_servers': [], 'negative_ttl': 3600, 'domains': [], @@ -168,46 +167,6 @@ def get_config(arguments): if conf.exists('dnssec'): dns['dnssec'] = conf.return_value('dnssec') - ## Hacks and tricks - - # The old VyOS syntax that comes from dnsmasq was "listen-on $interface". - # pdns wants addresses instead, so we emulate it by looking up all addresses - # of a given interface and writing them to the config - if conf.exists('listen-on'): - print("WARNING: since VyOS 1.2.0, \"service dns forwarding listen-on\" is a limited compatibility option.") - print("It will only make DNS forwarder listen on addresses assigned to the interface at the time of commit") - print("which means it will NOT work properly with VRRP/clustering or addresses received from DHCP.") - print("Please reconfigure your system with \"service dns forwarding listen-address\" instead.") - - interfaces = conf.return_values('listen-on') - - listen4 = [] - listen6 = [] - for interface in interfaces: - try: - addrs = netifaces.ifaddresses(interface) - except ValueError: - print( - "WARNING: interface {0} does not exist".format(interface)) - continue - - if netifaces.AF_INET in addrs.keys(): - for ip4 in addrs[netifaces.AF_INET]: - listen4.append(ip4['addr']) - - if netifaces.AF_INET6 in addrs.keys(): - for ip6 in addrs[netifaces.AF_INET6]: - listen6.append(ip6['addr']) - - if (not listen4) and (not (listen6)): - print( - "WARNING: interface {0} has no configured addresses".format(interface)) - - dns['listen_on'] = dns['listen_on'] + listen4 + listen6 - - # Save interfaces in the dict for the reference - dns['interfaces'] = interfaces - # Add name servers received from DHCP if conf.exists('dhcp'): interfaces = [] -- 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 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 525438ac4ad1e4fba534c7d1c51283c4c2561d3e Mon Sep 17 00:00:00 2001 From: Daniil Baturin Date: Tue, 20 Aug 2019 19:47:16 +0200 Subject: T1598: start vyos-hostsd before everything that may need it. --- src/systemd/vyos-hostsd.service | 1 + 1 file changed, 1 insertion(+) diff --git a/src/systemd/vyos-hostsd.service b/src/systemd/vyos-hostsd.service index fe6c163d7..8ff213d2b 100644 --- a/src/systemd/vyos-hostsd.service +++ b/src/systemd/vyos-hostsd.service @@ -1,6 +1,7 @@ [Unit] Description=VyOS DNS configuration keeper After=auditd.service systemd-user-sessions.service time-sync.target +Before=vyos-router.service cloud-init.service [Service] ExecStart=/usr/bin/python3 -u /usr/libexec/vyos/services/vyos-hostsd -- 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(-) 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 3cbebc67f970f1add6548c5e386b40d5632f8452 Mon Sep 17 00:00:00 2001 From: Daniil Baturin Date: Wed, 21 Aug 2019 08:46:56 +0200 Subject: T1598: improve autogenerated file comments. --- src/services/vyos-hostsd | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/services/vyos-hostsd b/src/services/vyos-hostsd index 972c2e3dc..aec06c72c 100755 --- a/src/services/vyos-hostsd +++ b/src/services/vyos-hostsd @@ -38,6 +38,9 @@ HOSTS_FILE = '/etc/hosts' hosts_tmpl_source = """ ### Autogenerated by VyOS ### +### Do not edit, your changes will get overwritten ### + +# Local host 127.0.0.1 localhost 127.0.1.1 {{ host_name }}{% if domain_name %}.{{ domain_name }}{% endif %} @@ -48,7 +51,7 @@ ff00::0 ip6-mcastprefix ff02::1 ip6-allnodes ff02::2 ip6-allrouters - +# From DHCP and "system static host-mapping" {%- if hosts %} {% for h in hosts -%} {{hosts[h]['address']}}\t{{h}}\t{% for a in hosts[h]['aliases'] %} {{a}} {% endfor %} @@ -60,6 +63,8 @@ hosts_tmpl = jinja2.Template(hosts_tmpl_source) resolv_tmpl_source = """ ### Autogenerated by VyOS ### +### Do not edit, your changes will get overwritten ### + {% for ns in name_servers -%} nameserver {{ns}} {% endfor -%} @@ -83,7 +88,7 @@ resolv_tmpl = jinja2.Template(resolv_tmpl_source) # {"server": {"tag": }} # # Hosts entries are similar: -# {"host": {"tag": , "address": }} +# {"host": {"tag": , "address": , "aliases": }} # # The tag is either "static" or "dhcp-" # It's used to distinguish entries created -- cgit v1.2.3 From a923efc0062119189d876e37514bd24b7773509e Mon Sep 17 00:00:00 2001 From: Daniil Baturin Date: Wed, 21 Aug 2019 09:08:27 +0200 Subject: T1598: clean up vyos-hostsd state dump on clean shutdown. --- src/services/vyos-hostsd | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/services/vyos-hostsd b/src/services/vyos-hostsd index aec06c72c..1bcb2e1f5 100755 --- a/src/services/vyos-hostsd +++ b/src/services/vyos-hostsd @@ -20,6 +20,7 @@ import os import sys import time import json +import signal import traceback import zmq @@ -218,8 +219,16 @@ def handle_message(msg_json): with open(STATE_FILE, 'w') as f: json.dump(STATE, f) +def exit_handler(sig, frame): + """ Clean up the state when shutdown correctly """ + print("Cleaning up state") + os.unlink(STATE_FILE) + sys.exit(0) + if __name__ == '__main__': + signal.signal(signal.SIGTERM, exit_handler) + # Create a directory for state checkpoints os.makedirs(DATA_DIR, exist_ok=True) if os.path.exists(STATE_FILE): -- cgit v1.2.3 From 63855b5c6b674d102804b43bcda85499a43973a4 Mon Sep 17 00:00:00 2001 From: Daniil Baturin Date: Wed, 21 Aug 2019 09:09:07 +0200 Subject: T1598: redo host_name.py to use vyos-hostsd. --- src/conf_mode/host_name.py | 196 +++++++++------------------------------------ 1 file changed, 36 insertions(+), 160 deletions(-) diff --git a/src/conf_mode/host_name.py b/src/conf_mode/host_name.py index 2fad57db6..bb1ec9597 100755 --- a/src/conf_mode/host_name.py +++ b/src/conf_mode/host_name.py @@ -30,57 +30,12 @@ import argparse import jinja2 import vyos.util +import vyos.hostsd_client from vyos.config import Config from vyos import ConfigError -parser = argparse.ArgumentParser() -parser.add_argument("--dhclient", action="store_true", - help="Started from dhclient-script") - -config_file_hosts = '/etc/hosts' -config_file_resolv = '/etc/resolv.conf' - -config_tmpl_hosts = """ -### Autogenerated by host_name.py ### -127.0.0.1 localhost -127.0.1.1 {{ hostname }}{% if domain_name %}.{{ domain_name }} {{ hostname }}{% 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 - -# static hostname mappings -{%- if static_host_mapping['hostnames'] %} -{% for hn in static_host_mapping['hostnames'] -%} -{{static_host_mapping['hostnames'][hn]['ipaddr']}}\t{{static_host_mapping['hostnames'][hn]['alias']}}\t{{hn}} -{% endfor -%} -{%- endif %} - -### modifications from other scripts should be added below - -""" - -config_tmpl_resolv = """ -### Autogenerated by host_name.py ### -{% for ns in nameserver -%} -nameserver {{ ns }} -{% endfor -%} - -{%- if domain_name %} -domain {{ domain_name }} -{%- endif %} - -{%- if domain_search %} -search {{ domain_search | join(" ") }} -{%- endif %} - -""" - default_config_data = { 'hostname': 'vyos', 'domain_name': '', @@ -89,32 +44,10 @@ default_config_data = { 'no_dhcp_ns': False } -# borrowed from: https://github.com/donjajo/py-world/blob/master/resolvconfReader.py, THX! -def get_resolvers(file): - resolv = {} - try: - with open(file, 'r') as resolvconf: - lines = [line.split('#', 1)[0].rstrip() - for line in resolvconf.readlines()] - resolvers = [line.split()[1] - for line in lines if 'nameserver' in line] - domains = [line.split()[1] for line in lines if 'search' in line] - resolv['resolvers'] = resolvers - resolv['domains'] = domains - return resolv - except IOError: - return [] - - -def get_config(arguments): +def get_config(): conf = Config() hosts = copy.deepcopy(default_config_data) - if arguments.dhclient: - conf.exists = conf.exists_effective - conf.return_value = conf.return_effective_value - conf.return_values = conf.return_effective_values - if conf.exists("system host-name"): hosts['hostname'] = conf.return_value("system host-name") # This may happen if the config is not loaded yet, @@ -136,19 +69,15 @@ def get_config(arguments): hosts['no_dhcp_ns'] = conf.exists('system disable-dhcp-nameservers') # system static-host-mapping - hosts['static_host_mapping'] = {'hostnames': {}} + hosts['static_host_mapping'] = [] if conf.exists('system static-host-mapping host-name'): for hn in conf.list_nodes('system static-host-mapping host-name'): - hosts['static_host_mapping']['hostnames'][hn] = { - 'ipaddr': conf.return_value('system static-host-mapping host-name ' + hn + ' inet'), - 'alias': '' - } - - if conf.exists('system static-host-mapping host-name ' + hn + ' alias'): - a = conf.return_values( - 'system static-host-mapping host-name ' + hn + ' alias') - hosts['static_host_mapping']['hostnames'][hn]['alias'] = " ".join(a) + mapping = {} + mapping['host'] = hn + mapping['address'] = conf.return_value('system static-host-mapping host-name {0} inet'.format(hn)) + mapping['aliases'] = conf.return_values('system static-host-mapping host-name {0} alias'.format(hn)) + hosts['static_host_mapping'].append(mapping) return hosts @@ -180,83 +109,43 @@ def verify(config): 'The search list is currently limited to 256 characters') # static mappings alias hostname - if config['static_host_mapping']['hostnames']: - for hn in config['static_host_mapping']['hostnames']: - if not config['static_host_mapping']['hostnames'][hn]['ipaddr']: - raise ConfigError('IP address required for ' + hn) - for hn_alias in config['static_host_mapping']['hostnames'][hn]['alias'].split(' '): - if not hostname_regex.match(hn_alias) and len(hn_alias) != 0: - raise ConfigError('Invalid hostname alias ' + hn_alias) + if config['static_host_mapping']: + for m in config['static_host_mapping']: + if not m['address']: + raise ConfigError('IP address required for ' + m['host']) + for a in m['aliases']: + if not hostname_regex.match(a) and len(a) != 0: + raise ConfigError('Invalid alias \'{0}\' in mapping {1}'.format(a, m['host'])) return None def generate(config): + pass + +def apply(config): if config is None: return None - # If "system disable-dhcp-nameservers" is __configured__ all DNS resolvers - # received via dhclient should not be added into the final 'resolv.conf'. - # - # We iterate over every resolver file and retrieve the received nameservers - # for later adjustment of the system nameservers - dhcp_ns = [] - dhcp_sd = [] - for file in glob.glob('/etc/resolv.conf.dhclient-new*'): - for key, value in get_resolvers(file).items(): - ns = [r for r in value if key == 'resolvers'] - dhcp_ns.extend(ns) - sd = [d for d in value if key == 'domains'] - dhcp_sd.extend(sd) - - if not config['no_dhcp_ns']: - config['nameserver'] += dhcp_ns - config['domain_search'] += dhcp_sd - - # Prune duplicate values - # Not order preserving, but then when multiple DHCP clients are used, - # there can't be guarantees about the order anyway - dhcp_ns = list(set(dhcp_ns)) - dhcp_sd = list(set(dhcp_sd)) - - # We have third party scripts altering /etc/hosts, too. - # One example are the DHCP hostname update scripts thus we need to cache in - # every modification first - so changing domain-name, domain-search or hostname - # during runtime works - old_hosts = "" - with open(config_file_hosts, 'r') as f: - # Skips text before the beginning of our marker. - # NOTE: Marker __MUST__ match the one specified in config_tmpl_hosts - for line in f: - if line.strip() == '### modifications from other scripts should be added below': - break - - for line in f: - # This additional line.strip() filters empty lines - if line.strip(): - old_hosts += line - - # Add an additional newline - old_hosts += '\n' - - tmpl = jinja2.Template(config_tmpl_hosts) - config_text = tmpl.render(config) - - with open(config_file_hosts, 'w') as f: - f.write(config_text) - f.write(old_hosts) - - tmpl = jinja2.Template(config_tmpl_resolv) - config_text = tmpl.render(config) - with open(config_file_resolv, 'w') as f: - f.write(config_text) + ## Send the updated data to vyos-hostsd - return None + # vyos-hostsd uses "tags" to identify data sources + tag = "static" + try: + client = vyos.hostsd_client.Client() -def apply(config): - if config is None: - return None + client.set_host_name(config['hostname'], config['domain_name'], config['domain_search']) + + client.delete_name_servers(tag) + client.add_name_servers(tag, config['nameserver']) + + client.delete_hosts(tag) + client.add_hosts(tag, config['static_host_mapping']) + except vyos.hostsd_client.VyOSHostsdError as e: + raise ConfigError(str(e)) + + ## Actually update the hostname -- vyos-hostsd doesn't do that # No domain name -- the Debian way. hostname_new = config['hostname'] @@ -283,22 +172,9 @@ def apply(config): if __name__ == '__main__': - args = parser.parse_args() - - if args.dhclient: - # There's a big chance it was triggered by a commit still in progress - # so we need to wait until the new values are in the running config - vyos.util.wait_for_commit_lock() - - try: - c = get_config(args) - # If it's called from dhclient, then either: - # a) verification was already done at commit time - # b) it's run on an unconfigured system, e.g. by cloud-init - # Therefore, verification is either redundant or useless - if not args.dhclient: - verify(c) + c = get_config() + verify(c) generate(c) apply(c) except ConfigError as e: -- cgit v1.2.3 From fe343f428a9740cdd3c6cf966f84238a14cd49fc Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Wed, 21 Aug 2019 18:31:42 +0200 Subject: loopback: T1601: rewrite using XML/Python definitions --- interface-definitions/interfaces-loopback.xml | 46 ++++++++++++ src/conf_mode/interface-loopback.py | 102 ++++++++++++++++++++++++++ 2 files changed, 148 insertions(+) create mode 100644 interface-definitions/interfaces-loopback.xml create mode 100755 src/conf_mode/interface-loopback.py diff --git a/interface-definitions/interfaces-loopback.xml b/interface-definitions/interfaces-loopback.xml new file mode 100644 index 000000000..267731b1c --- /dev/null +++ b/interface-definitions/interfaces-loopback.xml @@ -0,0 +1,46 @@ + + + + + + + Loopback interface + 300 + + lo$ + + Loopback interface must be named lo + + lo + Loopback interface + + + + + + 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) + + + + + + + diff --git a/src/conf_mode/interface-loopback.py b/src/conf_mode/interface-loopback.py new file mode 100755 index 000000000..445a9af64 --- /dev/null +++ b/src/conf_mode/interface-loopback.py @@ -0,0 +1,102 @@ +#!/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 copy + +import vyos.configinterface as VyIfconfig + +from vyos.config import Config +from vyos import ConfigError + +default_config_data = { + 'address': [], + 'address_remove': [], + 'deleted': False, + 'description': '', +} + +def diff(first, second): + second = set(second) + return [item for item in first if item not in second] + +def get_config(): + loopback = copy.deepcopy(default_config_data) + conf = Config() + + # determine tagNode instance + try: + loopback['intf'] = os.environ['VYOS_TAGNODE_VALUE'] + except KeyError as E: + print("Interface not specified") + + # Check if interface has been removed + if not conf.exists('interfaces loopback ' + loopback['intf']): + loopback['deleted'] = True + + # set new configuration level + conf.set_level('interfaces loopback ' + loopback['intf']) + + # retrieve configured interface addresses + if conf.exists('address'): + loopback['address'] = conf.return_values('address') + + # retrieve interface description + if conf.exists('description'): + loopback['description'] = conf.return_value('description') + + # 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') + loopback['address_remove'] = diff(eff_addr, act_addr) + + return loopback + +def verify(loopback): + return None + +def generate(loopback): + return None + +def apply(loopback): + # Remove loopback interface + if not loopback['deleted']: + # update interface description used e.g. within SNMP + VyIfconfig.set_description(loopback['intf'], loopback['description']) + + # Configure interface address(es) + for addr in loopback['address']: + VyIfconfig.add_interface_address(loopback['intf'], addr) + + # Remove interface address(es) + for addr in loopback['address_remove']: + VyIfconfig.remove_interface_address(loopback['intf'], addr) + + return None + +if __name__ == '__main__': + try: + c = get_config() + verify(c) + generate(c) + apply(c) + except ConfigError as e: + print(e) + sys.exit(1) -- cgit v1.2.3 From 63cdf781f4604ed38591d0be71fa92ae3667da73 Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Wed, 21 Aug 2019 18:33:06 +0200 Subject: bridge: T1556: remove superfluous if statements --- src/conf_mode/interface-bridge.py | 9 --------- 1 file changed, 9 deletions(-) diff --git a/src/conf_mode/interface-bridge.py b/src/conf_mode/interface-bridge.py index 543349e7b..65b5c4066 100755 --- a/src/conf_mode/interface-bridge.py +++ b/src/conf_mode/interface-bridge.py @@ -178,9 +178,6 @@ def get_config(): return bridge def verify(bridge): - if bridge is None: - return None - conf = Config() for br in conf.list_nodes('interfaces bridge'): # it makes no sense to verify ourself in this case @@ -195,15 +192,9 @@ def verify(bridge): return None def generate(bridge): - if bridge is None: - return None - return None def apply(bridge): - if bridge is None: - return None - cmd = '' if bridge['deleted']: # bridges need to be shutdown first -- cgit v1.2.3 From d553b380d59961093f777ce55f7e98cc8bd1844b Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Wed, 21 Aug 2019 18:33:30 +0200 Subject: dummy: T1580: remove superfluous if statements --- src/conf_mode/interface-dummy.py | 9 --------- 1 file changed, 9 deletions(-) diff --git a/src/conf_mode/interface-dummy.py b/src/conf_mode/interface-dummy.py index 668e4acc7..ff9d57c89 100755 --- a/src/conf_mode/interface-dummy.py +++ b/src/conf_mode/interface-dummy.py @@ -77,21 +77,12 @@ def get_config(): return dummy def verify(dummy): - if dummy is None: - return None - return None def generate(dummy): - if dummy is None: - return None - return None def apply(dummy): - if dummy is None: - return None - # Remove dummy interface if dummy['deleted']: VyIfconfig.remove_interface(dummy['intf']) -- 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(-) 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(-) 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 db67a7e9e6f448567203ceb5f6364e78c7bb3300 Mon Sep 17 00:00:00 2001 From: John Estabrook Date: Thu, 22 Aug 2019 12:51:12 -0500 Subject: T1606: change vyos-hostsd systemd target to avoid boot problems --- src/systemd/vyos-hostsd.service | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/systemd/vyos-hostsd.service b/src/systemd/vyos-hostsd.service index 8ff213d2b..3b0fadb5c 100644 --- a/src/systemd/vyos-hostsd.service +++ b/src/systemd/vyos-hostsd.service @@ -1,7 +1,8 @@ [Unit] Description=VyOS DNS configuration keeper -After=auditd.service systemd-user-sessions.service time-sync.target -Before=vyos-router.service cloud-init.service +After=auditd.service time-sync.target +Before=network-pre.target vyos-router.service +Wants=network-pre.target [Service] ExecStart=/usr/bin/python3 -u /usr/libexec/vyos/services/vyos-hostsd @@ -18,6 +19,6 @@ User=root Group=vyattacfg [Install] -# Installing in a earlier target leaves ExecStartPre waiting -WantedBy=getty.target +# +WantedBy=network.target -- cgit v1.2.3 From 87be8bbd814f2e387c3b60fbd3f44e55a21b2bce Mon Sep 17 00:00:00 2001 From: DmitriyEshenko Date: Fri, 23 Aug 2019 13:26:08 +0000 Subject: [dummy] T1609 Fixing dummy interface state --- interface-definitions/interfaces-dummy.xml | 1 + src/conf_mode/interface-dummy.py | 6 ++++++ 2 files changed, 7 insertions(+) diff --git a/interface-definitions/interfaces-dummy.xml b/interface-definitions/interfaces-dummy.xml index 7c425480a..4450b69b7 100644 --- a/interface-definitions/interfaces-dummy.xml +++ b/interface-definitions/interfaces-dummy.xml @@ -42,6 +42,7 @@ Disable interface + diff --git a/src/conf_mode/interface-dummy.py b/src/conf_mode/interface-dummy.py index ff9d57c89..8c939ce95 100755 --- a/src/conf_mode/interface-dummy.py +++ b/src/conf_mode/interface-dummy.py @@ -22,6 +22,7 @@ import copy import vyos.configinterface as VyIfconfig +from vyos.interfaceconfig import Interface from vyos.config import Config from vyos import ConfigError @@ -100,6 +101,11 @@ def apply(dummy): for addr in dummy['address']: VyIfconfig.add_interface_address(dummy['intf'], addr) + if dummy['disable']: + Interface(dummy['intf']).linkstate = 'down' + else: + Interface(dummy['intf']).linkstate = 'up' + return None if __name__ == '__main__': -- 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 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(-) 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 a235b4453b4b0dde6d8da1d8b01aac1c5cdf3491 Mon Sep 17 00:00:00 2001 From: Jernej Jakob Date: Sat, 24 Aug 2019 23:49:53 +0200 Subject: T1611: check if config node exists before getting value --- src/migration-scripts/interfaces/0-to-1 | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/src/migration-scripts/interfaces/0-to-1 b/src/migration-scripts/interfaces/0-to-1 index 38f2bd8f5..96e18b5d5 100755 --- a/src/migration-scripts/interfaces/0-to-1 +++ b/src/migration-scripts/interfaces/0-to-1 @@ -30,20 +30,22 @@ else: # for br in config.list_nodes(base): # STP: check if enabled - stp_val = config.return_value(base + [br, 'stp']) - # STP: delete node with old syntax - config.delete(base + [br, 'stp']) - # STP: set new node - if enabled - if stp_val == "true": - config.set(base + [br, 'stp'], value=None) + if config.exists(base + [br, 'stp']): + stp_val = config.return_value(base + [br, 'stp']) + # STP: delete node with old syntax + config.delete(base + [br, 'stp']) + # STP: set new node - if enabled + if stp_val == "true": + config.set(base + [br, 'stp'], value=None) # igmp-snooping: check if enabled - igmp_val = config.return_value(base + [br, 'igmp-snooping', 'querier']) - # igmp-snooping: delete node with old syntax - config.delete(base + [br, 'igmp-snooping', 'querier']) - # igmp-snooping: set new node - if enabled - if igmp_val == "enable": - config.set(base + [br, 'igmp', 'querier'], value=None) + if config.exists(base + [br, 'igmp-snooping', 'querier']): + igmp_val = config.return_value(base + [br, 'igmp-snooping', 'querier']) + # igmp-snooping: delete node with old syntax + config.delete(base + [br, 'igmp-snooping', 'querier']) + # igmp-snooping: set new node - if enabled + if igmp_val == "enable": + config.set(base + [br, 'igmp', 'querier'], value=None) # # move interface based bridge-group to actual bridge (de-nest) -- cgit v1.2.3 From d78afa762043e113df38fcb871f937ff52679998 Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Mon, 26 Aug 2019 13:13:26 +0200 Subject: openvpn: T1548: fix indention on op-mode definition --- op-mode-definitions/openvpn.xml | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/op-mode-definitions/openvpn.xml b/op-mode-definitions/openvpn.xml index ac0c42789..2adbfba53 100644 --- a/op-mode-definitions/openvpn.xml +++ b/op-mode-definitions/openvpn.xml @@ -2,25 +2,25 @@ - - - OpenVPN key generation tool - - - - - Generate shared-secret key with specified file name - - <filename> - - - + + + OpenVPN key generation tool + + + + + Generate shared-secret key with specified file name + + <filename> + + + result=1; key_path=$4 full_path= # Prepend /config/auth if the path is not absolute - if echo $key_path | egrep -ve '^/.*' > /dev/null; then + if echo $key_path | egrep -ve '^/.*' > /dev/null; then full_path=/config/auth/$key_path else full_path=$key_path @@ -40,9 +40,9 @@ fi /usr/libexec/vyos/validators/file-exists --directory /config/auth "$full_path" - - - + + + @@ -59,7 +59,7 @@ - echo kill $4 | socat - UNIX-CONNECT:/tmp/openvpn-mgmt-intf > /dev/null + echo kill $4 | socat - UNIX-CONNECT:/tmp/openvpn-mgmt-intf > /dev/null -- cgit v1.2.3 From c83d12cf79635a14f87696fa9817840cc2f6fa8d Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Mon, 26 Aug 2019 13:14:35 +0200 Subject: openvpn: T1548: add 'show openvpn' command vyos@vyos:~$ show openvpn site-to-site OpenVPN status on vtun1 Client CN Remote Host Local Host TX bytes RX bytes Connected Since --------- ----------- ---------- -------- -------- --------------- None (PSK) N/A 172.18.201.10:1195 3.3 KiB 3.3 KiB N/A vyos@vyos:~$ show openvpn server OpenVPN status on vtun10 Client CN Remote Host Local Host TX bytes RX bytes Connected Since --------- ----------- ---------- -------- -------- --------------- client1 172.18.202.10:58644 172.18.201.10:1194 63.6 KiB 63.4 KiB Mon Aug 26 11:47:56 2019 client3 172.18.204.10:52641 172.18.201.10:1194 63.1 KiB 62.7 KiB Mon Aug 26 11:47:58 2019 OpenVPN status on vtun11 Client CN Remote Host Local Host TX bytes RX bytes Connected Since --------- ----------- ---------- -------- -------- --------------- client2 172.18.203.10:39472 172.18.201.10:1200 61.2 KiB 61.5 KiB Mon Aug 26 11:50:30 2019 --- op-mode-definitions/openvpn.xml | 25 ++++++ src/op_mode/show_openvpn.py | 170 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 195 insertions(+) create mode 100755 src/op_mode/show_openvpn.py diff --git a/op-mode-definitions/openvpn.xml b/op-mode-definitions/openvpn.xml index 2adbfba53..368cc9115 100644 --- a/op-mode-definitions/openvpn.xml +++ b/op-mode-definitions/openvpn.xml @@ -110,6 +110,31 @@ + + + Show OpenVPN information + + + + + Show tunnel status for OpenVPN client interfaces + + sudo ${vyos_op_scripts_dir}/show_openvpn.py --mode=client + + + + Show tunnel status for OpenVPN server interfaces + + sudo ${vyos_op_scripts_dir}/show_openvpn.py --mode=server + + + + Show tunnel status for OpenVPN site-to-site interfaces + + sudo ${vyos_op_scripts_dir}/show_openvpn.py --mode=site-to-site + + + diff --git a/src/op_mode/show_openvpn.py b/src/op_mode/show_openvpn.py new file mode 100755 index 000000000..ee3729fc3 --- /dev/null +++ b/src/op_mode/show_openvpn.py @@ -0,0 +1,170 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2018 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 jinja2 +import argparse + +from vyos.config import Config + +outp_tmpl = """ +{% if clients %} +OpenVPN status on {{ intf }} + +Client CN Remote Host Local Host TX bytes RX bytes Connected Since +--------- ----------- ---------- -------- -------- --------------- +{%- for c in clients %} +{{ "%-15s"|format(c.name) }} {{ "%-21s"|format(c.remote) }} {{ "%-15s"|format(local) }} {{ "%-9s"|format(c.tx_bytes) }} {{ "%-9s"|format(c.tx_bytes) }} {{ c.online_since }} +{%- endfor %} +{% endif %} +""" + +def bytes2HR(size): + # we need to operate in integers + size = int(size) + + suff = ['B', 'KB', 'MB', 'GB', 'TB'] + suffIdx = 0 + + while size > 1024: + # incr. suffix index + suffIdx += 1 + # divide + size = size/1024.0 + + output="{0:.1f} {1}".format(size, suff[suffIdx]) + return output + +def get_status(mode, interface): + status_file = '/opt/vyatta/etc/openvpn/status/{}.status'.format(interface) + # this is an empirical value - I assume we have no more then 999999 + # current OpenVPN connections + routing_table_line = 999999 + + data = { + 'mode': mode, + 'intf': interface, + 'local': '', + 'date': '', + 'clients': [], + } + + with open(status_file, 'r') as f: + lines = f.readlines() + for line_no, line in enumerate(lines): + # remove trailing newline character first + line = line.rstrip('\n') + + # check first line header + if line_no == 0: + if mode == 'server': + if not line == 'OpenVPN CLIENT LIST': + raise NameError('Expected "OpenVPN CLIENT LIST"') + else: + if not line == 'OpenVPN STATISTICS': + raise NameError('Expected "OpenVPN STATISTICS"') + + continue + + # second line informs us when the status file has been last updated + if line_no == 1: + data['date'] = line.lstrip('Updated,').rstrip('\n') + continue + + if mode == 'server': + # followed by line3 giving output information and the actual output data + # + # Common Name,Real Address,Bytes Received,Bytes Sent,Connected Since + # client1,172.18.202.10:55904,2880587,2882653,Fri Aug 23 16:25:48 2019 + # client3,172.18.204.10:41328,2850832,2869729,Fri Aug 23 16:25:43 2019 + # client2,172.18.203.10:48987,2856153,2871022,Fri Aug 23 16:25:45 2019 + if (line_no >= 3) and (line_no < routing_table_line): + # indicator that there are no more clients and we will continue with the + # routing table + if line == 'ROUTING TABLE': + routing_table_line = line_no + continue + + client = { + 'name': line.split(',')[0], + 'remote': line.split(',')[1], + 'rx_bytes': bytes2HR(line.split(',')[2]), + 'tx_bytes': bytes2HR(line.split(',')[3]), + 'online_since': line.split(',')[4] + } + + data['clients'].append(client) + continue + else: + if line_no == 2: + client = { + 'name': 'N/A', + 'rx_bytes': bytes2HR(line.split(',')[1]), + 'tx_bytes': '', + 'online_since': 'N/A' + } + continue + + if line_no == 3: + client['tx_bytes'] = bytes2HR(line.split(',')[1]) + data['clients'].append(client) + break + + return data + +if __name__ == '__main__': + parser = argparse.ArgumentParser() + parser.add_argument('-m', '--mode', help='OpenVPN operation mode (server, client, site-2-site)', required=True) + + args = parser.parse_args() + + # Do nothing if service is not configured + config = Config() + if len(config.list_effective_nodes('interfaces openvpn')) == 0: + print("No OpenVPN interfaces configured") + sys.exit(0) + + # search all OpenVPN interfaces and add those with a matching mode to our + # interfaces list + interfaces = [] + for intf in config.list_effective_nodes('interfaces openvpn'): + # get interface type (server, client, site-to-site) + mode = config.return_effective_value('interfaces openvpn {} mode'.format(intf)) + if args.mode == mode: + interfaces.append(intf) + + for intf in interfaces: + data = get_status(args.mode, intf) + local_host = config.return_effective_value('interfaces openvpn {} local-host'.format(intf)) + local_port = config.return_effective_value('interfaces openvpn {} local-port'.format(intf)) + data['local'] = local_host + ':' + local_port + + if args.mode in ['client', 'site-to-site']: + for client in data['clients']: + if config.exists_effective('interfaces openvpn {} shared-secret-key-file'.format(intf)): + client['name'] = "None (PSK)" + + remote_host = config.return_effective_values('interfaces openvpn {} remote-host'.format(intf)) + remote_port = config.return_effective_value('interfaces openvpn {} remote-port'.format(intf)) + if len(remote_host) >= 1: + client['remote'] = str(remote_host[0]) + ':' + remote_port + else: + client['remote'] = 'N/A' + + + tmpl = jinja2.Template(outp_tmpl) + print(tmpl.render(data)) + -- cgit v1.2.3 From 37e4d24f3679c2f24513cf5dcfbeb761d5bf701d Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Mon, 26 Aug 2019 13:35:36 +0200 Subject: openvpn: T1548: add missing if statement in 'show openvpn' command --- src/op_mode/show_openvpn.py | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/src/op_mode/show_openvpn.py b/src/op_mode/show_openvpn.py index ee3729fc3..23a8156ec 100755 --- a/src/op_mode/show_openvpn.py +++ b/src/op_mode/show_openvpn.py @@ -24,10 +24,10 @@ outp_tmpl = """ {% if clients %} OpenVPN status on {{ intf }} -Client CN Remote Host Local Host TX bytes RX bytes Connected Since ---------- ----------- ---------- -------- -------- --------------- +Client CN Remote Host Local Host TX bytes RX bytes Connected Since +--------- ----------- ---------- -------- -------- --------------- {%- for c in clients %} -{{ "%-15s"|format(c.name) }} {{ "%-21s"|format(c.remote) }} {{ "%-15s"|format(local) }} {{ "%-9s"|format(c.tx_bytes) }} {{ "%-9s"|format(c.tx_bytes) }} {{ c.online_since }} +{{ "%-15s"|format(c.name) }} {{ "%-21s"|format(c.remote) }} {{ "%-21s"|format(local) }} {{ "%-9s"|format(c.tx_bytes) }} {{ "%-9s"|format(c.tx_bytes) }} {{ c.online_since }} {%- endfor %} {% endif %} """ @@ -57,7 +57,7 @@ def get_status(mode, interface): data = { 'mode': mode, 'intf': interface, - 'local': '', + 'local': 'N/A', 'date': '', 'clients': [], } @@ -112,6 +112,7 @@ def get_status(mode, interface): if line_no == 2: client = { 'name': 'N/A', + 'remote': 'N/A', 'rx_bytes': bytes2HR(line.split(',')[1]), 'tx_bytes': '', 'online_since': 'N/A' @@ -150,7 +151,8 @@ if __name__ == '__main__': data = get_status(args.mode, intf) local_host = config.return_effective_value('interfaces openvpn {} local-host'.format(intf)) local_port = config.return_effective_value('interfaces openvpn {} local-port'.format(intf)) - data['local'] = local_host + ':' + local_port + if local_host and local_port: + data['local'] = local_host + ':' + local_port if args.mode in ['client', 'site-to-site']: for client in data['clients']: @@ -161,9 +163,6 @@ if __name__ == '__main__': remote_port = config.return_effective_value('interfaces openvpn {} remote-port'.format(intf)) if len(remote_host) >= 1: client['remote'] = str(remote_host[0]) + ':' + remote_port - else: - client['remote'] = 'N/A' - tmpl = jinja2.Template(outp_tmpl) print(tmpl.render(data)) -- cgit v1.2.3 From d5e9512b8461f55d276182b4a75267378aa11f50 Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Mon, 26 Aug 2019 16:19:31 +0200 Subject: bridge: T1556: reword exception error when beeing member of multiple bridges --- src/conf_mode/interface-bridge.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/conf_mode/interface-bridge.py b/src/conf_mode/interface-bridge.py index 65b5c4066..fc1243867 100755 --- a/src/conf_mode/interface-bridge.py +++ b/src/conf_mode/interface-bridge.py @@ -187,7 +187,8 @@ def verify(bridge): for intf in bridge['member']: tmp = conf.list_nodes('interfaces bridge {} member interface'.format(br)) if intf['name'] in tmp: - raise ConfigError('{} can be assigned to any one bridge only'.format(intf['name'])) + raise ConfigError('Interface "{}" belongs to bridge "{}" and can not be enslaved.'.format(intf['name'], bridge['intf'])) + return None -- cgit v1.2.3 From 4a8ab14dc3cbe4245b95250c51ee427eb6241372 Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Mon, 26 Aug 2019 16:20:03 +0200 Subject: bridge: T1608: deny adding non existing interfaces to bridge config --- src/conf_mode/interface-bridge.py | 5 ++++ src/helpers/vyos-bridge-sync.py | 53 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 58 insertions(+) create mode 100755 src/helpers/vyos-bridge-sync.py diff --git a/src/conf_mode/interface-bridge.py b/src/conf_mode/interface-bridge.py index fc1243867..c5c5bd4ac 100755 --- a/src/conf_mode/interface-bridge.py +++ b/src/conf_mode/interface-bridge.py @@ -23,6 +23,7 @@ import subprocess import vyos.configinterface as VyIfconfig +from netifaces import interfaces from vyos.config import Config from vyos import ConfigError @@ -189,6 +190,10 @@ def verify(bridge): if intf['name'] in tmp: raise ConfigError('Interface "{}" belongs to bridge "{}" and can not be enslaved.'.format(intf['name'], bridge['intf'])) + # the interface must exist prior adding it to a bridge + for intf in bridge['member']: + if intf['name'] not in interfaces(): + raise ConfigError('Can not add non existing interface "{}" to bridge "{}"'.format(intf['name'], bridge['intf'])) return None diff --git a/src/helpers/vyos-bridge-sync.py b/src/helpers/vyos-bridge-sync.py new file mode 100755 index 000000000..495eb5d40 --- /dev/null +++ b/src/helpers/vyos-bridge-sync.py @@ -0,0 +1,53 @@ +#!/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 . +# + +# Script is used to synchronize configured bridge interfaces. +# one can add a non existing interface to a bridge group (e.g. VLAN) +# but the vlan interface itself does yet not exist. It should be added +# to the bridge automatically once it's available + +import argparse +import subprocess + +from sys import exit +from time import sleep +from vyos.config import Config + +def subprocess_cmd(command): + process = subprocess.Popen(command,stdout=subprocess.PIPE, shell=True) + proc_stdout = process.communicate()[0].strip() + pass + +if __name__ == '__main__': + parser = argparse.ArgumentParser() + parser.add_argument('-i', '--interface', action='store', help='Interface name which should be added to bridge it is configured for', required=True) + args, unknownargs = parser.parse_known_args() + + conf = Config() + if not conf.list_nodes('interfaces bridge'): + # no bridge interfaces exist .. bail out early + exit(0) + else: + for bridge in conf.list_nodes('interfaces bridge'): + for member_if in conf.list_nodes('interfaces bridge {} member interface'.format(bridge)): + if args.interface == member_if: + cmd = 'brctl addif "{}" "{}"'.format(bridge, args.interface) + # let interfaces etc. settle - especially required for OpenVPN bridged interfaces + sleep(4) + subprocess_cmd(cmd) + + exit(0) -- 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(-) 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 fdae741be5ffaa3719ce889d0342c3091ad3c92c Mon Sep 17 00:00:00 2001 From: Daniil Baturin Date: Mon, 26 Aug 2019 19:58:39 +0200 Subject: T1598: make dns_forwarding.py retrieve name servers from vyos-hostsd. --- src/conf_mode/dns_forwarding.py | 23 ++++++++--------------- 1 file changed, 8 insertions(+), 15 deletions(-) diff --git a/src/conf_mode/dns_forwarding.py b/src/conf_mode/dns_forwarding.py index 9e81c7294..38f3cb4de 100755 --- a/src/conf_mode/dns_forwarding.py +++ b/src/conf_mode/dns_forwarding.py @@ -24,6 +24,7 @@ import jinja2 import netifaces import vyos.util +import vyos.hostsd_client from vyos.config import Config from vyos import ConfigError @@ -94,19 +95,6 @@ default_config_data = { } -# borrowed from: https://github.com/donjajo/py-world/blob/master/resolvconfReader.py, THX! -def get_resolvers(file): - try: - with open(file, 'r') as resolvconf: - lines = [line.split('#', 1)[0].rstrip() - for line in resolvconf.readlines()] - resolvers = [line.split()[1] - for line in lines if 'nameserver' in line] - return resolvers - except IOError: - return [] - - def get_config(arguments): dns = default_config_data conf = Config() @@ -171,11 +159,16 @@ def get_config(arguments): if conf.exists('dhcp'): interfaces = [] interfaces = conf.return_values('dhcp') + hc = vyos.hostsd_client.Client() + for interface in interfaces: - dhcp_resolvers = get_resolvers( - "/etc/resolv.conf.dhclient-new-{0}".format(interface)) + dhcp_resolvers = hc.get_name_servers("dhcp-{0}".format(interface)) + dhcpv6_resolvers = hc.get_name_servers("dhcpv6-{0}".format(interface)) + if dhcp_resolvers: dns['name_servers'] = dns['name_servers'] + dhcp_resolvers + if dhcpv6_resolvers: + dns['name_servers'] = dns['name_servers'] + dhcpv6_resolvers return dns -- cgit v1.2.3 From 7c79684a931b6bff3abb0cb8fb75c72cb916f1ce Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Mon, 26 Aug 2019 20:54:03 +0200 Subject: bridge: T1556: bugfix: disable node must be valueless --- interface-definitions/interfaces-bridge.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/interface-definitions/interfaces-bridge.xml b/interface-definitions/interfaces-bridge.xml index adb525a46..14887ee02 100644 --- a/interface-definitions/interfaces-bridge.xml +++ b/interface-definitions/interfaces-bridge.xml @@ -117,6 +117,7 @@ Disable this bridge interface + -- cgit v1.2.3 From 727ece3b4a9d1d5fced67f0e97fb22e2671eaabc Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Mon, 26 Aug 2019 22:28:00 +0200 Subject: bridge: T1556: bugfix: aging range validator --- interface-definitions/interfaces-bridge.xml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/interface-definitions/interfaces-bridge.xml b/interface-definitions/interfaces-bridge.xml index 14887ee02..e92a55d63 100644 --- a/interface-definitions/interfaces-bridge.xml +++ b/interface-definitions/interfaces-bridge.xml @@ -57,8 +57,7 @@ Address aging time for bridge seconds (default 300) - - + -- cgit v1.2.3 From 6e36bafad6d8300b0bd90261f2a57cf65716ac7f Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Mon, 26 Aug 2019 22:29:16 +0200 Subject: bridge: T1556: migrate interface configuration to pyroute2 Tested with: set interfaces bridge br0 address '192.0.2.1/24' set interfaces bridge br0 aging '500' set interfaces bridge br0 disable-link-detect set interfaces bridge br0 forwarding-delay '11' set interfaces bridge br0 hello-time '5' set interfaces bridge br0 igmp querier set interfaces bridge br0 max-age '11' set interfaces bridge br0 member interface eth1 cost '1000' set interfaces bridge br0 member interface eth1 priority '4' set interfaces bridge br0 member interface eth2 cost '1001' set interfaces bridge br0 member interface eth2 priority '56' --- debian/control | 1 + src/conf_mode/interface-bridge.py | 238 +++++++++++++++++--------------------- 2 files changed, 109 insertions(+), 130 deletions(-) diff --git a/debian/control b/debian/control index 12eb7c309..7b75ca111 100644 --- a/debian/control +++ b/debian/control @@ -29,6 +29,7 @@ Depends: python3, python3-vici (>= 5.7.2), python3-bottle, python3-zmq, + python3-pyroute2, ipaddrcheck, tcpdump, tshark, diff --git a/src/conf_mode/interface-bridge.py b/src/conf_mode/interface-bridge.py index c5c5bd4ac..d5ef85940 100755 --- a/src/conf_mode/interface-bridge.py +++ b/src/conf_mode/interface-bridge.py @@ -16,13 +16,10 @@ # # -import os -import sys -import copy -import subprocess - -import vyos.configinterface as VyIfconfig - +from os import environ +from copy import deepcopy +from sys import exit +from pyroute2 import IPDB from netifaces import interfaces from vyos.config import Config from vyos import ConfigError @@ -30,44 +27,42 @@ from vyos import ConfigError default_config_data = { 'address': [], 'address_remove': [], - 'aging': '300', - 'arp_cache_timeout_ms': '30000', + 'aging': 300, + 'arp_cache_timeout_ms': 30000, 'description': '', 'deleted': False, - 'dhcp_client_id': '', - 'dhcp_hostname': '', - 'dhcpv6_parameters_only': False, - 'dhcpv6_temporary': False, 'disable': False, - 'disable_link_detect': False, - 'forwarding_delay': '15', - 'hello_time': '2', + 'disable_link_detect': 1, + 'forwarding_delay': 14, + 'hello_time': 2, 'igmp_querier': 0, 'intf': '', 'mac' : '', - 'max_age': '20', + 'max_age': 20, 'member': [], 'member_remove': [], - 'priority': '32768', - 'stp': 'off' + 'priority': 32768, + 'stp': 0 } -def subprocess_cmd(command): - process = subprocess.Popen(command,stdout=subprocess.PIPE, shell=True) - proc_stdout = process.communicate()[0].strip() - pass +def freeze(d): + if isinstance(d, dict): + return frozenset((key, freeze(value)) for key, value in d.items()) + elif isinstance(d, list): + return tuple(freeze(value) for value in d) + return d def diff(first, second): second = set(second) return [item for item in first if item not in second] def get_config(): - bridge = copy.deepcopy(default_config_data) + bridge = deepcopy(default_config_data) conf = Config() # determine tagNode instance try: - bridge['intf'] = os.environ['VYOS_TAGNODE_VALUE'] + bridge['intf'] = environ['VYOS_TAGNODE_VALUE'] except KeyError as E: print("Interface not specified") @@ -85,27 +80,13 @@ def get_config(): # retrieve aging - how long addresses are retained if conf.exists('aging'): - bridge['aging'] = conf.return_value('aging') + bridge['aging'] = int(conf.return_value('aging')) # retrieve interface description if conf.exists('description'): bridge['description'] = conf.return_value('description') - - # DHCP client identifier - if conf.exists('dhcp-options client-id'): - bridge['dhcp_client_id'] = conf.return_value('dhcp-options client-id') - - # DHCP client hostname - if conf.exists('dhcp-options host-name'): - bridge['dhcp_hostname'] = conf.return_value('dhcp-options host-name') - - # DHCPv6 acquire only config parameters, no address - if conf.exists('dhcpv6-options parameters-only'): - bridge['dhcpv6_parameters_only'] = True - - # DHCPv6 IPv6 "temporary" address - if conf.exists('dhcpv6-options temporary'): - bridge['dhcpv6_temporary'] = True + else: + bridge['description'] = bridge['intf'] # Disable this bridge interface if conf.exists('disable'): @@ -113,15 +94,15 @@ def get_config(): # Ignore link state changes if conf.exists('disable-link-detect'): - bridge['disable_link_detect'] = True + bridge['disable_link_detect'] = 2 # Forwarding delay if conf.exists('forwarding-delay'): - bridge['forwarding_delay'] = conf.return_value('forwarding-delay') + bridge['forwarding_delay'] = int(conf.return_value('forwarding-delay')) # Hello packet advertisment interval if conf.exists('hello-time'): - bridge['hello_time'] = conf.return_value('hello-time') + bridge['hello_time'] = int(conf.return_value('hello-time')) # Enable Internet Group Management Protocol (IGMP) querier if conf.exists('igmp querier'): @@ -129,8 +110,7 @@ def get_config(): # ARP cache entry timeout in seconds if conf.exists('ip arp-cache-timeout'): - tmp = 1000 * int(conf.return_value('ip arp-cache-timeout')) - bridge['arp_cache_timeout_ms'] = str(tmp) + bridge['arp_cache_timeout_ms'] = int(conf.return_value('ip arp-cache-timeout')) * 1000 # Media Access Control (MAC) address if conf.exists('mac'): @@ -138,21 +118,24 @@ def get_config(): # Interval at which neighbor bridges are removed if conf.exists('max-age'): - bridge['max_age'] = conf.return_value('max-age') + bridge['max_age'] = int(conf.return_value('max-age')) # Determine bridge member interface (currently configured) for intf in conf.list_nodes('member interface'): + # cost and priority initialized with linux defaults + # by reading /sys/devices/virtual/net/br0/brif/eth2/{path_cost,priority} + # after adding interface to bridge after reboot iface = { 'name': intf, - 'cost': '', - 'priority': '' + 'cost': 100, + 'priority': 32 } if conf.exists('member interface {} cost'.format(intf)): - iface['cost'] = conf.return_value('member interface {} cost'.format(intf)) + iface['cost'] = int(conf.return_value('member interface {} cost'.format(intf))) if conf.exists('member interface {} priority'.format(intf)): - iface['priority'] = conf.return_value('member interface {} priority'.format(intf)) + iface['priority'] = int(conf.return_value('member interface {} priority'.format(intf))) bridge['member'].append(iface) @@ -170,11 +153,11 @@ def get_config(): # Priority for this bridge if conf.exists('priority'): - bridge['priority'] = conf.return_value('priority') + bridge['priority'] = int(conf.return_value('priority')) # Enable spanning tree protocol if conf.exists('stp'): - bridge['stp'] = 'on' + bridge['stp'] = 1 return bridge @@ -201,94 +184,89 @@ def generate(bridge): return None def apply(bridge): - cmd = '' - if bridge['deleted']: - # bridges need to be shutdown first - cmd += 'ip link set dev "{}" down'.format(bridge['intf']) - cmd += ' && ' - # delete bridge - cmd += 'brctl delbr "{}"'.format(bridge['intf']) - subprocess_cmd(cmd) + ipdb = IPDB(mode='explicit') + brif = bridge['intf'] + if bridge['deleted']: + try: + # delete bridge interface + with ipdb.interfaces[ brif ] as br: + br.remove() + except: + pass else: - # create bridge if it does not exist - if not os.path.exists("/sys/class/net/" + bridge['intf']): - # create bridge interface - cmd += 'brctl addbr "{}"'.format(bridge['intf']) - cmd += ' && ' - # activate "UP" the interface - cmd += 'ip link set dev "{}" up'.format(bridge['intf']) - cmd += ' && ' - - # set ageing time - cmd += 'brctl setageing "{}" "{}"'.format(bridge['intf'], bridge['aging']) - cmd += ' && ' - - # set bridge forward delay - cmd += 'brctl setfd "{}" "{}"'.format(bridge['intf'], bridge['forwarding_delay']) - cmd += ' && ' - - # set hello time - cmd += 'brctl sethello "{}" "{}"'.format(bridge['intf'], bridge['hello_time']) - cmd += ' && ' - - # set max message age - cmd += 'brctl setmaxage "{}" "{}"'.format(bridge['intf'], bridge['max_age']) - cmd += ' && ' - + try: + # create bridge interface if it not already exists + ipdb.create(kind='bridge', ifname=brif).commit() + except: + pass + + # get handle in bridge interface + br = ipdb.interfaces[brif] + # begin() a transaction prior to make any change + br.begin() + # enable interface + br.up() + # set ageing time - - value is in centiseconds YES! centiseconds! + br.br_ageing_time = bridge['aging'] * 100 + # set bridge forward delay - value is in centiseconds YES! centiseconds! + br.br_forward_delay = bridge['forwarding_delay'] * 100 + # set hello time - value is in centiseconds YES! centiseconds! + br.br_hello_time = bridge['hello_time'] * 100 + # set max message age - value is in centiseconds YES! centiseconds! + br.br_max_age = bridge['max_age'] * 100 # set bridge priority - cmd += 'brctl setbridgeprio "{}" "{}"'.format(bridge['intf'], bridge['priority']) - cmd += ' && ' - + br.br_priority = bridge['priority'] # turn stp on/off - cmd += 'brctl stp "{}" "{}"'.format(bridge['intf'], bridge['stp']) - - for intf in bridge['member_remove']: - # remove interface from bridge - cmd += ' && ' - cmd += 'brctl delif "{}" "{}"'.format(bridge['intf'], intf) - - for intf in bridge['member']: - # add interface to bridge - # but only if it is not yet member of this bridge - if not os.path.exists('/sys/devices/virtual/net/' + bridge['intf'] + '/brif/' + intf['name']): - cmd += ' && ' - cmd += 'brctl addif "{}" "{}"'.format(bridge['intf'], intf['name']) - - # set bridge port cost - if intf['cost']: - cmd += ' && ' - cmd += 'brctl setpathcost "{}" "{}" "{}"'.format(bridge['intf'], intf['name'], intf['cost']) - - # set bridge port priority - if intf['priority']: - cmd += ' && ' - cmd += 'brctl setportprio "{}" "{}" "{}"'.format(bridge['intf'], intf['name'], intf['priority']) - - subprocess_cmd(cmd) + br.br_stp_state = bridge['stp'] + # enable or disable IGMP querier + br.br_mcast_querier = bridge['igmp_querier'] + # update interface description used e.g. within SNMP + br.ifalias = bridge['description'] # Change interface MAC address if bridge['mac']: - VyIfconfig.set_mac_address(bridge['intf'], bridge['mac']) + br.set_address = bridge['mac'] - # update interface description used e.g. within SNMP - VyIfconfig.set_description(bridge['intf'], bridge['description']) - - # Ignore link state changes? - VyIfconfig.set_link_detect(bridge['intf'], bridge['disable_link_detect']) - - # enable or disable IGMP querier - VyIfconfig.set_multicast_querier(bridge['intf'], bridge['igmp_querier']) + # remove interface from bridge + for intf in bridge['member_remove']: + br.del_port( intf['name'] ) - # ARP cache entry timeout in seconds - VyIfconfig.set_arp_cache_timeout(bridge['intf'], bridge['arp_cache_timeout_ms']) + # configure bridge member interfaces + for member in bridge['member']: + # add interface + br.add_port(member['name']) # Configure interface address(es) for addr in bridge['address_remove']: - VyIfconfig.remove_interface_address(bridge['intf'], addr) - + br.del_ip(addr) for addr in bridge['address']: - VyIfconfig.add_interface_address(bridge['intf'], addr) + br.add_ip(addr) + + # up/down interface + if bridge['disable']: + br.down() + + # commit change son bridge interface + br.commit() + + # configure additional bridge member options + for member in bridge['member']: + # configure ARP cache timeout in milliseconds + with open('/proc/sys/net/ipv4/neigh/' + member['name'] + '/base_reachable_time_ms', 'w') as f: + f.write(str(bridge['arp_cache_timeout_ms'])) + # ignore link state changes + with open('/proc/sys/net/ipv4/conf/' + member['name'] + '/link_filter', 'w') as f: + f.write(str(bridge['disable_link_detect'])) + + # adjust member port stp attributes + member_if = ipdb.interfaces[ member['name'] ] + member_if.begin() + # set bridge port cost + member_if.brport_cost = member['cost'] + # set bridge port priority + member_if.brport_priority = member['priority'] + member_if.commit() return None @@ -300,4 +278,4 @@ if __name__ == '__main__': apply(c) except ConfigError as e: print(e) - sys.exit(1) + exit(1) -- cgit v1.2.3 From 3dbb258f5f02076aa96bb9cd840464bcf08725ab Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Mon, 26 Aug 2019 22:39:37 +0200 Subject: bridge: T1556: fix comment --- src/conf_mode/interface-bridge.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/conf_mode/interface-bridge.py b/src/conf_mode/interface-bridge.py index d5ef85940..85ea68e26 100755 --- a/src/conf_mode/interface-bridge.py +++ b/src/conf_mode/interface-bridge.py @@ -247,7 +247,7 @@ def apply(bridge): if bridge['disable']: br.down() - # commit change son bridge interface + # commit changes on bridge interface br.commit() # configure additional bridge member options -- cgit v1.2.3 From 0e1f53c5fb810d54a3c64e09d86b955ddbcbce87 Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Mon, 26 Aug 2019 22:41:39 +0200 Subject: loopback: T1601: migrate to pyroute2 --- src/conf_mode/interface-loopback.py | 42 ++++++++++++++++++++++--------------- 1 file changed, 25 insertions(+), 17 deletions(-) diff --git a/src/conf_mode/interface-loopback.py b/src/conf_mode/interface-loopback.py index 445a9af64..5c1419b11 100755 --- a/src/conf_mode/interface-loopback.py +++ b/src/conf_mode/interface-loopback.py @@ -14,14 +14,11 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . # -# - -import os -import sys -import copy - -import vyos.configinterface as VyIfconfig +from os import environ +from sys import exit +from copy import deepcopy +from pyroute2 import IPDB from vyos.config import Config from vyos import ConfigError @@ -37,12 +34,12 @@ def diff(first, second): return [item for item in first if item not in second] def get_config(): - loopback = copy.deepcopy(default_config_data) + loopback = deepcopy(default_config_data) conf = Config() # determine tagNode instance try: - loopback['intf'] = os.environ['VYOS_TAGNODE_VALUE'] + loopback['intf'] = environ['VYOS_TAGNODE_VALUE'] except KeyError as E: print("Interface not specified") @@ -60,6 +57,8 @@ def get_config(): # retrieve interface description if conf.exists('description'): loopback['description'] = conf.return_value('description') + else: + loopback['description'] = loopback['intf'] # Determine interface addresses (currently effective) - to determine which # address is no longer valid and needs to be removed from the interface @@ -76,19 +75,28 @@ def generate(loopback): return None def apply(loopback): - # Remove loopback interface + 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() + if not loopback['deleted']: # update interface description used e.g. within SNMP - VyIfconfig.set_description(loopback['intf'], loopback['description']) - - # Configure interface address(es) + # update interface description used e.g. within SNMP + lo.ifalias = loopback['description'] + # configure interface address(es) for addr in loopback['address']: - VyIfconfig.add_interface_address(loopback['intf'], addr) + lo.add_ip(addr) - # Remove interface address(es) + # remove interface address(es) for addr in loopback['address_remove']: - VyIfconfig.remove_interface_address(loopback['intf'], addr) + lo.del_ip(addr) + # commit changes on loopback interface + lo.commit() return None if __name__ == '__main__': @@ -99,4 +107,4 @@ if __name__ == '__main__': apply(c) except ConfigError as e: print(e) - sys.exit(1) + exit(1) -- 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 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 93184326fc3768216b734a5fcc60e193b5e27fad Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Tue, 27 Aug 2019 11:10:32 +0200 Subject: dummy: T1580: migrate implementation to pyroute2 --- src/conf_mode/interface-dummy.py | 64 +++++++++++++++++++++++++--------------- 1 file changed, 40 insertions(+), 24 deletions(-) diff --git a/src/conf_mode/interface-dummy.py b/src/conf_mode/interface-dummy.py index c7c5ac99c..d8a36a5b2 100755 --- a/src/conf_mode/interface-dummy.py +++ b/src/conf_mode/interface-dummy.py @@ -16,11 +16,10 @@ # # -import os -import sys -import copy - -from vyos.interfaceconfig import Interface +from os import environ +from copy import deepcopy +from sys import exit +from pyroute2 import IPDB from vyos.config import Config from vyos import ConfigError @@ -38,12 +37,12 @@ def diff(first, second): return [item for item in first if item not in second] def get_config(): - dummy = copy.deepcopy(default_config_data) + dummy = deepcopy(default_config_data) conf = Config() # determine tagNode instance try: - dummy['intf'] = os.environ['VYOS_TAGNODE_VALUE'] + dummy['intf'] = environ['VYOS_TAGNODE_VALUE'] except KeyError as E: print("Interface not specified") @@ -62,6 +61,8 @@ def get_config(): # retrieve interface description if conf.exists('description'): dummy['description'] = conf.return_value('description') + else: + dummy['description'] = dummy['intf'] # Disable this interface if conf.exists('disable'): @@ -82,30 +83,45 @@ def generate(dummy): return None def apply(dummy): + ipdb = IPDB(mode='explicit') + dummyif = dummy['intf'] + # Remove dummy interface if dummy['deleted']: - Interface(dummy['intf']).remove_interface() + try: + # delete dummy interface + with ipdb.interface[ dummyif ] as du: + du.remove() + except: + pass else: - # Interface will only be added if it yet does not exist - Interface(dummy['intf'], 'dummy') - + try: + # create dummy interface if it's non existing + ipdb.create(kind='dummy', ifname=dummyif).commit() + except: + pass + + # retrieve handle to dummy interface + du = ipdb.interfaces[dummyif] + # begin a transaction prior to make any change + du.begin() + # enable interface + du.up() # update interface description used e.g. within SNMP - if dummy['description']: - Interface(dummy['intf']).ifalias = dummy['description'] + du.ifalias = dummy['description'] # Configure interface address(es) - if len(dummy['address_remove']) > 0: - Interface(dummy['intf']).del_addr(dummy['address_remove']) - - 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) + for addr in dummy['address_remove']: + du.del_ip(addr) + for addr in dummy['address']: + du.add_ip(addr) + # disable interface on demand if dummy['disable']: - Interface(dummy['intf']).linkstate = 'down' - else: - Interface(dummy['intf']).linkstate = 'up' + du.down() + + # commit changes on bridge interface + du.commit() return None @@ -117,4 +133,4 @@ if __name__ == '__main__': apply(c) except ConfigError as e: print(e) - sys.exit(1) + exit(1) -- cgit v1.2.3 From cd9bb2963e9e7c30d3bbebce9d4ef170107533a9 Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Tue, 27 Aug 2019 11:55:16 +0200 Subject: list-interfaces: T1614: support listing interfaces which can be bonded --- src/completion/list_interfaces.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/src/completion/list_interfaces.py b/src/completion/list_interfaces.py index 66432af19..5e444ef78 100755 --- a/src/completion/list_interfaces.py +++ b/src/completion/list_interfaces.py @@ -2,15 +2,14 @@ import sys import argparse - import vyos.interfaces - parser = argparse.ArgumentParser() group = parser.add_mutually_exclusive_group() group.add_argument("-t", "--type", type=str, help="List interfaces of specific type") group.add_argument("-b", "--broadcast", action="store_true", help="List all broadcast interfaces") group.add_argument("-br", "--bridgeable", action="store_true", help="List all bridgeable interfaces") +group.add_argument("-bo", "--bondable", action="store_true", help="List all bondable interfaces") args = parser.parse_args() @@ -21,11 +20,13 @@ if args.type: except ValueError as e: print(e, file=sys.stderr) print("") + elif args.broadcast: eth = vyos.interfaces.list_interfaces_of_type("ethernet") bridge = vyos.interfaces.list_interfaces_of_type("bridge") bond = vyos.interfaces.list_interfaces_of_type("bonding") interfaces = eth + bridge + bond + elif args.bridgeable: eth = vyos.interfaces.list_interfaces_of_type("ethernet") bond = vyos.interfaces.list_interfaces_of_type("bonding") @@ -34,6 +35,15 @@ elif args.bridgeable: vxlan = vyos.interfaces.list_interfaces_of_type("vxlan") wireless = vyos.interfaces.list_interfaces_of_type("wireless") interfaces = eth + bond + l2tpv3 + openvpn + vxlan + wireless + +elif args.bondable: + eth = vyos.interfaces.list_interfaces_of_type("ethernet") + # we need to filter out VLAN interfaces identified by a dot (.) in their name + for intf in eth: + if '.' in intf: + eth.remove(intf) + interfaces = eth + else: interfaces = vyos.interfaces.list_interfaces() -- cgit v1.2.3 From 343a48be36b1573286db8f944aa933911b09530b Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Tue, 27 Aug 2019 12:24:09 +0200 Subject: bridge: T1556: remove unused function freeze() --- src/conf_mode/interface-bridge.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/src/conf_mode/interface-bridge.py b/src/conf_mode/interface-bridge.py index 85ea68e26..c1527e4a7 100755 --- a/src/conf_mode/interface-bridge.py +++ b/src/conf_mode/interface-bridge.py @@ -45,13 +45,6 @@ default_config_data = { 'stp': 0 } -def freeze(d): - if isinstance(d, dict): - return frozenset((key, freeze(value)) for key, value in d.items()) - elif isinstance(d, list): - return tuple(freeze(value) for value in d) - return d - def diff(first, second): second = set(second) return [item for item in first if item not in second] -- 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(-) 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(+) 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 e4f1bbb270f0afea295646764516675bbcfe0be5 Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Tue, 27 Aug 2019 22:18:41 +0200 Subject: openvpn: T1617: bugfix for server push-route --- src/conf_mode/interface-openvpn.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/conf_mode/interface-openvpn.py b/src/conf_mode/interface-openvpn.py index a8313378b..4e5915d4e 100755 --- a/src/conf_mode/interface-openvpn.py +++ b/src/conf_mode/interface-openvpn.py @@ -522,9 +522,9 @@ def get_config(): # Route to be pushed to all clients if conf.exists('server push-route'): - network = conf.return_value('server push-route') - tmp = IPv4Interface(network).with_netmask - openvpn['server_push_route'] = tmp.replace(r'/', ' ') + for network in conf.return_values('server push-route'): + tmp = IPv4Interface(network).with_netmask + openvpn['server_push_route'].append(tmp.replace(r'/', ' ')) # Reject connections from clients that are not explicitly configured if conf.exists('server reject-unconfigured-clients'): -- cgit v1.2.3 From 71f7a947539963112c61fef2a5f278d524d71198 Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Wed, 28 Aug 2019 10:59:09 +0200 Subject: bridge: T1615: add missing support for DHCP/DHCPv6 interface address This feature is not well supported by pyroute2 and thus uses the proof-of-concept vyos.interfaceconfig library. Maybe it's a better idea to write our own library from scratch. --- src/conf_mode/interface-bridge.py | 38 ++++++++++++++++++++++++++++++++------ 1 file changed, 32 insertions(+), 6 deletions(-) diff --git a/src/conf_mode/interface-bridge.py b/src/conf_mode/interface-bridge.py index c1527e4a7..9918cbec7 100755 --- a/src/conf_mode/interface-bridge.py +++ b/src/conf_mode/interface-bridge.py @@ -22,6 +22,8 @@ from sys import exit 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 import ConfigError default_config_data = { @@ -62,7 +64,9 @@ def get_config(): # Check if bridge has been removed if not conf.exists('interfaces bridge ' + bridge['intf']): bridge['deleted'] = True - return bridge + # 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 # set new configuration level conf.set_level('interfaces bridge ' + bridge['intf']) @@ -185,6 +189,13 @@ def apply(bridge): # delete bridge interface with ipdb.interfaces[ brif ] as br: br.remove() + + # stop DHCP(v6) clients if configured + for addr in bridge['address_remove']: + if addr == 'dhcp': + IF(brif).del_dhcpv4() + elif addr == 'dhcpv6': + IF(brif).del_dhcpv6() except: pass else: @@ -225,16 +236,31 @@ def apply(bridge): for intf in bridge['member_remove']: br.del_port( intf['name'] ) - # configure bridge member interfaces + # add interfaces to bridge for member in bridge['member']: - # add interface br.add_port(member['name']) - # Configure interface address(es) + # remove configured network interface addresses/DHCP(v6) configuration for addr in bridge['address_remove']: - br.del_ip(addr) + try: + is_ip(addr) + br.del_ip(addr) + except ValueError: + if addr == 'dhcp': + IF(brif).del_dhcpv4() + elif addr == 'dhcpv6': + IF(brif).del_dhcpv6() + + # add configured network interface addresses/DHCP(v6) configuration for addr in bridge['address']: - br.add_ip(addr) + try: + is_ip(addr) + br.add_ip(addr) + except: + if addr == 'dhcp': + IF(brif).set_dhcpv4() + elif addr == 'dhcpv6': + IF(brif).set_dhcpv6() # up/down interface if bridge['disable']: -- 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(-) 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(-) 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(-) 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(-) 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(-) 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(-) 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(-) 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(-) 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(-) 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(-) 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(-) 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(-) 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(-) 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(-) 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 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 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 ff05e2a90edf8af5d7b8ad5c69cae2dd40af2c8d Mon Sep 17 00:00:00 2001 From: Daniil Baturin Date: Fri, 30 Aug 2019 18:40:44 +0200 Subject: T1598: fix vyos-hostsd unit dependencies. --- src/systemd/vyos-hostsd.service | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/src/systemd/vyos-hostsd.service b/src/systemd/vyos-hostsd.service index 3b0fadb5c..2444f5352 100644 --- a/src/systemd/vyos-hostsd.service +++ b/src/systemd/vyos-hostsd.service @@ -1,8 +1,7 @@ [Unit] Description=VyOS DNS configuration keeper -After=auditd.service time-sync.target -Before=network-pre.target vyos-router.service -Wants=network-pre.target +DefaultDependencies=no +After=systemd-remount-fs.service [Service] ExecStart=/usr/bin/python3 -u /usr/libexec/vyos/services/vyos-hostsd @@ -19,6 +18,4 @@ User=root Group=vyattacfg [Install] -# -WantedBy=network.target - +RequiredBy=cloud-init-local.service vyos-router.service -- 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(-) 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(-) 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 0af7ae0e91a2214f70a415b84273e7b2bba3e757 Mon Sep 17 00:00:00 2001 From: DmitriyEshenko Date: Sat, 31 Aug 2019 00:26:06 +0000 Subject: [l2tp ipsec] T1605: Changed ipsec marking only for inbound policy, all functionality must saved --- src/conf_mode/ipsec-settings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/conf_mode/ipsec-settings.py b/src/conf_mode/ipsec-settings.py index 8d25e7abd..156bb2edd 100755 --- a/src/conf_mode/ipsec-settings.py +++ b/src/conf_mode/ipsec-settings.py @@ -62,7 +62,7 @@ conn {{ra_conn_name}} left={{outside_addr}} leftsubnet=%dynamic[/1701] rightsubnet=%dynamic - mark=%unique + mark_in=%unique auto=add ike=aes256-sha1-modp1024,3des-sha1-modp1024,3des-sha1-modp1024! dpddelay=15 -- cgit v1.2.3 From 3b119c91ca70c51aab24d4ef8b3913f47281321a Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Sat, 31 Aug 2019 12:43:52 +0200 Subject: bridge: T1556: increase max-age range to 1200 (30 minutes) --- interface-definitions/interfaces-bridge.xml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/interface-definitions/interfaces-bridge.xml b/interface-definitions/interfaces-bridge.xml index e92a55d63..98998bfa1 100644 --- a/interface-definitions/interfaces-bridge.xml +++ b/interface-definitions/interfaces-bridge.xml @@ -191,13 +191,13 @@ Interval at which neighbor bridges are removed - 1-40 + 1-1200 Bridge maximum aging time in seconds (default 20) - + - Bridge max aging value must be between 1 and 40 seconds + Bridge max aging value must be between 1 and 1200 seconds -- 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(-) 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 04b192c7d1949a6c8223051f144287ac8dbbd1ec Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Sat, 31 Aug 2019 13:02:04 +0200 Subject: bridge: T1615: replace pyroute2 by vyos.ifconfig --- src/conf_mode/interface-bridge.py | 127 ++++++++++++++------------------------ 1 file changed, 47 insertions(+), 80 deletions(-) diff --git a/src/conf_mode/interface-bridge.py b/src/conf_mode/interface-bridge.py index d5661be93..187be677e 100755 --- a/src/conf_mode/interface-bridge.py +++ b/src/conf_mode/interface-bridge.py @@ -16,21 +16,21 @@ # # -from os import environ +import os + from copy import deepcopy from sys import exit -from pyroute2 import IPDB from netifaces import interfaces from vyos.config import Config from vyos.validate import is_ip -from vyos.ifconfig import Interface as IF +from vyos.ifconfig import BridgeIf, Interface from vyos import ConfigError default_config_data = { 'address': [], 'address_remove': [], 'aging': 300, - 'arp_cache_timeout_ms': 30000, + 'arp_cache_tmo': 30, 'description': '', 'deleted': False, 'disable': False, @@ -57,7 +57,7 @@ def get_config(): # determine tagNode instance try: - bridge['intf'] = environ['VYOS_TAGNODE_VALUE'] + bridge['intf'] = os.environ['VYOS_TAGNODE_VALUE'] except KeyError as E: print("Interface not specified") @@ -107,7 +107,7 @@ def get_config(): # ARP cache entry timeout in seconds if conf.exists('ip arp-cache-timeout'): - bridge['arp_cache_timeout_ms'] = int(conf.return_value('ip arp-cache-timeout')) * 1000 + bridge['arp_cache_tmo'] = int(conf.return_value('ip arp-cache-timeout')) # Media Access Control (MAC) address if conf.exists('mac'): @@ -181,56 +181,35 @@ def generate(bridge): return None def apply(bridge): - ipdb = IPDB(mode='explicit') - brif = bridge['intf'] + br = BridgeIf(bridge['intf']) if bridge['deleted']: - try: - # delete bridge interface - with ipdb.interfaces[ brif ] as br: - br.remove() - - # stop DHCP(v6) clients if configured - for addr in bridge['address_remove']: - if addr == 'dhcp': - IF(brif).del_dhcpv4() - elif addr == 'dhcpv6': - IF(brif).del_dhcpv6() - except: - pass + # delete bridge interface + # DHCP is stopped inside remove() + br.remove() else: - try: - # create bridge interface if it not already exists - ipdb.create(kind='bridge', ifname=brif).commit() - except: - pass - - # get handle in bridge interface - br = ipdb.interfaces[brif] - # begin() a transaction prior to make any change - br.begin() # enable interface - br.up() - # set ageing time - - value is in centiseconds YES! centiseconds! - br.br_ageing_time = bridge['aging'] * 100 - # set bridge forward delay - value is in centiseconds YES! centiseconds! - br.br_forward_delay = bridge['forwarding_delay'] * 100 - # set hello time - value is in centiseconds YES! centiseconds! - br.br_hello_time = bridge['hello_time'] * 100 - # set max message age - value is in centiseconds YES! centiseconds! - br.br_max_age = bridge['max_age'] * 100 + br.state = 'up' + # set ageing time + br.ageing_time = bridge['aging'] + # set bridge forward delay + br.forward_delay = bridge['forwarding_delay'] + # set hello time + br.hello_time = bridge['hello_time'] + # set max message age + br.max_age = bridge['max_age'] # set bridge priority - br.br_priority = bridge['priority'] + br.priority = bridge['priority'] # turn stp on/off - br.br_stp_state = bridge['stp'] + br.stp_state = bridge['stp'] # enable or disable IGMP querier - br.br_mcast_querier = bridge['igmp_querier'] + br.multicast_querier = bridge['igmp_querier'] # update interface description used e.g. within SNMP br.ifalias = bridge['description'] # Change interface MAC address if bridge['mac']: - br.set_address = bridge['mac'] + br.mac = bridge['mac'] # remove interface from bridge for intf in bridge['member_remove']: @@ -240,52 +219,40 @@ def apply(bridge): for member in bridge['member']: br.add_port(member['name']) + # up/down interface + if bridge['disable']: + br.state = 'down' + # remove configured network interface addresses/DHCP(v6) configuration for addr in bridge['address_remove']: - try: - is_ip(addr) - br.del_ip(addr) - except ValueError: - if addr == 'dhcp': - IF(brif).del_dhcpv4() - elif addr == 'dhcpv6': - IF(brif).del_dhcpv6() + 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 for addr in bridge['address']: - try: - is_ip(addr) - br.add_ip(addr) - except: - if addr == 'dhcp': - IF(brif).set_dhcpv4() - elif addr == 'dhcpv6': - IF(brif).set_dhcpv6() - - # up/down interface - if bridge['disable']: - br.down() - - # commit changes on bridge interface - br.commit() + if addr == 'dhcp': + br.set_dhcp() + elif addr == 'dhcpv6': + br.set_dhcpv6() + else: + br.add_addr(addr) # configure additional bridge member options for member in bridge['member']: - # configure ARP cache timeout in milliseconds - with open('/proc/sys/net/ipv4/neigh/' + member['name'] + '/base_reachable_time_ms', 'w') as f: - f.write(str(bridge['arp_cache_timeout_ms'])) - # ignore link state changes - with open('/proc/sys/net/ipv4/conf/' + member['name'] + '/link_filter', 'w') as f: - f.write(str(bridge['disable_link_detect'])) - - # adjust member port stp attributes - member_if = ipdb.interfaces[ member['name'] ] - member_if.begin() # set bridge port cost - member_if.brport_cost = member['cost'] + br.set_cost(member['name'], member['cost']) # set bridge port priority - member_if.brport_priority = member['priority'] - member_if.commit() + br.set_priority(member['name'], member['priority']) + + i = Interface(member['name']) + # configure ARP cache timeout + i.arp_cache_tmo = bridge['arp_cache_tmo'] + # ignore link state changes + i.link_detect = bridge['disable_link_detect'] return None -- cgit v1.2.3 From 3dc08ceaf2c9574133f3b4ff94822b65789a2139 Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Sat, 31 Aug 2019 13:08:14 +0200 Subject: dummy: T1580: migrate from pyroute2 -> vyos.ifconfig --- src/conf_mode/interface-dummy.py | 33 +++++++-------------------------- 1 file changed, 7 insertions(+), 26 deletions(-) diff --git a/src/conf_mode/interface-dummy.py b/src/conf_mode/interface-dummy.py index d8a36a5b2..1f76d0638 100755 --- a/src/conf_mode/interface-dummy.py +++ b/src/conf_mode/interface-dummy.py @@ -19,8 +19,8 @@ from os import environ from copy import deepcopy from sys import exit -from pyroute2 import IPDB from vyos.config import Config +from vyos.ifconfig import DummyIf from vyos import ConfigError default_config_data = { @@ -83,45 +83,26 @@ def generate(dummy): return None def apply(dummy): - ipdb = IPDB(mode='explicit') - dummyif = dummy['intf'] + du = DummyIf(dummy['intf']) # Remove dummy interface if dummy['deleted']: - try: - # delete dummy interface - with ipdb.interface[ dummyif ] as du: - du.remove() - except: - pass + du.remove() else: - try: - # create dummy interface if it's non existing - ipdb.create(kind='dummy', ifname=dummyif).commit() - except: - pass - - # retrieve handle to dummy interface - du = ipdb.interfaces[dummyif] - # begin a transaction prior to make any change - du.begin() # enable interface - du.up() + du.state = 'up' # update interface description used e.g. within SNMP du.ifalias = dummy['description'] # Configure interface address(es) for addr in dummy['address_remove']: - du.del_ip(addr) + du.del_addr(addr) for addr in dummy['address']: - du.add_ip(addr) + du.add_addr(addr) # disable interface on demand if dummy['disable']: - du.down() - - # commit changes on bridge interface - du.commit() + du.state = 'down' return None -- 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(-) 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 bfb2b883071b27adbb33035ae51caf69fc338972 Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Sat, 31 Aug 2019 13:12:04 +0200 Subject: Debian: remove pyroute2 dependency --- debian/control | 1 - 1 file changed, 1 deletion(-) diff --git a/debian/control b/debian/control index 7b75ca111..12eb7c309 100644 --- a/debian/control +++ b/debian/control @@ -29,7 +29,6 @@ Depends: python3, python3-vici (>= 5.7.2), python3-bottle, python3-zmq, - python3-pyroute2, ipaddrcheck, tcpdump, tshark, -- cgit v1.2.3 From aa5054e524a4996ba9cc11deec25fdf4279b29cf Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Sat, 31 Aug 2019 13:18:11 +0200 Subject: dummy: T1580: support deleting interface description --- src/conf_mode/interface-dummy.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/conf_mode/interface-dummy.py b/src/conf_mode/interface-dummy.py index 1f76d0638..03afdc668 100755 --- a/src/conf_mode/interface-dummy.py +++ b/src/conf_mode/interface-dummy.py @@ -61,8 +61,6 @@ def get_config(): # retrieve interface description if conf.exists('description'): dummy['description'] = conf.return_value('description') - else: - dummy['description'] = dummy['intf'] # Disable this interface if conf.exists('disable'): -- cgit v1.2.3 From c306c248ff2ebdbd2f669e8c052a6ecd85a50fad Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Sat, 31 Aug 2019 13:18:15 +0200 Subject: loopback: T1601: support deleting interface description --- src/conf_mode/interface-loopback.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/conf_mode/interface-loopback.py b/src/conf_mode/interface-loopback.py index 1dd68c039..be47324c1 100755 --- a/src/conf_mode/interface-loopback.py +++ b/src/conf_mode/interface-loopback.py @@ -57,8 +57,6 @@ def get_config(): # retrieve interface description if conf.exists('description'): loopback['description'] = conf.return_value('description') - else: - loopback['description'] = loopback['intf'] # Determine interface addresses (currently effective) - to determine which # address is no longer valid and needs to be removed from the interface -- 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(-) 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(-) 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(-) 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 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 0ce3b4142a172a092d034a8c1c9e2178340b43d1 Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Sun, 1 Sep 2019 11:08:21 +0200 Subject: bridge: T1556: change 'aging' help text --- interface-definitions/interfaces-bridge.xml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/interface-definitions/interfaces-bridge.xml b/interface-definitions/interfaces-bridge.xml index 98998bfa1..02d4fa103 100644 --- a/interface-definitions/interfaces-bridge.xml +++ b/interface-definitions/interfaces-bridge.xml @@ -47,14 +47,14 @@ - Interval addresses are retained + MAC address aging interval 0 - Disable retaining address in bridge (always flood) + Disable MAC address learning (always flood) 10-1000000 - Address aging time for bridge seconds (default 300) + MAC address aging time in seconds (default: 300) -- 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(-) 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 5ffa30bcc98be1af1d96b076acb0d8ff44fcb588 Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Sun, 1 Sep 2019 11:48:36 +0200 Subject: Revert "bridge: T1556: increase max-age range to 1200 (30 minutes)" This reverts commit 3b119c91ca70c51aab24d4ef8b3913f47281321a. --- interface-definitions/interfaces-bridge.xml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/interface-definitions/interfaces-bridge.xml b/interface-definitions/interfaces-bridge.xml index 02d4fa103..98633382c 100644 --- a/interface-definitions/interfaces-bridge.xml +++ b/interface-definitions/interfaces-bridge.xml @@ -191,13 +191,13 @@ Interval at which neighbor bridges are removed - 1-1200 + 1-40 Bridge maximum aging time in seconds (default 20) - + - Bridge max aging value must be between 1 and 1200 seconds + Bridge max aging value must be between 1 and 40 seconds -- cgit v1.2.3 From 650293e0e02442803801896128f52624495c319b Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Sun, 1 Sep 2019 12:01:52 +0200 Subject: bridge: T1615: support deleting interface description --- src/conf_mode/interface-bridge.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/conf_mode/interface-bridge.py b/src/conf_mode/interface-bridge.py index 187be677e..7bc457680 100755 --- a/src/conf_mode/interface-bridge.py +++ b/src/conf_mode/interface-bridge.py @@ -82,8 +82,6 @@ def get_config(): # retrieve interface description if conf.exists('description'): bridge['description'] = conf.return_value('description') - else: - bridge['description'] = bridge['intf'] # Disable this bridge interface if conf.exists('disable'): -- cgit v1.2.3 From 81add62632bcdd02a96f6ec2a4bbb533865d68ee Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Sun, 1 Sep 2019 12:18:30 +0200 Subject: bridge: T1615: remove is_ip import from vyos.validate --- src/conf_mode/interface-bridge.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/conf_mode/interface-bridge.py b/src/conf_mode/interface-bridge.py index 7bc457680..6e48a1382 100755 --- a/src/conf_mode/interface-bridge.py +++ b/src/conf_mode/interface-bridge.py @@ -22,7 +22,6 @@ from copy import deepcopy from sys import exit from netifaces import interfaces from vyos.config import Config -from vyos.validate import is_ip from vyos.ifconfig import BridgeIf, Interface from vyos import ConfigError -- 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(-) 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(+) 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(+) 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(+) 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(-) 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(-) 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(-) 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(-) 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(-) 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(-) 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(-) 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(-) 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(+) 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(-) 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(+) 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(-) 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(-) 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 6205c4d6701bda5f8a859291a5e152e009252301 Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Tue, 3 Sep 2019 13:52:33 +0200 Subject: bonding: T1614: Initial version in new style XML/Python interface The node 'interfaces ethernet eth0 bond-group' has been changed and de-nested. Bond members are now configured in the bond interface itself. set interfaces bonding bond0 member interface eth0 --- Makefile | 1 + interface-definitions/interfaces-bonding.xml | 673 +++++++++++++++++++++++++++ src/conf_mode/interface-bonding.py | 409 ++++++++++++++++ src/migration-scripts/interfaces/1-to-2 | 44 ++ 4 files changed, 1127 insertions(+) create mode 100644 interface-definitions/interfaces-bonding.xml create mode 100755 src/conf_mode/interface-bonding.py create mode 100755 src/migration-scripts/interfaces/1-to-2 diff --git a/Makefile b/Makefile index 186e63678..03b4712fc 100644 --- a/Makefile +++ b/Makefile @@ -12,6 +12,7 @@ interface_definitions: rm -f $(TMPL_DIR)/firewall/node.def 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)/protocols/node.def rm -f $(TMPL_DIR)/protocols/static/node.def rm -f $(TMPL_DIR)/system/node.def diff --git a/interface-definitions/interfaces-bonding.xml b/interface-definitions/interfaces-bonding.xml new file mode 100644 index 000000000..d4c3bb704 --- /dev/null +++ b/interface-definitions/interfaces-bonding.xml @@ -0,0 +1,673 @@ + + + + + + + Bonding interface name + 315 + + bond[0-9]+$ + + Bonding interface must be named bondN + + bondN + Bonding interface name + + + + + + IP address + + dhcp dhcpv6 + + + ipv4net + IPv4 address and prefix length + + + ipv6net + IPv6 address and prefix length + + + dhcp + Dynamic Host Configuration Protocol + + + dhcpv6 + Dynamic Host Configuration Protocol for IPv6 + + + + (dhcp|dhcpv6) + + + + + + + ARP link monitoring parameters + + + + + ARP link monitoring interval + + 0-4294967295 + Specifies the ARP link monitoring frequency in milliseconds + + + + + + + + + IP address used for ARP monitoring + + ipv4 + Network Time Protocol (NTP) IPv4 address + + + + + + + + + + + + Interface description + + ^.{1,256}$ + + Interface description too long (limit 256 characters) + + + + + DHCP options + + + + + DHCP client identifier + + + + + DHCP client host name (overrides the system host name) + + + + + + + DHCPv6 options + 319 + + + + + Acquire only config parameters, no address + + + + + + IPv6 "temporary" address + + + + + + + + Ignore link state changes + + + + + + Disable this bridge interface + + + + + + Bonding transmit hash policy + + layer2 layer2+3 layer3+4 + + + layer2 + use MAC addresses to generate the hash (802.3ad, default) + + + layer2+3 + combine MAC address and IP address to make hash + + + layer3+4 + combine IP address and port to make hash + + + (layer2\\+3|layer3\\+4|layer2) + + hash-policy must be layer2 layer2+3 or layer3+4 + + + + + + + ARP cache entry timeout in seconds + + 1-86400 + ARP cache entry timout in seconds (default 30) + + + + + Bridge max aging value must be between 6 and 86400 seconds + + + + + Enable proxy-arp on this interface + + + + + + Enable private VLAN proxy ARP on this interface + + + + + + + + Media Access Control (MAC) address + + h:h:h:h:h:h + Hardware (MAC) address + + + + + + + + + Bonding mode + + 802.3ad active-backup broadcast round-robin transmit-load-balance adaptive-load-balance xor-hash + + + 802.3ad + IEEE 802.3ad Dynamic link aggregation (Default) + + + active-backup + Fault tolerant: only one slave in the bond is active + + + broadcast + Fault tolerant: transmits everything on all slave interfaces + + + round-robin + Load balance: transmit packets in sequential order + + + transmit-load-balance + Load balance: adapts based on transmit load and speed + + + adaptive-load-balance + Load balance: adapts based on transmit and receive plus ARP + + + xor-hash + Distribute based on MAC address + + + (802.3ad|active-backup|broadcast|round-robin|transmit-load-balance|adaptive-load-balance|xor-hash) + + mode must be 802.3ad, active-backup, broadcast, round-robin, transmit-load-balance, adaptive-load-balance, or xor + + + + + Bridge member interfaces + + + + + Member interface name + + + + + + + + + + + Maximum Transmission Unit (MTU) + + 68-9000 + Maximum Transmission Unit + + + + + MTU must be between 68 and 9000 + + + + + Primary device interface + + + + + + + + QinQ TAG-S Virtual Local Area Network (VLAN) ID + + + + VLAN ID must be between 0 and 4094 + + + + + IP address + + dhcp dhcpv6 + + + ipv4net + IPv4 address and prefix length + + + ipv6net + IPv6 address and prefix length + + + dhcp + Dynamic Host Configuration Protocol + + + dhcpv6 + Dynamic Host Configuration Protocol for IPv6 + + + + (dhcp|dhcpv6) + + + + + + + Interface description + + ^.{1,256}$ + + Interface description too long (limit 256 characters) + + + + + DHCP options + + + + + DHCP client identifier + + + + + DHCP client host name (overrides the system host name) + + + + + + + DHCPv6 options + 319 + + + + + Acquire only config parameters, no address + + + + + + IPv6 "temporary" address + + + + + + + + Ignore link state changes + + + + + + Disable this bridge interface + + + + + + Set Ethertype + + 0x88A8 0x8100 + + + 0x88A8 + 802.1ad + + + 0x8100 + 802.1q + + + (0x88A8|0x8100) + + Ethertype must be 0x88A8 or 0x8100 + + + + + Media Access Control (MAC) address + + h:h:h:h:h:h + Hardware (MAC) address + + + + + + + + + Maximum Transmission Unit (MTU) + + 68-9000 + Maximum Transmission Unit + + + + + MTU must be between 68 and 9000 + + + + + QinQ TAG-C Virtual Local Area Network (VLAN) ID + + + + VLAN ID must be between 0 and 4094 + + + + + IP address + + dhcp dhcpv6 + + + ipv4net + IPv4 address and prefix length + + + ipv6net + IPv6 address and prefix length + + + dhcp + Dynamic Host Configuration Protocol + + + dhcpv6 + Dynamic Host Configuration Protocol for IPv6 + + + + (dhcp|dhcpv6) + + + + + + + Interface description + + ^.{1,256}$ + + Interface description too long (limit 256 characters) + + + + + DHCP options + + + + + DHCP client identifier + + + + + DHCP client host name (overrides the system host name) + + + + + + + DHCPv6 options + 319 + + + + + Acquire only config parameters, no address + + + + + + IPv6 "temporary" address + + + + + + + + Ignore link state changes + + + + + + Disable this bridge interface + + + + + + Media Access Control (MAC) address + + h:h:h:h:h:h + Hardware (MAC) address + + + + + + + + + Maximum Transmission Unit (MTU) + + 68-9000 + Maximum Transmission Unit + + + + + MTU must be between 68 and 9000 + + + + + + + + + Virtual Local Area Network (VLAN) ID + + + + VLAN ID must be between 0 and 4094 + + + + + IP address + + dhcp dhcpv6 + + + ipv4net + IPv4 address and prefix length + + + ipv6net + IPv6 address and prefix length + + + dhcp + Dynamic Host Configuration Protocol + + + dhcpv6 + Dynamic Host Configuration Protocol for IPv6 + + + + (dhcp|dhcpv6) + + + + + + + Interface description + + ^.{1,256}$ + + Interface description too long (limit 256 characters) + + + + + DHCP options + + + + + DHCP client identifier + + + + + DHCP client host name (overrides the system host name) + + + + + + + DHCPv6 options + 319 + + + + + Acquire only config parameters, no address + + + + + + IPv6 "temporary" address + + + + + + + + Ignore link state changes + + + + + + Disable this bridge interface + + + + + + Media Access Control (MAC) address + + h:h:h:h:h:h + Hardware (MAC) address + + + + + + + + + Maximum Transmission Unit (MTU) + + 68-9000 + Maximum Transmission Unit + + + + + MTU must be between 68 and 9000 + + + + + + + + + diff --git a/src/conf_mode/interface-bonding.py b/src/conf_mode/interface-bonding.py new file mode 100755 index 000000000..6cd8fdc56 --- /dev/null +++ b/src/conf_mode/interface-bonding.py @@ -0,0 +1,409 @@ +#!/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 + +from copy import deepcopy +from sys import exit +from netifaces import interfaces +from vyos.ifconfig import BondIf +from vyos.config import Config +from vyos import ConfigError + +default_config_data = { + 'address': [], + 'address_remove': [], + 'arp_mon_intvl': 0, + 'arp_mon_tgt': [], + 'description': '', + 'deleted': False, + 'dhcp_client_id': '', + 'dhcp_hostname': '', + 'dhcpv6_prm_only': False, + 'dhcpv6_temporary': False, + 'disable': False, + 'disable_link_detect': 1, + 'hash_policy': 'layer2', + 'ip_arp_cache_tmo': 30, + 'ip_proxy_arp': 0, + 'ip_proxy_arp_pvlan': 0, + 'intf': '', + 'mac': '', + 'mode': '802.3ad', + 'member': [], + 'member_remove': [], + 'mtu': 1500, + 'primary': '', + 'vif_s': [], + 'vif': [] +} + +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': + return 'balance-rr' + elif mode == 'active-backup': + return 'active-backup' + elif mode == 'xor-hash': + return 'balance-xor' + elif mode == 'broadcast': + return 'broadcast' + elif mode == '802.3ad': + return '802.3ad' + elif mode == 'transmit-load-balance': + return 'balance-tlb' + elif mode == 'adaptive-load-balance': + return 'balance-alb' + else: + raise ConfigError('invalid bond mode "{}"'.format(mode)) + +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 + + # ethertype (only on vif-s nodes) + if conf.exists('ethertype'): + vlan['ethertype'] = conf.return_value('ethertype') + + # 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')) + + # 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 + vlan['vif-c'] = [] + 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 get_config(): + # initialize kernel module if not loaded + if not os.path.isfile('/sys/class/net/bonding_masters'): + import syslog + syslog.syslog(syslog.LOG_NOTICE, "loading bonding kernel module") + if os.system('modprobe bonding max_bonds=0 miimon=250') != 0: + syslog.syslog(syslog.LOG_NOTICE, "failed loading bonding kernel module") + raise ConfigError("failed loading bonding kernel module") + + bond = deepcopy(default_config_data) + conf = Config() + + # determine tagNode instance + try: + bond['intf'] = os.environ['VYOS_TAGNODE_VALUE'] + except KeyError as E: + print("Interface not specified") + + # check if bond has been removed + cfg_base = 'interfaces bonding ' + bond['intf'] + if not conf.exists(cfg_base): + bond['deleted'] = True + return bond + + # set new configuration level + conf.set_level(cfg_base) + + # retrieve configured interface addresses + if conf.exists('address'): + bond['address'] = conf.return_values('address') + + # ARP link monitoring frequency in milliseconds + if conf.exists('arp-monitor interval'): + bond['arp_mon_intvl'] = conf.return_value('arp-monitor interval') + + # IP address to use for ARP monitoring + if conf.exists('arp-monitor target'): + bond['arp_mon_tgt'] = conf.return_values('arp-monitor target') + + # retrieve interface description + if conf.exists('description'): + bond['description'] = conf.return_value('description') + else: + bond['description'] = bond['intf'] + + # get DHCP client identifier + if conf.exists('dhcp-options client-id'): + bond['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'): + bond['dhcp_hostname'] = conf.return_value('dhcp-options host-name') + + # DHCPv6 only acquire config parameters, no address + if conf.exists('dhcpv6-options parameters-only'): + bond['dhcpv6_prm_only'] = conf.return_value('dhcpv6-options parameters-only') + + # DHCPv6 temporary IPv6 address + if conf.exists('dhcpv6-options temporary'): + bond['dhcpv6_temporary'] = conf.return_value('dhcpv6-options temporary') + + # ignore link state changes + if conf.exists('disable-link-detect'): + bond['disable_link_detect'] = 2 + + # disable bond interface + if conf.exists('disable'): + bond['disable'] = True + + # Bonding transmit hash policy + if conf.exists('hash-policy'): + bond['hash_policy'] = conf.return_value('hash-policy') + + # ARP cache entry timeout in seconds + if conf.exists('ip arp-cache-timeout'): + bond['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'): + bond['ip_proxy_arp'] = 1 + + # Enable private VLAN proxy ARP on this interface + if conf.exists('ip proxy-arp-pvlan'): + bond['ip_proxy_arp_pvlan'] = 1 + + # Media Access Control (MAC) address + if conf.exists('mac'): + bond['mac'] = conf.return_value('mac') + + # Bonding mode + if conf.exists('mode'): + bond['mode'] = get_bond_mode(conf.return_value('mode')) + + # Maximum Transmission Unit (MTU) + if conf.exists('mtu'): + bond['mtu'] = int(conf.return_value('mtu')) + + # determine bond member interfaces (currently configured) + if conf.exists('member interface'): + bond['member'] = conf.return_values('member interface') + + # Determine bond member interface (currently effective) - to determine which + # interfaces is no longer assigend to the bond and thus can be removed + eff_intf = conf.return_effective_values('member interface') + act_intf = conf.return_values('member interface') + bond['member_remove'] = 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 bond + eff_addr = conf.return_effective_values('address') + act_addr = conf.return_values('address') + bond['address_remove'] = diff(eff_addr, act_addr) + + # Primary device interface + if conf.exists('primary'): + bond['primary'] = conf.return_value('primary') + + # re-set configuration level and retrieve vif-s interfaces + conf.set_level(cfg_base) + if conf.exists('vif-s'): + for vif_s in conf.list_nodes('vif-s'): + # set config level to vif-s interface + conf.set_level(cfg_base + ' vif-s ' + vif_s) + bond['vif_s'].append(vlan_to_dict(conf)) + + # re-set configuration level and retrieve vif-s interfaces + conf.set_level(cfg_base) + 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)) + + 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') + + if bond['primary']: + if bond['mode'] not in ['active-backup', 'balance-tlb', 'balance-alb']: + raise ConfigError('Mode dependency failed, primary not supported in this mode.'.format()) + + return None + +def generate(bond): + return None + +def apply(bond): + b = BondIf(bond['intf']) + + if bond['deleted']: + # delete bonding interface + b.remove() + else: + # Some parameters can not be changed when the bond is up. + # Always disable the bond prior changing anything + b.state = 'down' + + # 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 + if bond['arp_mon_intvl'] == 0: + # reset miimon to default + b.bond_miimon = 250 + + # ARP monitor targets need to be synchronized between sysfs and CLI. + # Unfortunately an address can't be send twice to sysfs as this will + # result in the following exception: OSError: [Errno 22] Invalid argument. + # + # We remove ALL adresses prior adding new ones, this will remove addresses + # added manually by the user too - but as we are limited to 16 adresses + # 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: + b.arp_ip_target = '-' + addr + + # Add configured ARP target addresses + for addr in bond['arp_mon_tgt']: + b.arp_ip_target = '+' + addr + + # update interface description used e.g. within SNMP + b.ifalias = bond['description'] + + # + # missing DHCP/DHCPv6 options go here + # + + # ignore link state changes + b.link_detect = bond['disable_link_detect'] + # Bonding transmit hash policy + b.xmit_hash_policy = bond['hash_policy'] + # configure ARP cache timeout in milliseconds + b.arp_cache_tmp = bond['ip_arp_cache_tmo'] + # Enable proxy-arp on this interface + b.proxy_arp = bond['ip_proxy_arp'] + # Enable private VLAN proxy ARP on this interface + b.proxy_arp_pvlan = bond['ip_proxy_arp_pvlan'] + + # Change interface MAC address + if bond['mac']: + b.mac = bond['mac'] + + # The bonding mode can not be changed when there are interfaces enslaved + # to this bond, thus we will free all interfaces from the bond first! + for intf in b.get_slaves(): + b.del_port(intf) + + # Bonding policy + b.mode = bond['mode'] + # Maximum Transmission Unit (MTU) + b.mtu = bond['mtu'] + # 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) + + # 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__': + try: + c = get_config() + verify(c) + generate(c) + apply(c) + except ConfigError as e: + print(e) + exit(1) diff --git a/src/migration-scripts/interfaces/1-to-2 b/src/migration-scripts/interfaces/1-to-2 new file mode 100755 index 000000000..10d542d1d --- /dev/null +++ b/src/migration-scripts/interfaces/1-to-2 @@ -0,0 +1,44 @@ +#!/usr/bin/env python3 + +# Change syntax of bond interface +# - move interface based bond-group to actual bond (de-nest) +# https://phabricator.vyos.net/T1614 + +import sys +from vyos.configtree import ConfigTree + +if (len(sys.argv) < 1): + print("Must specify file name!") + sys.exit(1) + +file_name = sys.argv[1] + +with open(file_name, 'r') as f: + config_file = f.read() + +config = ConfigTree(config_file) +base = ['interfaces', 'bonding'] + +if not config.exists(base): + # Nothing to do + sys.exit(0) +else: + # + # move interface based bond-group to actual bond (de-nest) + # + for intf in config.list_nodes(['interfaces', 'ethernet']): + # check if bond-group exists + if config.exists(['interfaces', 'ethernet', intf, 'bond-group']): + # get configured bond interface + bond = config.return_value(['interfaces', 'ethernet', intf, 'bond-group']) + # delete old interface asigned (nested) bond group + config.delete(['interfaces', 'ethernet', intf, 'bond-group']) + # create new bond member interface + config.set(base + [bond, 'member', 'interface'], value=intf, replace=False) + + try: + with open(file_name, 'w') as f: + f.write(config.to_string()) + except OSError as e: + print("Failed to save the modified config: {}".format(e)) + sys.exit(1) -- cgit v1.2.3 From 3fad8f2f74224502018c5e53b5931251595ad3b7 Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Tue, 3 Sep 2019 14:56:21 +0200 Subject: bonding: T1614: can not set primary interface when it's not part of the bond --- src/conf_mode/interface-bonding.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/conf_mode/interface-bonding.py b/src/conf_mode/interface-bonding.py index 6cd8fdc56..b150578e3 100755 --- a/src/conf_mode/interface-bonding.py +++ b/src/conf_mode/interface-bonding.py @@ -302,6 +302,9 @@ def verify(bond): if bond['mode'] not in ['active-backup', 'balance-tlb', 'balance-alb']: raise ConfigError('Mode dependency failed, primary not supported in this mode.'.format()) + if bond['primary'] not in bond['member']: + raise ConfigError('Interface "{}" is not part of the bond'.format(bond['primary'])) + return None def generate(bond): -- cgit v1.2.3 From 029085250c4e2dea8a98f3031cf6b8037ffdd534 Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Tue, 3 Sep 2019 17:14:11 +0200 Subject: bonding: T1614: remove obsolete 'member_remove' dict-key --- src/conf_mode/interface-bonding.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/src/conf_mode/interface-bonding.py b/src/conf_mode/interface-bonding.py index b150578e3..a623ca524 100755 --- a/src/conf_mode/interface-bonding.py +++ b/src/conf_mode/interface-bonding.py @@ -46,7 +46,6 @@ default_config_data = { 'mac': '', 'mode': '802.3ad', 'member': [], - 'member_remove': [], 'mtu': 1500, 'primary': '', 'vif_s': [], @@ -260,12 +259,6 @@ def get_config(): if conf.exists('member interface'): bond['member'] = conf.return_values('member interface') - # Determine bond member interface (currently effective) - to determine which - # interfaces is no longer assigend to the bond and thus can be removed - eff_intf = conf.return_effective_values('member interface') - act_intf = conf.return_values('member interface') - bond['member_remove'] = 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 bond eff_addr = conf.return_effective_values('address') -- 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(-) 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(-) 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 da2bb10b8e4c15aaf993fc82eb0f9b8c564a2113 Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Tue, 3 Sep 2019 21:55:49 +0200 Subject: bonding: T1614: identical ID on vif and vif-s is not allowed --- src/conf_mode/interface-bonding.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/conf_mode/interface-bonding.py b/src/conf_mode/interface-bonding.py index 9e7be19f5..c2f0086aa 100755 --- a/src/conf_mode/interface-bonding.py +++ b/src/conf_mode/interface-bonding.py @@ -361,6 +361,11 @@ def verify(bond): if bond['primary'] not in bond['member']: raise ConfigError('Interface "{}" is not part of the bond'.format(bond['primary'])) + for vif_s in bond['vif_s']: + for vif in bond['vif']: + if vif['id'] == vif_s['id']: + raise ConfigError('Can not use identical ID on vif and vif-s interface') + return None -- cgit v1.2.3 From 0d156a3947981e71774359b4566ac2af5892abe9 Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Tue, 3 Sep 2019 22:23:38 +0200 Subject: bonding: T1614: add vif-c VLAN interface support Tested using: ============= set interfaces bonding bond0 address 192.0.2.1/24 set interfaces bonding bond0 description "VyOS bonding" set interfaces bonding bond0 disable-link-detect set interfaces bonding bond0 hash-policy layer2+3 set interfaces bonding bond0 ip arp-cache-timeout 86400 set interfaces bonding bond0 mac 00:91:00:00:00:01 set interfaces bonding bond0 mode active-backup set interfaces bonding bond0 mtu 9000 set interfaces bonding bond0 member interface eth1 set interfaces bonding bond0 member interface eth2 set interfaces bonding bond0 vif-s 100 address 192.168.10.1/24 set interfaces bonding bond0 vif-s 100 description "802.1ad service VLAN 100" set interfaces bonding bond0 vif-s 100 mtu 1500 set interfaces bonding bond0 vif-s 100 mac 00:91:00:00:00:02 set interfaces bonding bond0 vif-s 100 vif-c 110 address "192.168.110.1/24" set interfaces bonding bond0 vif-s 100 vif-c 110 description "client VLAN 110" set interfaces bonding bond0 vif-s 100 vif-c 120 address "192.168.120.1/24" set interfaces bonding bond0 vif-s 100 vif-c 120 description "client VLAN 120" set interfaces bonding bond0 vif-s 100 vif-c 130 address "192.168.130.1/24" set interfaces bonding bond0 vif-s 100 vif-c 130 description "client VLAN 130" set interfaces bonding bond0 vif 400 address 192.168.40.1/24 set interfaces bonding bond0 vif 400 description "802.1q VLAN 400" set interfaces bonding bond0 vif 400 mtu 1500 set interfaces bonding bond0 vif 400 mac 00:91:00:00:00:03 --- src/conf_mode/interface-bonding.py | 65 ++++++++++++++++++++++++-------------- 1 file changed, 42 insertions(+), 23 deletions(-) diff --git a/src/conf_mode/interface-bonding.py b/src/conf_mode/interface-bonding.py index c2f0086aa..b01018e5b 100755 --- a/src/conf_mode/interface-bonding.py +++ b/src/conf_mode/interface-bonding.py @@ -145,33 +145,41 @@ def vlan_to_dict(conf): 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) - # 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')) - - # 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 - vlan['vif-c'] = [] - 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)) + # 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 @@ -309,7 +317,7 @@ def get_config(): if conf.exists('member interface'): bond['member'] = conf.return_values('member interface') - # Determine interface addresses (currently effective) - to determine which + # 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') @@ -321,8 +329,8 @@ 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 + # get 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) @@ -458,8 +466,19 @@ def apply(bond): # 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) + s_vlan = b.add_vlan(vif_s['id'], ethertype=vif_s['ethertype']) + apply_vlan_config(s_vlan, vif_s) + + # remove no longer required client VLAN interfaces (vif-c) + # on lower service VLAN interface + for vif_c in vif_s['vif_c_remove']: + s_vlan.del_vlan(vif_c) + + # create client VLAN interfaces (vif-c) + # on lower service VLAN interface + for vif_c in vif_s['vif_c']: + c_vlan = s_vlan.add_vlan(vif_c['id']) + apply_vlan_config(c_vlan, vif_c) # remove no longer required VLAN interfaces (vif) for vif in bond['vif_remove']: -- cgit v1.2.3 From 7e367a928505051d8fe05e09a0a284226eb7ecc3 Mon Sep 17 00:00:00 2001 From: Daniil Baturin Date: Wed, 4 Sep 2019 14:59:47 +0200 Subject: T1443: add dependencies on nginx-light and ssl-cert. --- debian/control | 1 + 1 file changed, 1 insertion(+) diff --git a/debian/control b/debian/control index 12eb7c309..dce463157 100644 --- a/debian/control +++ b/debian/control @@ -65,6 +65,7 @@ Depends: python3, mtr-tiny, telnet, traceroute, + ssl-cert, nginx-light, ${shlibs:Depends}, ${misc:Depends} Description: VyOS configuration scripts and data -- cgit v1.2.3 From 9e1cbaa5f8943221caf4ce4fd3bebc3fec8dc49a Mon Sep 17 00:00:00 2001 From: Daniil Baturin Date: Wed, 4 Sep 2019 16:03:15 +0200 Subject: [service https] T1443: create /etc/vyos if it doesn't exist. --- src/conf_mode/http-api.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/conf_mode/http-api.py b/src/conf_mode/http-api.py index 1f91ac582..9c062f0aa 100755 --- a/src/conf_mode/http-api.py +++ b/src/conf_mode/http-api.py @@ -69,6 +69,9 @@ def generate(http_api): if http_api is None: return None + if not os.path.exists('/etc/vyos'): + os.mkdir('/etc/vyos') + with open(config_file, 'w') as f: json.dump(http_api, f, indent=2) -- cgit v1.2.3 From 07ebd7589a961aaa8d4f3099836dd94e4bce2379 Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Wed, 4 Sep 2019 15:52:39 +0200 Subject: bonding: T1614: T532: new commit validators As in the past during the priority race of the bash script invalid configuration could appear in the CLI and are de-synced from the kernle state, e.g. some bonding modes do not support arp_interval. This is no longer allowed and added to the migration script so that the config again represents the truth. --- src/conf_mode/interface-bonding.py | 53 ++++++++++++++++++++++++++++----- src/migration-scripts/interfaces/1-to-2 | 19 ++++++++++++ 2 files changed, 65 insertions(+), 7 deletions(-) diff --git a/src/conf_mode/interface-bonding.py b/src/conf_mode/interface-bonding.py index b01018e5b..03d28954d 100755 --- a/src/conf_mode/interface-bonding.py +++ b/src/conf_mode/interface-bonding.py @@ -249,7 +249,7 @@ def get_config(): # ARP link monitoring frequency in milliseconds if conf.exists('arp-monitor interval'): - bond['arp_mon_intvl'] = conf.return_value('arp-monitor interval') + bond['arp_mon_intvl'] = int(conf.return_value('arp-monitor interval')) # IP address to use for ARP monitoring if conf.exists('arp-monitor target'): @@ -374,6 +374,43 @@ def verify(bond): if vif['id'] == vif_s['id']: raise ConfigError('Can not use identical ID on vif and vif-s interface') + + conf = Config() + for intf in bond['member']: + # we can not add disabled slave interfaces to our bond + if conf.exists('interfaces ethernet ' + intf + ' disable'): + raise ConfigError('can not add disabled interface {} to {}'.format(intf, bond['intf'])) + + # can not add interfaces with an assigned address to a bond + if conf.exists('interfaces ethernet ' + intf + ' address'): + raise ConfigError('can not add interface {} with an assigned address to {}'.format(intf, bond['intf'])) + + # bond members are not allowed to be bridge members, too + for bridge in conf.list_nodes('interfaces bridge'): + if conf.exists('interfaces bridge ' + bridge + ' member interface ' + intf): + raise ConfigError('can not add interface {} that is part of bridge {} to {}'.format(intf, bridge, bond['intf'])) + + # bond members are not allowed to be vrrp members, too + for vrrp in conf.list_nodes('high-availability vrrp group'): + if conf.exists('high-availability vrrp group ' + vrrp + ' interface ' + intf): + raise ConfigError('can not add interface {} which belongs to a VRRP group to {}'.format(intf, bond['intf'])) + + # bond members are not allowed to be underlaying psuedo-ethernet devices + for peth in conf.list_nodes('interfaces pseudo-ethernet'): + if conf.exists('interfaces pseudo-ethernet ' + peth + ' link ' + intf): + raise ConfigError('can not add interface {} used by pseudo-ethernet {} to {}'.format(intf, peth, bond['intf'])) + + if bond['primary']: + if bond['primary'] not in bond['member']: + raise ConfigError('primary interface must be a member interface of {}'.format(bond['intf'])) + + if bond['mode'] not in ['active-backup', 'balance-tlb', 'balance-alb']: + raise ConfigError('primary interface only works for mode active-backup, transmit-load-balance or adaptive-load-balance') + + if bond['arp_mon_intvl'] > 0: + if bond['mode'] in ['802.3ad', 'balance-tlb', 'balance-alb']: + raise ConfigError('ARP link monitoring does not work for mode 802.3ad, transmit-load-balance or adaptive-load-balance') + return None @@ -392,6 +429,11 @@ def apply(bond): # Always disable the bond prior changing anything b.state = 'down' + # The bonding mode can not be changed when there are interfaces enslaved + # to this bond, thus we will free all interfaces from the bond first! + for intf in b.get_slaves(): + b.del_port(intf) + # Configure interface address(es) for addr in bond['address_remove']: b.del_addr(addr) @@ -444,17 +486,14 @@ def apply(bond): if bond['mac']: b.mac = bond['mac'] - # The bonding mode can not be changed when there are interfaces enslaved - # to this bond, thus we will free all interfaces from the bond first! - for intf in b.get_slaves(): - b.del_port(intf) - # Bonding policy b.mode = bond['mode'] # Maximum Transmission Unit (MTU) b.mtu = bond['mtu'] + # Primary device interface - b.primary = bond['primary'] + if bond['primary']: + b.primary = bond['primary'] # Add (enslave) interfaces to bond for intf in bond['member']: diff --git a/src/migration-scripts/interfaces/1-to-2 b/src/migration-scripts/interfaces/1-to-2 index 10d542d1d..050137318 100755 --- a/src/migration-scripts/interfaces/1-to-2 +++ b/src/migration-scripts/interfaces/1-to-2 @@ -36,6 +36,25 @@ else: # create new bond member interface config.set(base + [bond, 'member', 'interface'], value=intf, replace=False) + # + # some combinations were allowed in the past from a CLI perspective + # but the kernel overwrote them - remove from CLI to not confuse the users. + # In addition new consitency checks are in place so users can't repeat the + # mistake. One of those nice issues is https://phabricator.vyos.net/T532 + for bond in config.list_nodes(base): + if config.exists(base + [bond, 'arp-monitor', 'interval']) and config.exists(base + [bond, 'mode']): + mode = config.return_value(base + [bond, 'mode']) + if mode in ['802.3ad', 'transmit-load-balance', 'adaptive-load-balance']: + intvl = int(config.return_value(base + [bond, 'arp-monitor', 'interval'])) + if intvl > 0: + # this is not allowed and the linux kernel replies with: + # option arp_interval: mode dependency failed, not supported in mode 802.3ad(4) + # option arp_interval: mode dependency failed, not supported in mode balance-alb(6) + # option arp_interval: mode dependency failed, not supported in mode balance-tlb(5) + # + # so we simply disable arp_interval by setting it to 0 and miimon will take care about the link + config.set(base + [bond, 'arp-monitor', 'interval'], value='0') + try: with open(file_name, 'w') as f: f.write(config.to_string()) -- cgit v1.2.3 From 053f208b2ed9477fc70b770ab7ac884109d9a89a Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Wed, 4 Sep 2019 16:22:15 +0200 Subject: bridge: T1615: can not add member interface to bridge if it is also part of a bond --- src/conf_mode/interface-bridge.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/conf_mode/interface-bridge.py b/src/conf_mode/interface-bridge.py index 6e48a1382..1d3587114 100755 --- a/src/conf_mode/interface-bridge.py +++ b/src/conf_mode/interface-bridge.py @@ -172,6 +172,13 @@ def verify(bridge): if intf['name'] not in interfaces(): raise ConfigError('Can not add non existing interface "{}" to bridge "{}"'.format(intf['name'], bridge['intf'])) + # bridge members are not allowed to be bond members, too + for intf in bridge['member']: + for bond in conf.list_nodes('interfaces bonding'): + if conf.exists('interfaces bonding ' + bond + ' member interface'): + if intf['name'] in conf.return_values('interfaces bonding ' + bond + ' member interface'): + raise ConfigError('Interface {} belongs to bond {}, can not add it to {}'.format(intf['name'], bond, bridge['intf'])) + return None def generate(bridge): -- cgit v1.2.3 From e695dee0f587285526e947e0e4ae5cd6581e9f12 Mon Sep 17 00:00:00 2001 From: Daniil Baturin Date: Wed, 4 Sep 2019 17:08:36 +0200 Subject: [service https] T1443: use "listen-address" option instead of "listen-addresses" to follow the established convention. --- interface-definitions/https.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/interface-definitions/https.xml b/interface-definitions/https.xml index 7a87133f3..a3bcacc09 100644 --- a/interface-definitions/https.xml +++ b/interface-definitions/https.xml @@ -9,7 +9,7 @@ 1001 - + Addresses to listen for HTTPS requests -- cgit v1.2.3 From 77e4de232feea979ce55abd60d7dc9352b5bfe86 Mon Sep 17 00:00:00 2001 From: Daniil Baturin Date: Wed, 4 Sep 2019 17:46:40 +0200 Subject: [service https] T1443: correct the listen-address option in the script. --- src/conf_mode/https.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/conf_mode/https.py b/src/conf_mode/https.py index d5aa1f5b3..2e7aeb5a4 100755 --- a/src/conf_mode/https.py +++ b/src/conf_mode/https.py @@ -133,12 +133,12 @@ def get_config(): else: conf.set_level('service https') - if conf.exists('listen-addresses'): + if conf.exists('listen-address'): addrs = {} - for addr in conf.list_nodes('listen-addresses'): + for addr in conf.list_nodes('listen-address'): addrs[addr] = ['_'] - if conf.exists('listen-addresses {0} server-names'.format(addr)): - names = conf.return_values('listen-addresses {0} server-names'.format(addr)) + if conf.exists('listen-address {0} server-names'.format(addr)): + names = conf.return_values('listen-address {0} server-names'.format(addr)) addrs[addr] = names[:] https['listen_addresses'] = addrs -- cgit v1.2.3 From b142ab9f5093e10915cba08852f75a94be0b9ee6 Mon Sep 17 00:00:00 2001 From: Daniil Baturin Date: Wed, 4 Sep 2019 18:15:27 +0200 Subject: [service https] T1443: rename "server-names" option to "server-name". --- interface-definitions/https.xml | 2 +- src/conf_mode/https.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/interface-definitions/https.xml b/interface-definitions/https.xml index a3bcacc09..2fb3bf082 100644 --- a/interface-definitions/https.xml +++ b/interface-definitions/https.xml @@ -31,7 +31,7 @@ - + Server names: exact, wildcard, regex, or '_' (any) diff --git a/src/conf_mode/https.py b/src/conf_mode/https.py index 2e7aeb5a4..f948063e9 100755 --- a/src/conf_mode/https.py +++ b/src/conf_mode/https.py @@ -137,8 +137,8 @@ def get_config(): addrs = {} for addr in conf.list_nodes('listen-address'): addrs[addr] = ['_'] - if conf.exists('listen-address {0} server-names'.format(addr)): - names = conf.return_values('listen-address {0} server-names'.format(addr)) + if conf.exists('listen-address {0} server-name'.format(addr)): + names = conf.return_values('listen-address {0} server-name'.format(addr)) addrs[addr] = names[:] https['listen_addresses'] = addrs -- cgit v1.2.3 From 8257a5e67679fe6b7b74e8ba4b57547209a281d2 Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Wed, 4 Sep 2019 20:32:36 +0200 Subject: openvpn: T1617: T1632: support quotes in openvpn-option The following CLI command can be used to add a raw option to OpenVPN which requires quotes: > set interfaces openvpn vtun10 openvpn-option 'push "keepalive 1 10"' The resulting config file will then have the following set: > push "keepalive 1 10" --- src/conf_mode/interface-openvpn.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/conf_mode/interface-openvpn.py b/src/conf_mode/interface-openvpn.py index 4e5915d4e..91679084e 100755 --- a/src/conf_mode/interface-openvpn.py +++ b/src/conf_mode/interface-openvpn.py @@ -806,6 +806,11 @@ def generate(openvpn): tmpl = jinja2.Template(config_tmpl) config_text = tmpl.render(openvpn) + + # we need to support quoting of raw parameters from OpenVPN CLI + # see https://phabricator.vyos.net/T1632 + config_text = config_text.replace(""",'"') + with open(get_config_name(interface), 'w') as f: f.write(config_text) os.chown(get_config_name(interface), uid, gid) -- 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(-) 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(-) 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(-) 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(-) 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(-) 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(-) 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(+) 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(-) 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(-) 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 8b0e9c1fec6793c8c8708bcb330569ac490ae0aa Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Thu, 5 Sep 2019 18:21:04 +0200 Subject: gitignore: add patterns used by SlickEdit --- .gitignore | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.gitignore b/.gitignore index b42512134..2d84a4209 100644 --- a/.gitignore +++ b/.gitignore @@ -119,3 +119,9 @@ debian/debhelper-build-stamp # Sonar Cloud .scannerwork /.vs + +# SlickEdit +*.vpj +*.vpw +*.vpwhist +*.vtg -- cgit v1.2.3 From 33d67cb7b6c428a4283e4c799e6bc352f76cd2b6 Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Thu, 5 Sep 2019 18:24:40 +0200 Subject: bonding: T1614: make ARP cache constraint error message more generic --- interface-definitions/interfaces-bonding.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/interface-definitions/interfaces-bonding.xml b/interface-definitions/interfaces-bonding.xml index d4c3bb704..88dbab6ab 100644 --- a/interface-definitions/interfaces-bonding.xml +++ b/interface-definitions/interfaces-bonding.xml @@ -171,7 +171,7 @@ - Bridge max aging value must be between 6 and 86400 seconds + ARP cache entry timeout must be between 1 and 86400 seconds -- cgit v1.2.3 From 4615feb0f7b192afc0c9a1b11628877c5f430180 Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Thu, 5 Sep 2019 18:25:07 +0200 Subject: bridge: T1556: make ARP cache constraint error message more generic --- interface-definitions/interfaces-bridge.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/interface-definitions/interfaces-bridge.xml b/interface-definitions/interfaces-bridge.xml index 98633382c..4b82972dc 100644 --- a/interface-definitions/interfaces-bridge.xml +++ b/interface-definitions/interfaces-bridge.xml @@ -170,7 +170,7 @@ - Bridge max aging value must be between 6 and 86400 seconds + ARP cache entry timeout must be between 1 and 86400 seconds -- cgit v1.2.3 From bff9d998e9638540fc85338f50d4dbc7d9581c67 Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Thu, 5 Sep 2019 18:26:43 +0200 Subject: openvpn: T1548: use long syntax on list_interfaces.py '--type' instead of '-t' --- interface-definitions/interfaces-openvpn.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/interface-definitions/interfaces-openvpn.xml b/interface-definitions/interfaces-openvpn.xml index bb5c5a965..d282a8773 100644 --- a/interface-definitions/interfaces-openvpn.xml +++ b/interface-definitions/interfaces-openvpn.xml @@ -42,7 +42,7 @@ Interface to a bridge-group - + -- cgit v1.2.3 From 7e681c180de8adceb6beb216e194c09ad4d3107c Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Thu, 5 Sep 2019 18:27:20 +0200 Subject: wireguard: T427: use long syntax on list_interfaces.py '--type' instead of '-t' --- op-mode-definitions/wireguard.xml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/op-mode-definitions/wireguard.xml b/op-mode-definitions/wireguard.xml index 681bb5f47..fa5e4a206 100644 --- a/op-mode-definitions/wireguard.xml +++ b/op-mode-definitions/wireguard.xml @@ -19,7 +19,7 @@ generate a wireguard preshared key ${vyos_op_scripts_dir}/wireguard.py --genpsk - + @@ -51,7 +51,7 @@ show wireguard interface information - + sudo wg show "$4" @@ -74,7 +74,7 @@ sudo wg show "$4" peers - + -- cgit v1.2.3 From 96e0f5697b181be4f2f3c5c763821ef574881a93 Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Fri, 6 Sep 2019 10:41:55 +0200 Subject: bonding: T1614: enslaved interfaces can be added to only one bond at a time --- src/conf_mode/interface-bonding.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/conf_mode/interface-bonding.py b/src/conf_mode/interface-bonding.py index 81667289d..7b86a0528 100755 --- a/src/conf_mode/interface-bonding.py +++ b/src/conf_mode/interface-bonding.py @@ -270,6 +270,12 @@ def verify(bond): conf = Config() for intf in bond['member']: + # a bond member is only allowed to be assigned to any one bond + for tmp in conf.list_nodes('interfaces bonding'): + if conf.exists('interfaces bonding ' + tmp + ' member interface ' + intf): + raise ConfigError('can not add interface {} that is part of another bond ({}) to {}'.format( + intf, tmp, bond['intf'])) + # we can not add disabled slave interfaces to our bond if conf.exists('interfaces ethernet ' + intf + ' disable'): raise ConfigError('can not add disabled interface {} to {}'.format(intf, bond['intf'])) -- cgit v1.2.3 From f9eff4fcdf30223638169e6ed5fa7ca49fcfa223 Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Fri, 6 Sep 2019 10:49:02 +0200 Subject: bonding: T1614: reword verify() error messages --- src/conf_mode/interface-bonding.py | 46 +++++++++++++++++++++++--------------- 1 file changed, 28 insertions(+), 18 deletions(-) diff --git a/src/conf_mode/interface-bonding.py b/src/conf_mode/interface-bonding.py index 7b86a0528..acf088de8 100755 --- a/src/conf_mode/interface-bonding.py +++ b/src/conf_mode/interface-bonding.py @@ -257,10 +257,12 @@ def verify(bond): if bond['primary']: if bond['mode'] not in ['active-backup', 'balance-tlb', 'balance-alb']: - raise ConfigError('Mode dependency failed, primary not supported in this mode.'.format()) + raise ConfigError('Mode dependency failed, primary not supported ' \ + 'in this mode.'.format()) if bond['primary'] not in bond['member']: - raise ConfigError('Interface "{}" is not part of the bond'.format(bond['primary'])) + raise ConfigError('Interface "{}" is not part of the bond' \ + .format(bond['primary'])) for vif_s in bond['vif_s']: for vif in bond['vif']: @@ -273,42 +275,50 @@ def verify(bond): # a bond member is only allowed to be assigned to any one bond for tmp in conf.list_nodes('interfaces bonding'): if conf.exists('interfaces bonding ' + tmp + ' member interface ' + intf): - raise ConfigError('can not add interface {} that is part of another bond ({}) to {}'.format( - intf, tmp, bond['intf'])) + raise ConfigError('can not enslave interface {} which already ' \ + 'belongs to bond {}'.format(intf, tmp)) # we can not add disabled slave interfaces to our bond if conf.exists('interfaces ethernet ' + intf + ' disable'): - raise ConfigError('can not add disabled interface {} to {}'.format(intf, bond['intf'])) + raise ConfigError('can not enslave disabled interface {}' \ + .format(intf)) # can not add interfaces with an assigned address to a bond if conf.exists('interfaces ethernet ' + intf + ' address'): - raise ConfigError('can not add interface {} with an assigned address to {}'.format(intf, bond['intf'])) + raise ConfigError('can not enslave interface {} which has an address ' \ + 'assigned'.format(intf)) # bond members are not allowed to be bridge members, too - for bridge in conf.list_nodes('interfaces bridge'): - if conf.exists('interfaces bridge ' + bridge + ' member interface ' + intf): - raise ConfigError('can not add interface {} that is part of bridge {} to {}'.format(intf, bridge, bond['intf'])) + for tmp in conf.list_nodes('interfaces bridge'): + if conf.exists('interfaces bridge ' + tmp + ' member interface ' + intf): + raise ConfigError('can not enslave interface {} which belongs to ' \ + 'bridge {}'.format(intf, tmp)) # bond members are not allowed to be vrrp members, too - for vrrp in conf.list_nodes('high-availability vrrp group'): - if conf.exists('high-availability vrrp group ' + vrrp + ' interface ' + intf): - raise ConfigError('can not add interface {} which belongs to a VRRP group to {}'.format(intf, bond['intf'])) + for tmp in conf.list_nodes('high-availability vrrp group'): + if conf.exists('high-availability vrrp group ' + tmp + ' interface ' + intf): + raise ConfigError('can not enslave interface {} which belongs to ' \ + 'VRRP group {}'.format(intf, tmp)) # bond members are not allowed to be underlaying psuedo-ethernet devices - for peth in conf.list_nodes('interfaces pseudo-ethernet'): - if conf.exists('interfaces pseudo-ethernet ' + peth + ' link ' + intf): - raise ConfigError('can not add interface {} used by pseudo-ethernet {} to {}'.format(intf, peth, bond['intf'])) + for tmp in conf.list_nodes('interfaces pseudo-ethernet'): + if conf.exists('interfaces pseudo-ethernet ' + tmp + ' link ' + intf): + raise ConfigError('can not enslave interface {} which belongs to ' \ + 'pseudo-ethernet {}'.format(intf, tmp)) if bond['primary']: if bond['primary'] not in bond['member']: - raise ConfigError('primary interface must be a member interface of {}'.format(bond['intf'])) + raise ConfigError('primary interface must be a member interface of {}' \ + .format(bond['intf'])) if bond['mode'] not in ['active-backup', 'balance-tlb', 'balance-alb']: - raise ConfigError('primary interface only works for mode active-backup, transmit-load-balance or adaptive-load-balance') + raise ConfigError('primary interface only works for mode active-backup, ' \ + 'transmit-load-balance or adaptive-load-balance') if bond['arp_mon_intvl'] > 0: if bond['mode'] in ['802.3ad', 'balance-tlb', 'balance-alb']: - raise ConfigError('ARP link monitoring does not work for mode 802.3ad, transmit-load-balance or adaptive-load-balance') + raise ConfigError('ARP link monitoring does not work for mode 802.3ad, ' \ + 'transmit-load-balance or adaptive-load-balance') return None -- cgit v1.2.3 From 5f87266d9ef3b72908b6f429e37df280f1be8cdf Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Fri, 6 Sep 2019 10:54:50 +0200 Subject: bonding: T1614: members are not allowed to be underlaying vxlan devices --- src/conf_mode/interface-bonding.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/conf_mode/interface-bonding.py b/src/conf_mode/interface-bonding.py index acf088de8..bce8f98fb 100755 --- a/src/conf_mode/interface-bonding.py +++ b/src/conf_mode/interface-bonding.py @@ -306,6 +306,13 @@ def verify(bond): raise ConfigError('can not enslave interface {} which belongs to ' \ 'pseudo-ethernet {}'.format(intf, tmp)) + # bond members are not allowed to be underlaying vxlan devices + for tmp in conf.list_nodes('interfaces vxlan'): + if conf.exists('interfaces vxlan ' + tmp + ' link ' + intf): + raise ConfigError('can not enslave interface {} which belongs to ' \ + 'vxlan {}'.format(intf, tmp)) + + if bond['primary']: if bond['primary'] not in bond['member']: raise ConfigError('primary interface must be a member interface of {}' \ -- cgit v1.2.3 From c38097eb62dfe5eb309d90752e3ce611999a48d1 Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Fri, 6 Sep 2019 12:28:51 +0200 Subject: dummy: loopback: T1580: T1601: synchronize comments --- src/conf_mode/interface-dummy.py | 2 ++ src/conf_mode/interface-loopback.py | 5 ++++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/conf_mode/interface-dummy.py b/src/conf_mode/interface-dummy.py index 4a1179672..614fe08db 100755 --- a/src/conf_mode/interface-dummy.py +++ b/src/conf_mode/interface-dummy.py @@ -91,6 +91,8 @@ def apply(dummy): du.ifalias = dummy['description'] # Configure interface address(es) + # - not longer required addresses get removed first + # - newly addresses will be added second for addr in dummy['address_remove']: du.del_addr(addr) for addr in dummy['address']: diff --git a/src/conf_mode/interface-loopback.py b/src/conf_mode/interface-loopback.py index e2df37655..a1a807868 100755 --- a/src/conf_mode/interface-loopback.py +++ b/src/conf_mode/interface-loopback.py @@ -77,7 +77,10 @@ def apply(loopback): # update interface description used e.g. within SNMP # update interface description used e.g. within SNMP lo.ifalias = loopback['description'] - # configure interface address(es) + + # Configure interface address(es) + # - not longer required addresses get removed first + # - newly addresses will be added second for addr in loopback['address']: lo.add_addr(addr) -- 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 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 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 2e48db607e3f228cf6f40b53478966e5fbe00dbc Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Fri, 6 Sep 2019 15:12:20 +0200 Subject: openvpn: T1630: support adding routes as unpriviledged user --- src/conf_mode/interface-openvpn.py | 5 +++-- src/system/unpriv-ip | 2 ++ 2 files changed, 5 insertions(+), 2 deletions(-) create mode 100755 src/system/unpriv-ip diff --git a/src/conf_mode/interface-openvpn.py b/src/conf_mode/interface-openvpn.py index 91679084e..5f5cbd67d 100755 --- a/src/conf_mode/interface-openvpn.py +++ b/src/conf_mode/interface-openvpn.py @@ -34,8 +34,8 @@ from vyos.config import Config from vyos import ConfigError from vyos.validate import is_addr_assigned -user = 'nobody' -group = 'nogroup' +user = 'openvpn' +group = 'openvpn' # Please be careful if you edit the template. config_tmpl = """ @@ -58,6 +58,7 @@ dev {{ intf }} user {{ uid }} group {{ gid }} persist-key +iproute /usr/libexec/vyos/system/unpriv-ip proto {% if 'tcp-active' in protocol -%}tcp-client{% elif 'tcp-passive' in protocol -%}tcp-server{% else %}udp{% endif %} diff --git a/src/system/unpriv-ip b/src/system/unpriv-ip new file mode 100755 index 000000000..1ea0d626a --- /dev/null +++ b/src/system/unpriv-ip @@ -0,0 +1,2 @@ +#!/bin/sh +sudo /sbin/ip $* -- cgit v1.2.3 From 2e3b701f9045c4a557c99ee311c11d4a6f4f7b78 Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Fri, 6 Sep 2019 18:02:40 +0200 Subject: openvpn: T1548: cleanup import statements --- src/conf_mode/interface-openvpn.py | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/src/conf_mode/interface-openvpn.py b/src/conf_mode/interface-openvpn.py index 5f5cbd67d..fe08cd51e 100755 --- a/src/conf_mode/interface-openvpn.py +++ b/src/conf_mode/interface-openvpn.py @@ -18,15 +18,15 @@ import os import re -import pwd -import grp import sys import stat -import copy import jinja2 -import psutil -from ipaddress import ip_address,ip_network,IPv4Interface +from copy import deepcopy +from grp import getgrnam +from ipaddress import ip_address,ip_network,IPv4Interface +from psutil import pid_exists +from pwd import getpwnam from signal import SIGUSR1 from subprocess import Popen, PIPE @@ -302,8 +302,8 @@ def openvpn_mkdir(directory): # fix permissions - corresponds to mode 755 os.chmod(directory, stat.S_IRWXU|stat.S_IRGRP|stat.S_IXGRP|stat.S_IROTH|stat.S_IXOTH) - uid = pwd.getpwnam(user).pw_uid - gid = grp.getgrnam(group).gr_gid + uid = getpwnam(user).pw_uid + gid = getgrnam(group).gr_gid os.chown(directory, uid, gid) def fixup_permission(filename, permission=stat.S_IRUSR): @@ -315,8 +315,8 @@ def fixup_permission(filename, permission=stat.S_IRUSR): os.chmod(filename, permission) # make file owned by root / vyattacfg - uid = pwd.getpwnam('root').pw_uid - gid = grp.getgrnam('vyattacfg').gr_gid + uid = getpwnam('root').pw_uid + gid = getgrnam('vyattacfg').gr_gid os.chown(filename, uid, gid) def checkCertHeader(header, filename): @@ -335,7 +335,7 @@ def checkCertHeader(header, filename): return False def get_config(): - openvpn = copy.deepcopy(default_config_data) + openvpn = deepcopy(default_config_data) conf = Config() # determine tagNode instance @@ -793,8 +793,8 @@ def generate(openvpn): fixup_permission(auth_file) # get numeric uid/gid - uid = pwd.getpwnam(user).pw_uid - gid = grp.getgrnam(group).gr_gid + uid = getpwnam(user).pw_uid + gid = getgrnam(group).gr_gid # Generate client specific configuration for client in openvpn['client']: @@ -834,7 +834,7 @@ def apply(openvpn): # we only need to stop the demon if it's running # daemon could have died or killed by someone - if psutil.pid_exists(pid): + if pid_exists(pid): cmd = 'start-stop-daemon --stop --quiet' cmd += ' --pidfile ' + pidfile subprocess_cmd(cmd) @@ -857,7 +857,7 @@ def apply(openvpn): return None # Send SIGUSR1 to the process instead of creating a new process - if psutil.pid_exists(pid): + if pid_exists(pid): os.kill(pid, SIGUSR1) return None -- cgit v1.2.3 From 26ace9a7b92020bffebe85897cd3790342820612 Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Fri, 6 Sep 2019 18:09:50 +0200 Subject: openvpn: T1548: always restart OpenVPN Previous implementations sent a SIGUSR1 to OpenVPN to initialte a restart after the configuration changed - as this was the same as the client keepalive mechanism did. Unfortunately on SIGUSR1 OpenVPN does not re-read the configuration file. Thus changed options were never taken into account. --- src/conf_mode/interface-openvpn.py | 54 +++++++++++++++++++------------------- 1 file changed, 27 insertions(+), 27 deletions(-) diff --git a/src/conf_mode/interface-openvpn.py b/src/conf_mode/interface-openvpn.py index fe08cd51e..548c78535 100755 --- a/src/conf_mode/interface-openvpn.py +++ b/src/conf_mode/interface-openvpn.py @@ -25,10 +25,11 @@ import jinja2 from copy import deepcopy from grp import getgrnam from ipaddress import ip_address,ip_network,IPv4Interface +from netifaces import interfaces from psutil import pid_exists from pwd import getpwnam -from signal import SIGUSR1 from subprocess import Popen, PIPE +from time import sleep from vyos.config import Config from vyos import ConfigError @@ -819,47 +820,46 @@ def generate(openvpn): return None def apply(openvpn): - interface = openvpn['intf'] - pid = 0 - pidfile = '/var/run/openvpn/{}.pid'.format(interface) + pidfile = '/var/run/openvpn/{}.pid'.format(openvpn['intf']) if os.path.isfile(pidfile): pid = 0 with open(pidfile, 'r') as f: pid = int(f.read()) - # If tunnel interface has been deleted - stop service - if openvpn['deleted'] or openvpn['disable']: - directory = os.path.dirname(get_config_name(interface)) - - # we only need to stop the demon if it's running - # daemon could have died or killed by someone - if pid_exists(pid): - cmd = 'start-stop-daemon --stop --quiet' - cmd += ' --pidfile ' + pidfile - subprocess_cmd(cmd) + # Always stop OpenVPN service. We can not send a SIGUSR1 for restart of the + # service as the configuration is not re-read. Stop daemon only if it's + # running - it could have died or killed by someone evil + if pid_exists(pid): + cmd = 'start-stop-daemon --stop --quiet' + cmd += ' --pidfile ' + pidfile + subprocess_cmd(cmd) - # cleanup old PID file - if os.path.isfile(pidfile): - os.remove(pidfile) + # cleanup old PID file + if os.path.isfile(pidfile): + os.remove(pidfile) + # Do some cleanup when OpenVPN is disabled/deleted + if openvpn['deleted'] or openvpn['disable']: # cleanup old configuration file - if os.path.isfile(get_config_name(interface)): - os.remove(get_config_name(interface)) + if os.path.isfile(get_config_name(openvpn['intf'])): + os.remove(get_config_name(openvpn['intf'])) # cleanup client config dir - if os.path.isdir(directory + '/ccd/' + interface): + directory = os.path.dirname(get_config_name(openvpn['intf'])) + if os.path.isdir(directory + '/ccd/' + openvpn['intf']): try: - os.remove(directory + '/ccd/' + interface + '/*') + os.remove(directory + '/ccd/' + openvpn['intf'] + '/*') except: pass return None - # Send SIGUSR1 to the process instead of creating a new process - if pid_exists(pid): - os.kill(pid, SIGUSR1) - return None + # On configuration change we need to wait for the 'old' interface to + # vanish from the Kernel, if it is not gone, OpenVPN will report: + # ERROR: Cannot ioctl TUNSETIFF vtun10: Device or resource busy (errno=16) + while openvpn['intf'] in interfaces(): + sleep(0.250) # 250ms # No matching OpenVPN process running - maybe it got killed or none # existed - nevertheless, spawn new OpenVPN process @@ -868,13 +868,13 @@ def apply(openvpn): cmd += ' --exec /usr/sbin/openvpn' # now pass arguments to openvpn binary cmd += ' --' - cmd += ' --config ' + get_config_name(interface) + cmd += ' --config ' + get_config_name(openvpn['intf']) # execute assembled command subprocess_cmd(cmd) - return None + if __name__ == '__main__': try: c = get_config() -- 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(-) 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 a9756cfd49b169e93afe70415ce9155ebf4e5ffa Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Sat, 7 Sep 2019 14:18:23 +0200 Subject: bridge: bonding: minor comment cleanup --- src/conf_mode/interface-bonding.py | 2 +- src/conf_mode/interface-bridge.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/conf_mode/interface-bonding.py b/src/conf_mode/interface-bonding.py index bce8f98fb..cd4c447ac 100755 --- a/src/conf_mode/interface-bonding.py +++ b/src/conf_mode/interface-bonding.py @@ -142,7 +142,7 @@ def get_config(): 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 + # address is no longer valid and needs to be removed eff_addr = conf.return_effective_values('address') bond['address_remove'] = list_diff(eff_addr, bond['address']) diff --git a/src/conf_mode/interface-bridge.py b/src/conf_mode/interface-bridge.py index 8996fceec..401182a0d 100755 --- a/src/conf_mode/interface-bridge.py +++ b/src/conf_mode/interface-bridge.py @@ -71,7 +71,7 @@ def get_config(): 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 + # address is no longer valid and needs to be removed eff_addr = conf.return_effective_values('address') bridge['address_remove'] = list_diff(eff_addr, bridge['address']) -- cgit v1.2.3 From 6f666f0a62fb98fcab800be813141f44dd1ab8a7 Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Sat, 7 Sep 2019 15:30:53 +0200 Subject: bonding: T1614: bugfix in validate - enslave failed Forgot to exclude our current bond interface in the search for duplicate interface enslavement. --- src/conf_mode/interface-bonding.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/conf_mode/interface-bonding.py b/src/conf_mode/interface-bonding.py index cd4c447ac..dc0363fb7 100755 --- a/src/conf_mode/interface-bonding.py +++ b/src/conf_mode/interface-bonding.py @@ -272,11 +272,14 @@ def verify(bond): conf = Config() for intf in bond['member']: - # a bond member is only allowed to be assigned to any one bond - for tmp in conf.list_nodes('interfaces bonding'): + # a bonding member interface is only allowed to be assigned to one bond! + all_bonds = conf.list_nodes('interfaces bonding') + # We do not need to check our own bond + all_bonds.remove(bond['intf']) + for tmp in all_bonds: if conf.exists('interfaces bonding ' + tmp + ' member interface ' + intf): raise ConfigError('can not enslave interface {} which already ' \ - 'belongs to bond {}'.format(intf, tmp)) + 'belongs to {}'.format(intf, tmp)) # we can not add disabled slave interfaces to our bond if conf.exists('interfaces ethernet ' + intf + ' disable'): -- 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(-) 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 From 1017c8103f12ebd6db4f250d8a154571fff32db1 Mon Sep 17 00:00:00 2001 From: hagbard Date: Mon, 9 Sep 2019 11:55:54 -0700 Subject: [wireguard]: T1572 - Wireguard keyPair per interface - param key location added in op-mode script - param delkey and listkey implemented in op-mode script - param delkey implemented in op-mode script - generate and store named keys - interface implementation tu use cli option 'private-key' --- Makefile | 1 + interface-definitions/interfaces-wireguard.xml | 8 ++ op-mode-definitions/wireguard.xml | 53 ++++++- src/conf_mode/interface-wireguard.py | 11 +- src/op_mode/wireguard.py | 188 ++++++++++++++++--------- 5 files changed, 188 insertions(+), 73 deletions(-) diff --git a/Makefile b/Makefile index d7b3f047d..ad05acff5 100644 --- a/Makefile +++ b/Makefile @@ -42,6 +42,7 @@ op_mode_definitions: rm -f $(OP_TMPL_DIR)/generate/node.def rm -f $(OP_TMPL_DIR)/show/vpn/node.def rm -f $(OP_TMPL_DIR)/show/system/node.def + rm -f $(OP_TMPL_DIR)/delete/node.def .PHONY: all all: clean interface_definitions op_mode_definitions diff --git a/interface-definitions/interfaces-wireguard.xml b/interface-definitions/interfaces-wireguard.xml index 6e2622018..f2a7cc316 100644 --- a/interface-definitions/interfaces-wireguard.xml +++ b/interface-definitions/interfaces-wireguard.xml @@ -77,6 +77,14 @@ + + + Private key to use on that interface + + + + + peer alias diff --git a/op-mode-definitions/wireguard.xml b/op-mode-definitions/wireguard.xml index fa5e4a206..785af202c 100644 --- a/op-mode-definitions/wireguard.xml +++ b/op-mode-definitions/wireguard.xml @@ -20,6 +20,12 @@ ${vyos_op_scripts_dir}/wireguard.py --genpsk + + + Generates named wireguard keypairs + + sudo ${vyos_op_scripts_dir}/wireguard.py --genkey --location "$4" + @@ -33,7 +39,7 @@ - show wireguard public key + Show wireguard public key ${vyos_op_scripts_dir}/wireguard.py --showpub @@ -43,6 +49,31 @@ ${vyos_op_scripts_dir}/wireguard.py --showpriv + + + Shows named wireguard keys + + + + + Show wireguard private named key + + + + + ${vyos_op_scripts_dir}/wireguard.py --showpub --location "$5" + + + + Show wireguard public named key + + + + + ${vyos_op_scripts_dir}/wireguard.py --showpriv --location "$5" + + + @@ -81,5 +112,25 @@ + + + + + Delete wireguard properties + + + + + Delete wireguard named keypair + + + + + sudo ${vyos_op_scripts_dir}/wireguard.py --delkdir --location "$4" + + + + + diff --git a/src/conf_mode/interface-wireguard.py b/src/conf_mode/interface-wireguard.py index 4c0e90ca6..0f9e66aa6 100755 --- a/src/conf_mode/interface-wireguard.py +++ b/src/conf_mode/interface-wireguard.py @@ -29,6 +29,9 @@ from vyos.ifconfig import WireGuardIf ifname = str(os.environ['VYOS_TAGNODE_VALUE']) intfc = WireGuardIf(ifname) +kdir = r'/config/auth/wireguard' + + def check_kmod(): if not os.path.exists('/sys/module/wireguard'): sl.syslog(sl.LOG_NOTICE, "loading wirguard kmod") @@ -52,7 +55,7 @@ def get_config(): 'fwmark': 0x00, 'mtu': 1420, 'peer': {}, - 'pk' : '/config/auth/wireguard/private.key' + 'pk': '{}/private.key'.format(kdir) } } @@ -77,6 +80,9 @@ def get_config(): ifname + ' description') if c.exists(ifname + ' mtu'): config_data[ifname]['mtu'] = c.return_value(ifname + ' mtu') + if c.exists(ifname + ' private-key'): + config_data[ifname]['pk'] = "{0}/{1}/private.key".format( + kdir, c.return_value(ifname + ' private-key')) if c.exists(ifname + ' peer'): for p in c.list_nodes(ifname + ' peer'): if not c.exists(ifname + ' peer ' + p + ' disable'): @@ -107,13 +113,14 @@ def get_config(): return config_data + def verify(c): if not c: return None if not os.path.exists(c[ifname]['pk']): raise ConfigError( - "No keys found, generate them by executing: \'run generate wireguard keypair\'") + "No keys found, generate them by executing: \'run generate wireguard [keypair|named-keypairs]\'") if c[ifname]['status'] != 'delete': if not c[ifname]['addr']: diff --git a/src/op_mode/wireguard.py b/src/op_mode/wireguard.py index 66622c04c..e48da2e40 100755 --- a/src/op_mode/wireguard.py +++ b/src/op_mode/wireguard.py @@ -19,91 +19,139 @@ import argparse import os import sys +import shutil import subprocess import syslog as sl + from vyos import ConfigError dir = r'/config/auth/wireguard' -pk = dir + '/private.key' -pub = dir + '/public.key' psk = dir + '/preshared.key' + def check_kmod(): - """ check if kmod is loaded, if not load it """ - 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_ERR, "modprobe wireguard failed") - raise ConfigError("modprobe wireguard failed") - -def generate_keypair(): - """ generates a keypair which is stored in /config/auth/wireguard """ - ret = subprocess.call(['wg genkey | tee ' + pk + '|wg pubkey > ' + pub], shell=True) - if ret != 0: - raise ConfigError("wireguard key-pair generation failed") - else: - sl.syslog(sl.LOG_NOTICE, "new keypair wireguard key generated in " + dir) - -def genkey(): - """ helper function to check, regenerate the keypair """ - old_umask = os.umask(0o077) - if os.path.exists(pk) and os.path.exists(pub): - try: - choice = input("You already have a wireguard key-pair already, do you want to re-generate? [y/n] ") - if choice == 'y' or choice == 'Y': - generate_keypair() - except KeyboardInterrupt: - sys.exit(0) - else: - """ if keypair is bing executed from a running iso """ - if not os.path.exists(dir): - os.umask(old_umask) - subprocess.call(['sudo mkdir -p ' + dir], shell=True) - subprocess.call(['sudo chgrp vyattacfg ' + dir], shell=True) - subprocess.call(['sudo chmod 770 ' + dir], shell=True) - generate_keypair() - os.umask(old_umask) + """ check if kmod is loaded, if not load it """ + 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_ERR, "modprobe wireguard failed") + raise ConfigError("modprobe wireguard failed") -def showkey(key): - """ helper function to show privkey or pubkey """ - if key == "pub": - if os.path.exists(pub): - print ( open(pub).read().strip() ) + +def generate_keypair(pk, pub): + """ generates a keypair which is stored in /config/auth/wireguard """ + old_umask = os.umask(0o027) + ret = subprocess.call( + ['wg genkey | tee ' + pk + '|wg pubkey > ' + pub], shell=True) + if ret != 0: + raise ConfigError("wireguard key-pair generation failed") else: - print("no public key found") + sl.syslog( + sl.LOG_NOTICE, "new keypair wireguard key generated in " + dir) + os.umask(old_umask) - if key == "pk": - if os.path.exists(pk): - print ( open(pk).read().strip() ) + +def genkey(location): + """ helper function to check, regenerate the keypair """ + pk = "{}/private.key".format(location) + pub = "{}/public.key".format(location) + old_umask = os.umask(0o027) + if os.path.exists(pk) and os.path.exists(pub): + try: + choice = input( + "You already have a wireguard key-pair, do you want to re-generate? [y/n] ") + if choice == 'y' or choice == 'Y': + generate_keypair(pk, pub) + except KeyboardInterrupt: + sys.exit(0) + else: + """ if keypair is bing executed from a running iso """ + if not os.path.exists(location): + subprocess.call(['sudo mkdir -p ' + location], shell=True) + subprocess.call(['sudo chgrp vyattacfg ' + location], shell=True) + subprocess.call(['sudo chmod 750 ' + location], shell=True) + generate_keypair(pk, pub) + os.umask(old_umask) + + +def showkey(key): + """ helper function to show privkey or pubkey """ + if os.path.exists(key): + print (open(key).read().strip()) else: - print("no private key found") + print ("{} not found".format(key)) + def genpsk(): - """ generates a preshared key and shows it on stdout, it's stroed only in the config """ - subprocess.call(['wg genpsk'], shell=True) + """ + generates a preshared key and shows it on stdout, + it's stored only in the cli config + """ + + subprocess.call(['wg genpsk'], shell=True) + + +def list_key_dirs(): + """ lists all dirs under /config/auth/wireguard """ + if os.path.exists(dir): + nks = next(os.walk(dir))[1] + for nk in nks: + print (nk) + + +def del_key_dir(kname): + """ deletes /config/auth/wireguard/ """ + kdir = "{0}/{1}".format(dir, kname) + if not os.path.isdir(kdir): + print ("named keypair {} not found".format(kname)) + return 1 + shutil.rmtree(kdir) + if __name__ == '__main__': - check_kmod() - - parser = argparse.ArgumentParser(description='wireguard key management') - parser.add_argument('--genkey', action="store_true", help='generate key-pair') - parser.add_argument('--showpub', action="store_true", help='shows public key') - parser.add_argument('--showpriv', action="store_true", help='shows private key') - parser.add_argument('--genpsk', action="store_true", help='generates preshared-key') - args = parser.parse_args() - - try: - if args.genkey: - genkey() - if args.showpub: - showkey("pub") - if args.showpriv: - showkey("pk") - if args.genpsk: - genpsk() - - except ConfigError as e: - print(e) - sys.exit(1) + check_kmod() + parser = argparse.ArgumentParser(description='wireguard key management') + parser.add_argument( + '--genkey', action="store_true", help='generate key-pair') + parser.add_argument( + '--showpub', action="store_true", help='shows public key') + parser.add_argument( + '--showpriv', action="store_true", help='shows private key') + parser.add_argument( + '--genpsk', action="store_true", help='generates preshared-key') + parser.add_argument( + '--location', action="store", help='key location within {}'.format(dir)) + parser.add_argument( + '--listkdir', action="store_true", help='lists named keydirectories') + parser.add_argument( + '--delkdir', action="store_true", help='removes named keydirectories') + args = parser.parse_args() + + try: + if args.genkey: + if args.location: + genkey("{0}/{1}".format(dir, args.location)) + else: + genkey(dir) + + if args.showpub: + if args.location: + showkey("{0}/{1}/public.key".format(dir, args.location)) + else: + showkey("{}/public.key".format(dir)) + if args.showpriv: + if args.location: + showkey("{0}/{1}/private.key".format(dir, args.location)) + else: + showkey("{}/private".format(dir)) + if args.genpsk: + genpsk() + if args.listkdir: + list_key_dirs() + if args.delkdir: + del_key_dir(args.location) + except ConfigError as e: + print(e) + sys.exit(1) -- cgit v1.2.3 From db07e6fa76d90eaf80a06729753fb89266437674 Mon Sep 17 00:00:00 2001 From: hagbard Date: Tue, 10 Sep 2019 11:28:53 -0700 Subject: [wireguard]: T1650 - cli option to delete default wg key --- op-mode-definitions/wireguard.xml | 6 +++--- src/conf_mode/interface-wireguard.py | 7 ++----- src/op_mode/wireguard.py | 18 +++++++++--------- 3 files changed, 14 insertions(+), 17 deletions(-) diff --git a/op-mode-definitions/wireguard.xml b/op-mode-definitions/wireguard.xml index 785af202c..3c54c81bd 100644 --- a/op-mode-definitions/wireguard.xml +++ b/op-mode-definitions/wireguard.xml @@ -12,7 +12,7 @@ generate a wireguard keypair - ${vyos_op_scripts_dir}/wireguard.py --genkey + sudo ${vyos_op_scripts_dir}/wireguard.py --genkey @@ -119,9 +119,9 @@ Delete wireguard properties - + - Delete wireguard named keypair + Delete a wireguard keypair diff --git a/src/conf_mode/interface-wireguard.py b/src/conf_mode/interface-wireguard.py index 0f9e66aa6..d51a7a08d 100755 --- a/src/conf_mode/interface-wireguard.py +++ b/src/conf_mode/interface-wireguard.py @@ -31,7 +31,6 @@ intfc = WireGuardIf(ifname) kdir = r'/config/auth/wireguard' - def check_kmod(): if not os.path.exists('/sys/module/wireguard'): sl.syslog(sl.LOG_NOTICE, "loading wirguard kmod") @@ -55,7 +54,7 @@ def get_config(): 'fwmark': 0x00, 'mtu': 1420, 'peer': {}, - 'pk': '{}/private.key'.format(kdir) + 'pk' : '{}/default/private.key'.format(kdir) } } @@ -81,8 +80,7 @@ def get_config(): if c.exists(ifname + ' mtu'): config_data[ifname]['mtu'] = c.return_value(ifname + ' mtu') if c.exists(ifname + ' private-key'): - config_data[ifname]['pk'] = "{0}/{1}/private.key".format( - kdir, c.return_value(ifname + ' private-key')) + config_data[ifname]['pk'] = "{0}/{1}/private.key".format(kdir,c.return_value(ifname + ' private-key')) if c.exists(ifname + ' peer'): for p in c.list_nodes(ifname + ' peer'): if not c.exists(ifname + ' peer ' + p + ' disable'): @@ -113,7 +111,6 @@ def get_config(): return config_data - def verify(c): if not c: return None diff --git a/src/op_mode/wireguard.py b/src/op_mode/wireguard.py index e48da2e40..4e93ec6aa 100755 --- a/src/op_mode/wireguard.py +++ b/src/op_mode/wireguard.py @@ -91,18 +91,16 @@ def genpsk(): subprocess.call(['wg genpsk'], shell=True) - def list_key_dirs(): - """ lists all dirs under /config/auth/wireguard """ + """ lists all dirs under /config/auth/wireguard """ if os.path.exists(dir): nks = next(os.walk(dir))[1] for nk in nks: print (nk) - def del_key_dir(kname): """ deletes /config/auth/wireguard/ """ - kdir = "{0}/{1}".format(dir, kname) + kdir = "{0}/{1}".format(dir,kname) if not os.path.isdir(kdir): print ("named keypair {} not found".format(kname)) return 1 @@ -133,24 +131,26 @@ if __name__ == '__main__': if args.location: genkey("{0}/{1}".format(dir, args.location)) else: - genkey(dir) - + genkey("{}/default".format(dir)) if args.showpub: if args.location: showkey("{0}/{1}/public.key".format(dir, args.location)) else: - showkey("{}/public.key".format(dir)) + showkey("{}/default/public.key".format(dir)) if args.showpriv: if args.location: showkey("{0}/{1}/private.key".format(dir, args.location)) else: - showkey("{}/private".format(dir)) + showkey("{}/default/private.key".format(dir)) if args.genpsk: genpsk() if args.listkdir: list_key_dirs() if args.delkdir: - del_key_dir(args.location) + if args.location: + del_key_dir(args.location) + else: + del_key_dir("default") except ConfigError as e: print(e) -- cgit v1.2.3 From e85c90bf3188c24c6a88c6a96a0d7fc30c221905 Mon Sep 17 00:00:00 2001 From: hagbard Date: Tue, 10 Sep 2019 12:16:46 -0700 Subject: [wireguard] - remove 'show wireguard keypair' in favor for 'show wireguard keypairs...' --- op-mode-definitions/wireguard.xml | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/op-mode-definitions/wireguard.xml b/op-mode-definitions/wireguard.xml index 3c54c81bd..c509dcad3 100644 --- a/op-mode-definitions/wireguard.xml +++ b/op-mode-definitions/wireguard.xml @@ -8,19 +8,13 @@ wireguard key generation utility - - - generate a wireguard keypair - - sudo ${vyos_op_scripts_dir}/wireguard.py --genkey - generate a wireguard preshared key ${vyos_op_scripts_dir}/wireguard.py --genpsk - + Generates named wireguard keypairs -- cgit v1.2.3 From a237bd44396f849f7f3c35d2ea6928edb1c93ea4 Mon Sep 17 00:00:00 2001 From: hagbard Date: Tue, 10 Sep 2019 12:31:49 -0700 Subject: Revert "[wireguard] - remove 'show wireguard keypair'" This reverts commit e85c90bf3188c24c6a88c6a96a0d7fc30c221905. --- op-mode-definitions/wireguard.xml | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/op-mode-definitions/wireguard.xml b/op-mode-definitions/wireguard.xml index c509dcad3..3c54c81bd 100644 --- a/op-mode-definitions/wireguard.xml +++ b/op-mode-definitions/wireguard.xml @@ -8,13 +8,19 @@ wireguard key generation utility + + + generate a wireguard keypair + + sudo ${vyos_op_scripts_dir}/wireguard.py --genkey + generate a wireguard preshared key ${vyos_op_scripts_dir}/wireguard.py --genpsk - + Generates named wireguard keypairs -- cgit v1.2.3 From 4f7b5ed06e85ad4b8810223e49c6f4e6645e73a2 Mon Sep 17 00:00:00 2001 From: hagbard Date: Tue, 10 Sep 2019 12:37:31 -0700 Subject: [wireguard] - clean up duplicated op options --- op-mode-definitions/wireguard.xml | 18 +++--------------- 1 file changed, 3 insertions(+), 15 deletions(-) diff --git a/op-mode-definitions/wireguard.xml b/op-mode-definitions/wireguard.xml index 3c54c81bd..c5c4c9914 100644 --- a/op-mode-definitions/wireguard.xml +++ b/op-mode-definitions/wireguard.xml @@ -8,9 +8,9 @@ wireguard key generation utility - + - generate a wireguard keypair + generates the wireguard default-keypair sudo ${vyos_op_scripts_dir}/wireguard.py --genkey @@ -37,19 +37,7 @@ Show wireguard properties - - - Show wireguard public key - - ${vyos_op_scripts_dir}/wireguard.py --showpub - - - - show wireguard private key - - ${vyos_op_scripts_dir}/wireguard.py --showpriv - - + Shows named wireguard keys -- cgit v1.2.3 From d34fd745438951d55c5c4899b2b3c7bfa5d08026 Mon Sep 17 00:00:00 2001 From: hagbard Date: Tue, 10 Sep 2019 14:56:37 -0700 Subject: [syslog] - T1597: 'del system system' stops now rsyslog --- src/conf_mode/syslog.py | 418 +++++++++++++++++++++++++----------------------- 1 file changed, 221 insertions(+), 197 deletions(-) diff --git a/src/conf_mode/syslog.py b/src/conf_mode/syslog.py index 7b79c701b..c4f3d2c9c 100755 --- a/src/conf_mode/syslog.py +++ b/src/conf_mode/syslog.py @@ -24,16 +24,16 @@ import jinja2 from vyos.config import Config from vyos import ConfigError -########### config templates +# config templates -#### /etc/rsyslog.d/vyos-rsyslog.conf ### +# /etc/rsyslog.d/vyos-rsyslog.conf ### configs = ''' ## generated by syslog.py ## ## file based logging {% if files['global']['marker'] -%} $ModLoad immark {% if files['global']['marker-interval'] %} -$MarkMessagePeriod {{files['global']['marker-interval']}} +$MarkMessagePeriod {{files['global']['marker-interval']}} {% endif %} {% endif -%} {% if files['global']['preserver_fqdn'] -%} @@ -80,217 +80,241 @@ logrotate_configs = ''' } {% endfor %} ''' -############# config templates end +# config templates end + def get_config(): - c = Config() - if not c.exists('system syslog'): - return None - c.set_level('system syslog') - - config_data = { - 'files' : {}, - 'console' : {}, - 'hosts' : {}, - 'user' : {} - } - - ##### - # /etc/rsyslog.d/vyos-rsyslog.conf - # 'set system syslog global' - ##### - config_data['files'].update( - { - 'global' : { - 'log-file' : '/var/log/messages', - 'max-size' : 262144, - 'action-on-max-size' : '/usr/sbin/logrotate /etc/logrotate.d/vyos-rsyslog', - 'selectors' : '*.notice;local7.debug', - 'max-files' : '5', - 'preserver_fqdn' : False - } - } - ) - - if c.exists('global marker'): - config_data['files']['global']['marker'] = True - if c.exists('global marker interval'): - config_data['files']['global']['marker-interval'] = c.return_value('global marker interval') - if c.exists('global facility'): - config_data['files']['global']['selectors'] = generate_selectors(c, 'global facility') - if c.exists('global archive size'): - config_data['files']['global']['max-size'] = int(c.return_value('global archive size'))* 1024 - if c.exists('global archive file'): - config_data['files']['global']['max-files'] = c.return_value('global archive file') - if c.exists('global preserve-fqdn'): - config_data['files']['global']['preserver_fqdn'] = True - - ### - # set system syslog file - ### - - if c.exists('file'): - filenames = c.list_nodes('file') - for filename in filenames: - config_data['files'].update( - { - filename : { - 'log-file' : '/var/log/user/' + filename, - 'max-files' : '5', - 'action-on-max-size' : '/usr/sbin/logrotate /etc/logrotate.d/' + filename, - 'selectors' : '*.err', - 'max-size' : 262144 - } - } - ) - - if c.exists('file ' + filename + ' facility'): - config_data['files'][filename]['selectors'] = generate_selectors(c, 'file ' + filename + ' facility') - if c.exists('file ' + filename + ' archive size'): - config_data['files'][filename]['max-size'] = int(c.return_value('file ' + filename + ' archive size'))* 1024 - if c.exists('file ' + filename + ' archive files'): - config_data['files'][filename]['max-files'] = c.return_value('file ' + filename + ' archive files') - - ## set system syslog console - if c.exists('console'): - config_data['console'] = { - '/dev/console' : { - 'selectors' : '*.err' - } + c = Config() + if not c.exists('system syslog'): + return None + c.set_level('system syslog') + + config_data = { + 'files': {}, + 'console': {}, + 'hosts': {}, + 'user': {} } - - for f in c.list_nodes('console facility'): - if c.exists('console facility ' + f + ' level'): - config_data['console'] = { - '/dev/console' : { - 'selectors' : generate_selectors(c, 'console facility') - } - } - - ## set system syslog host - if c.exists('host'): - proto = 'udp' - rhosts = c.list_nodes('host') - for rhost in rhosts: - for fac in c.list_nodes('host ' + rhost + ' facility'): - if c.exists('host ' + rhost + ' facility ' + fac + ' protocol'): - proto = c.return_value('host ' + rhost + ' facility ' + fac + ' protocol') - - config_data['hosts'].update( + + # + # /etc/rsyslog.d/vyos-rsyslog.conf + # 'set system syslog global' + # + config_data['files'].update( { - rhost : { - 'selectors' : generate_selectors(c, 'host ' + rhost + ' facility'), - 'proto' : proto - } + 'global': { + 'log-file': '/var/log/messages', + 'max-size': 262144, + 'action-on-max-size': '/usr/sbin/logrotate /etc/logrotate.d/vyos-rsyslog', + 'selectors': '*.notice;local7.debug', + 'max-files': '5', + 'preserver_fqdn': False + } } - ) + ) - ## set system syslog user - if c.exists('user'): - usrs = c.list_nodes('user') - for usr in usrs: - config_data['user'].update( - { - usr : { - 'selectors' : generate_selectors(c, 'user ' + usr + ' facility') - } + if c.exists('global marker'): + config_data['files']['global']['marker'] = True + if c.exists('global marker interval'): + config_data['files']['global'][ + 'marker-interval'] = c.return_value('global marker interval') + if c.exists('global facility'): + config_data['files']['global'][ + 'selectors'] = generate_selectors(c, 'global facility') + if c.exists('global archive size'): + config_data['files']['global']['max-size'] = int( + c.return_value('global archive size')) * 1024 + if c.exists('global archive file'): + config_data['files']['global'][ + 'max-files'] = c.return_value('global archive file') + if c.exists('global preserve-fqdn'): + config_data['files']['global']['preserver_fqdn'] = True + + # + # set system syslog file + # + + if c.exists('file'): + filenames = c.list_nodes('file') + for filename in filenames: + config_data['files'].update( + { + filename: { + 'log-file': '/var/log/user/' + filename, + 'max-files': '5', + 'action-on-max-size': '/usr/sbin/logrotate /etc/logrotate.d/' + filename, + 'selectors': '*.err', + 'max-size': 262144 + } + } + ) + + if c.exists('file ' + filename + ' facility'): + config_data['files'][filename]['selectors'] = generate_selectors( + c, 'file ' + filename + ' facility') + if c.exists('file ' + filename + ' archive size'): + config_data['files'][filename]['max-size'] = int( + c.return_value('file ' + filename + ' archive size')) * 1024 + if c.exists('file ' + filename + ' archive files'): + config_data['files'][filename]['max-files'] = c.return_value( + 'file ' + filename + ' archive files') + + # set system syslog console + if c.exists('console'): + config_data['console'] = { + '/dev/console': { + 'selectors': '*.err' + } } - ) - - return config_data + + for f in c.list_nodes('console facility'): + if c.exists('console facility ' + f + ' level'): + config_data['console'] = { + '/dev/console': { + 'selectors': generate_selectors(c, 'console facility') + } + } + + # set system syslog host + if c.exists('host'): + proto = 'udp' + rhosts = c.list_nodes('host') + for rhost in rhosts: + for fac in c.list_nodes('host ' + rhost + ' facility'): + if c.exists('host ' + rhost + ' facility ' + fac + ' protocol'): + proto = c.return_value( + 'host ' + rhost + ' facility ' + fac + ' protocol') + + config_data['hosts'].update( + { + rhost: { + 'selectors': generate_selectors(c, 'host ' + rhost + ' facility'), + 'proto': proto + } + } + ) + + # set system syslog user + if c.exists('user'): + usrs = c.list_nodes('user') + for usr in usrs: + config_data['user'].update( + { + usr: { + 'selectors': generate_selectors(c, 'user ' + usr + ' facility') + } + } + ) + + return config_data + def generate_selectors(c, config_node): -## protocols and security are being mapped here -## for backward compatibility with old configs -## security and protocol mappings can be removed later - if c.is_tag(config_node): - nodes = c.list_nodes(config_node) - selectors = "" - for node in nodes: - lvl = c.return_value( config_node + ' ' + node + ' level') - if lvl == None: - lvl = "err" - if lvl == 'all': - lvl = '*' - if node == 'all' and node != nodes[-1]: - selectors += "*." + lvl + ";" - elif node == 'all': - selectors += "*." + lvl - elif node != nodes[-1]: - if node == 'protocols': - node = 'local7' - if node == 'security': - node = 'auth' - selectors += node + "." + lvl + ";" - else: - if node == 'protocols': - node = 'local7' - if node == 'security': - node = 'auth' - selectors += node + "." + lvl - return selectors +# protocols and security are being mapped here +# for backward compatibility with old configs +# security and protocol mappings can be removed later + if c.is_tag(config_node): + nodes = c.list_nodes(config_node) + selectors = "" + for node in nodes: + lvl = c.return_value(config_node + ' ' + node + ' level') + if lvl == None: + lvl = "err" + if lvl == 'all': + lvl = '*' + if node == 'all' and node != nodes[-1]: + selectors += "*." + lvl + ";" + elif node == 'all': + selectors += "*." + lvl + elif node != nodes[-1]: + if node == 'protocols': + node = 'local7' + if node == 'security': + node = 'auth' + selectors += node + "." + lvl + ";" + else: + if node == 'protocols': + node = 'local7' + if node == 'security': + node = 'auth' + selectors += node + "." + lvl + return selectors + def generate(c): - if c == None: - return None + if c == None: + return None - tmpl = jinja2.Template(configs, trim_blocks=True) - config_text = tmpl.render(c) - with open('/etc/rsyslog.d/vyos-rsyslog.conf', 'w') as f: - f.write(config_text) + tmpl = jinja2.Template(configs, trim_blocks=True) + config_text = tmpl.render(c) + with open('/etc/rsyslog.d/vyos-rsyslog.conf', 'w') as f: + f.write(config_text) + + # eventually write for each file its own logrotate file, since size is + # defined it shouldn't matter + tmpl = jinja2.Template(logrotate_configs, trim_blocks=True) + config_text = tmpl.render(c) + with open('/etc/logrotate.d/vyos-rsyslog', 'w') as f: + f.write(config_text) - ## eventually write for each file its own logrotate file, since size is defined it shouldn't matter - tmpl = jinja2.Template(logrotate_configs, trim_blocks=True) - config_text = tmpl.render(c) - with open('/etc/logrotate.d/vyos-rsyslog', 'w') as f: - f.write(config_text) def verify(c): - if c == None: - return None - # - # /etc/rsyslog.conf is generated somewhere and copied over the original (exists in /opt/vyatta/etc/rsyslog.conf) - # it interferes with the global logging, to make sure we are using a single base, template is enforced here - # - if not os.path.islink('/etc/rsyslog.conf'): - os.remove('/etc/rsyslog.conf') - os.symlink('/usr/share/vyos/templates/rsyslog/rsyslog.conf', '/etc/rsyslog.conf') - - # /var/log/vyos-rsyslog were the old files, we may want to clean those up, but currently there - # is a chance that someone still needs it, so I don't automatically remove them - - if c == None: - return None - - fac = ['*','auth','authpriv','cron','daemon','kern','lpr','mail','mark','news','protocols','security',\ - 'syslog','user','uucp','local0','local1','local2','local3','local4','local5','local6','local7'] - lvl = ['emerg','alert','crit','err','warning','notice','info','debug','*'] - - for conf in c: - if c[conf]: - for item in c[conf]: - for s in c[conf][item]['selectors'].split(";"): - f = re.sub("\..*$","",s) - if f not in fac: - print (c[conf]) - raise ConfigError('Invalid facility ' + s + ' set in '+ conf + ' ' + item) - l = re.sub("^.+\.","",s) - if l not in lvl: - raise ConfigError('Invalid logging level ' + s + ' set in '+ conf + ' ' + item) + if c == None: + return None + # + # /etc/rsyslog.conf is generated somewhere and copied over the original (exists in /opt/vyatta/etc/rsyslog.conf) + # it interferes with the global logging, to make sure we are using a single base, template is enforced here + # + if not os.path.islink('/etc/rsyslog.conf'): + os.remove('/etc/rsyslog.conf') + os.symlink( + '/usr/share/vyos/templates/rsyslog/rsyslog.conf', '/etc/rsyslog.conf') + + # /var/log/vyos-rsyslog were the old files, we may want to clean those up, but currently there + # is a chance that someone still needs it, so I don't automatically remove + # them + + if c == None: + return None + + fac = [ + '*', 'auth', 'authpriv', 'cron', 'daemon', 'kern', 'lpr', 'mail', 'mark', 'news', 'protocols', 'security', + 'syslog', 'user', 'uucp', 'local0', 'local1', 'local2', 'local3', 'local4', 'local5', 'local6', 'local7'] + lvl = ['emerg', 'alert', 'crit', 'err', + 'warning', 'notice', 'info', 'debug', '*'] + + for conf in c: + if c[conf]: + for item in c[conf]: + for s in c[conf][item]['selectors'].split(";"): + f = re.sub("\..*$", "", s) + if f not in fac: + print (c[conf]) + raise ConfigError( + 'Invalid facility ' + s + ' set in ' + conf + ' ' + item) + l = re.sub("^.+\.", "", s) + if l not in lvl: + raise ConfigError( + 'Invalid logging level ' + s + ' set in ' + conf + ' ' + item) + def apply(c): - if not os.path.exists('/var/run/rsyslogd.pid'): - os.system("sudo systemctl start rsyslog >/dev/null") - else: - os.system("sudo systemctl restart rsyslog >/dev/null") + if not c and os.path.exists('/var/run/rsyslogd.pid'): + os.system("sudo systemctl stop syslog.socket") + os.system("sudo systemctl stop rsyslog") + else: + if not os.path.exists('/var/run/rsyslogd.pid'): + os.system("sudo systemctl start rsyslog >/dev/null") + else: + os.system("sudo systemctl restart rsyslog >/dev/null") if __name__ == '__main__': - try: - c = get_config() - verify(c) - generate(c) - apply(c) - except ConfigError as e: - print(e) - sys.exit(1) + try: + c = get_config() + verify(c) + generate(c) + apply(c) + except ConfigError as e: + print(e) + sys.exit(1) -- cgit v1.2.3 From 59e5e64cfbb67a5eb1a9d4d21dd54d946897b8d7 Mon Sep 17 00:00:00 2001 From: Daniil Baturin Date: Wed, 11 Sep 2019 23:14:23 +0200 Subject: T1598: annotate the vyos-hostsd unit file. --- src/systemd/vyos-hostsd.service | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/systemd/vyos-hostsd.service b/src/systemd/vyos-hostsd.service index 2444f5352..731e570c9 100644 --- a/src/systemd/vyos-hostsd.service +++ b/src/systemd/vyos-hostsd.service @@ -1,6 +1,12 @@ [Unit] Description=VyOS DNS configuration keeper + +# Without this option, lots of default dependencies are added, +# among them network.target, which creates a dependency cycle DefaultDependencies=no + +# Seemingly sensible way to say "as early as the system is ready" +# All vyos-hostsd needs is read/write mounted root After=systemd-remount-fs.service [Service] @@ -13,9 +19,13 @@ SyslogFacility=daemon Restart=on-failure -# Does't work but leave it here +# Does't work in Jessie but leave it here User=root Group=vyattacfg [Install] + +# Note: After= doesn't actually create a dependency, +# it just sets order for the case when both services are to start, +# and without RequiredBy it *does not* set vyos-hostsd to start. RequiredBy=cloud-init-local.service vyos-router.service -- cgit v1.2.3