diff options
Diffstat (limited to 'python/vyos')
-rw-r--r-- | python/vyos/configdict.py | 15 | ||||
-rw-r--r-- | python/vyos/ifconfig/bond.py | 6 | ||||
-rw-r--r-- | python/vyos/ifconfig/control.py | 10 | ||||
-rw-r--r-- | python/vyos/ifconfig/geneve.py | 2 | ||||
-rw-r--r-- | python/vyos/ifconfig/interface.py | 158 | ||||
-rw-r--r-- | python/vyos/ifconfig/l2tpv3.py | 4 | ||||
-rw-r--r-- | python/vyos/ifconfig/tunnel.py | 4 | ||||
-rw-r--r-- | python/vyos/ifconfig_vlan.py | 10 | ||||
-rw-r--r-- | python/vyos/util.py | 50 |
9 files changed, 209 insertions, 50 deletions
diff --git a/python/vyos/configdict.py b/python/vyos/configdict.py index a1499479a..66da52ff3 100644 --- a/python/vyos/configdict.py +++ b/python/vyos/configdict.py @@ -123,6 +123,9 @@ def vlan_to_dict(conf): 'ip_enable_arp_accept': 0, 'ip_enable_arp_announce': 0, 'ip_enable_arp_ignore': 0, + 'ipv6_autoconf': 0, + 'ipv6_forwarding': 1, + 'ipv6_dup_addr_detect': 1, 'ingress_qos': '', 'ingress_qos_changed': False, 'mac': '', @@ -187,6 +190,18 @@ def vlan_to_dict(conf): if conf.exists('ip enable-arp-ignore'): vlan['ip_enable_arp_ignore'] = 1 + # Enable acquisition of IPv6 address using stateless autoconfig (SLAAC) + if conf.exists('ipv6 address autoconf'): + vlan['ipv6_autoconf'] = 1 + + # Disable IPv6 forwarding on this interface + if conf.exists('ipv6 disable-forwarding'): + vlan['ipv6_forwarding'] = 0 + + # IPv6 Duplicate Address Detection (DAD) tries + if conf.exists('ipv6 dup-addr-detect-transmits'): + vlan['ipv6_dup_addr_detect'] = int(conf.return_value('ipv6 dup-addr-detect-transmits')) + # Media Access Control (MAC) address if conf.exists('mac'): vlan['mac'] = conf.return_value('mac') diff --git a/python/vyos/ifconfig/bond.py b/python/vyos/ifconfig/bond.py index 3c26b9b95..e2ff71490 100644 --- a/python/vyos/ifconfig/bond.py +++ b/python/vyos/ifconfig/bond.py @@ -101,7 +101,7 @@ class BondIf(Interface): for s in self.get_slaves(): slave = { 'ifname': s, - 'state': Interface(s).get_state() + 'state': Interface(s).get_admin_state() } slave_list.append(slave) @@ -112,7 +112,7 @@ class BondIf(Interface): # physical interface for slave in slave_list: i = Interface(slave['ifname']) - i.set_state(slave['state']) + i.set_admin_state(slave['state']) def set_hash_policy(self, mode): """ @@ -211,7 +211,7 @@ class BondIf(Interface): # 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).set_state('down') + Interface(interface).set_admin_state('down') return self.set_interface('bond_add_port', f'+{interface}') def del_port(self, interface): diff --git a/python/vyos/ifconfig/control.py b/python/vyos/ifconfig/control.py index f7b032478..1c9f7e284 100644 --- a/python/vyos/ifconfig/control.py +++ b/python/vyos/ifconfig/control.py @@ -49,15 +49,12 @@ class Control(Register): Using the defined names, set data write to sysfs. """ cmd = self._command_get[name]['shellcmd'].format(**config) - return self._cmd(cmd) + return self._command_get[name].get('format', lambda _: _)(self._cmd(cmd)) def _set_command(self, config, name, value): """ Using the defined names, set data write to sysfs. """ - if not value and not self._command_set[name].get('force', False): - return None - # the code can pass int as int value = str(value) @@ -75,7 +72,7 @@ class Control(Register): config = {**config, **{'value': value}} cmd = self._command_set[name]['shellcmd'].format(**config) - return self._cmd(cmd) + return self._command_set[name].get('format', lambda _: _)(self._cmd(cmd)) _sysfs_get = {} _sysfs_set = {} @@ -115,9 +112,6 @@ class Control(Register): """ Using the defined names, set data write to sysfs. """ - if not value and not self._sysfs_set[name].get('force', False): - return None - # the code can pass int as int value = str(value) diff --git a/python/vyos/ifconfig/geneve.py b/python/vyos/ifconfig/geneve.py index f27786417..0c1cdade9 100644 --- a/python/vyos/ifconfig/geneve.py +++ b/python/vyos/ifconfig/geneve.py @@ -49,7 +49,7 @@ class GeneveIf(Interface): self._cmd(cmd) # interface is always A/D down. It needs to be enabled explicitly - self.set_state('down') + self.set_admin_state('down') @classmethod def get_config(cls): diff --git a/python/vyos/ifconfig/interface.py b/python/vyos/ifconfig/interface.py index f2b43fd35..8b41d6158 100644 --- a/python/vyos/ifconfig/interface.py +++ b/python/vyos/ifconfig/interface.py @@ -21,10 +21,10 @@ import time from copy import deepcopy from vyos.validate import * # should not * include -from vyos.config import Config # not used anymore +from vyos.util import mac2eui64 from vyos import ConfigError -from ipaddress import IPv4Network, IPv6Address +from ipaddress import IPv4Network, IPv6Address, IPv6Network from netifaces import ifaddresses, AF_INET, AF_INET6 from time import sleep from os.path import isfile @@ -49,8 +49,15 @@ class Interface(DHCP): 'bridgeable': False, } + _command_get = { + 'admin_state': { + 'shellcmd': 'ip -json link show dev {ifname}', + 'format': lambda j: 'up' if 'UP' in json.loads(j)[0]['flags'] else 'down', + } + } + _command_set = { - 'state': { + 'admin_state': { 'validate': lambda v: assert_list(v, ['up', 'down']), 'shellcmd': 'ip link set dev {ifname} {value}', }, @@ -59,19 +66,24 @@ class Interface(DHCP): 'shellcmd': 'ip link set dev {ifname} address {value}', }, 'vrf': { - 'force': True, 'convert': lambda v: f'master {v}' if v else 'nomaster', 'shellcmd': 'ip link set dev {ifname} {value}', }, } _sysfs_get = { + 'alias': { + 'location': '/sys/class/net/{ifname}/ifalias', + }, 'mac': { 'location': '/sys/class/net/{ifname}/address', }, 'mtu': { 'location': '/sys/class/net/{ifname}/mtu', }, + 'oper_state':{ + 'location': '/sys/class/net/{ifname}/operstate', + }, } _sysfs_set = { @@ -103,6 +115,18 @@ class Interface(DHCP): 'validate': assert_boolean, 'location': '/proc/sys/net/ipv4/conf/{ifname}/arp_ignore', }, + 'ipv6_autoconf': { + 'validate': lambda fwd: assert_range(fwd,0,2), + 'location': '/proc/sys/net/ipv6/conf/{ifname}/autoconf', + }, + 'ipv6_forwarding': { + 'validate': lambda fwd: assert_range(fwd,0,2), + 'location': '/proc/sys/net/ipv6/conf/{ifname}/forwarding', + }, + 'ipv6_dad_transmits': { + 'validate': assert_positive, + 'location': '/proc/sys/net/ipv6/conf/{ifname}/dad_transmits', + }, 'proxy_arp': { 'validate': assert_boolean, 'location': '/proc/sys/net/ipv4/conf/{ifname}/proxy_arp', @@ -249,7 +273,7 @@ class Interface(DHCP): >>> Interface('eth0').get_mac() '00:50:ab:cd:ef:00' """ - self.get_interface('mac') + return self.get_interface('mac') def set_mac(self, mac): """ @@ -265,9 +289,9 @@ class Interface(DHCP): return None # MAC address can only be changed if interface is in 'down' state - prev_state = self.get_state() + prev_state = self.get_admin_state() if prev_state == 'up': - self.set_state('down') + self.set_admin_state('down') self.set_interface('mac', mac) @@ -362,6 +386,81 @@ class Interface(DHCP): """ return self.set_interface('arp_ignore', arp_ignore) + def set_ipv6_autoconf(self, autoconf): + """ + Autoconfigure addresses using Prefix Information in Router + Advertisements. + """ + return self.set_interface('ipv6_autoconf', autoconf) + + def set_ipv6_eui64_address(self, prefix): + """ + Extended Unique Identifier (EUI), as per RFC2373, allows a host to + assign iteslf a unique IPv6 address based on a given IPv6 prefix. + + If prefix is passed address is assigned, if prefix is '' address is + removed from interface. + """ + # if prefix is an empty string convert it to None so mac2eui64 works + # as expected + if not prefix: + prefix = None + + eui64 = mac2eui64(self.get_mac(), prefix) + + if not prefix: + # if prefix is empty - thus removed - we need to walk through all + # interface IPv6 addresses and find the one with the calculated + # EUI-64 identifier. The address is then removed + for addr in self.get_addr(): + addr_wo_prefix = addr.split('/')[0] + if is_ipv6(addr_wo_prefix): + if eui64 in IPv6Address(addr_wo_prefix).exploded: + self.del_addr(addr) + + return None + + # calculate and add EUI-64 IPv6 address + if IPv6Network(prefix): + # we also need to take the subnet length into account + prefix = prefix.split('/')[1] + eui64 = f'{eui64}/{prefix}' + self.add_addr(eui64 ) + + def set_ipv6_forwarding(self, forwarding): + """ + Configure IPv6 interface-specific Host/Router behaviour. + + False: + + By default, Host behaviour is assumed. This means: + + 1. IsRouter flag is not set in Neighbour Advertisements. + 2. If accept_ra is TRUE (default), transmit Router + Solicitations. + 3. If accept_ra is TRUE (default), accept Router + Advertisements (and do autoconfiguration). + 4. If accept_redirects is TRUE (default), accept Redirects. + + True: + + If local forwarding is enabled, Router behaviour is assumed. + This means exactly the reverse from the above: + + 1. IsRouter flag is set in Neighbour Advertisements. + 2. Router Solicitations are not sent unless accept_ra is 2. + 3. Router Advertisements are ignored unless accept_ra is 2. + 4. Redirects are ignored. + """ + return self.set_interface('ipv6_forwarding', forwarding) + + def set_ipv6_dad_messages(self, dad): + """ + The amount of Duplicate Address Detection probes to send. + Default: 1 + """ + return self.set_interface('ipv6_dad_transmits', dad) + def set_link_detect(self, link_filter): """ Configure kernel response in packets received on interfaces that are 'down' @@ -384,6 +483,16 @@ class Interface(DHCP): """ return self.set_interface('link_detect', link_filter) + def get_alias(self): + """ + Get interface alias name used by e.g. SNMP + + Example: + >>> Interface('eth0').get_alias() + 'interface description as set by user' + """ + return self.get_interface('alias') + def set_alias(self, ifalias=''): """ Set interface alias name used by e.g. SNMP @@ -398,36 +507,41 @@ class Interface(DHCP): """ self.set_interface('alias', ifalias) - def get_state(self): + def get_admin_state(self): """ Get interface administrative state. Function will return 'up' or 'down' Example: >>> from vyos.ifconfig import Interface - >>> Interface('eth0').get_state() + >>> Interface('eth0').get_admin_state() 'up' """ - cmd = 'ip -json link show dev {}'.format(self.config['ifname']) - tmp = self._cmd(cmd) - out = json.loads(tmp) - - state = 'down' - if 'UP' in out[0]['flags']: - state = 'up' + return self.get_interface('admin_state') - return state - - def set_state(self, state): + def set_admin_state(self, state): """ Set interface administrative state to be 'up' or 'down' Example: >>> from vyos.ifconfig import Interface - >>> Interface('eth0').set_state('down') - >>> Interface('eth0').get_state() + >>> Interface('eth0').set_admin_state('down') + >>> Interface('eth0').get_admin_state() 'down' """ - return self.set_interface('state', state) + return self.set_interface('admin_state', state) + + def get_oper_state(self): + """ + Get interface operational state + + Example: + >>> from vyos.ifconfig import Interface + >>> Interface('eth0').get_oper_sate() + 'up' + """ + # https://www.kernel.org/doc/Documentation/ABI/testing/sysfs-class-net + # "unknown", "notpresent", "down", "lowerlayerdown", "testing", "dormant", "up" + return self.get_interface('oper_state') def set_proxy_arp(self, enable): """ diff --git a/python/vyos/ifconfig/l2tpv3.py b/python/vyos/ifconfig/l2tpv3.py index fbfab4c6e..07f1cf8a3 100644 --- a/python/vyos/ifconfig/l2tpv3.py +++ b/python/vyos/ifconfig/l2tpv3.py @@ -62,7 +62,7 @@ class L2TPv3If(Interface): self._cmd(cmd) # interface is always A/D down. It needs to be enabled explicitly - self.set_state('down') + self.set_admin_state('down') def remove(self): """ @@ -76,7 +76,7 @@ class L2TPv3If(Interface): if os.path.exists('/sys/class/net/{}'.format(self.config['ifname'])): # interface is always A/D down. It needs to be enabled explicitly - self.set_state('down') + self.set_admin_state('down') if self._config['tunnel_id'] and self._config['session_id']: cmd = 'ip l2tp del session tunnel_id {} '.format( diff --git a/python/vyos/ifconfig/tunnel.py b/python/vyos/ifconfig/tunnel.py index a49bdd51c..1bbb9eb6a 100644 --- a/python/vyos/ifconfig/tunnel.py +++ b/python/vyos/ifconfig/tunnel.py @@ -98,10 +98,10 @@ class _Tunnel(Interface): options = " ".join(["{} {}".format(k, self.config[k]) for k in self.options if k in self.config and self.config[k]]) self._cmd('{} {}'.format(self.create.format(**self.config), options)) - self.set_interface('state', 'down') + self.set_admin_state('down') def _delete(self): - self.set_interface('state', 'down') + self.set_admin_state('down') cmd = self.delete.format(**self.config) return self._cmd(cmd) diff --git a/python/vyos/ifconfig_vlan.py b/python/vyos/ifconfig_vlan.py index 2b934cdfc..00270cf58 100644 --- a/python/vyos/ifconfig_vlan.py +++ b/python/vyos/ifconfig_vlan.py @@ -64,6 +64,12 @@ def apply_vlan_config(vlan, config): vlan.set_arp_announce(config['ip_enable_arp_announce']) # configure ARP ignore vlan.set_arp_ignore(config['ip_enable_arp_ignore']) + # IPv6 address autoconfiguration + vlan.set_ipv6_autoconf(config['ipv6_autoconf']) + # IPv6 forwarding + vlan.set_ipv6_forwarding(config['ipv6_forwarding']) + # IPv6 Duplicate Address Detection (DAD) tries + vlan.set_ipv6_dad_messages(config['ipv6_dup_addr_detect']) # Maximum Transmission Unit (MTU) vlan.set_mtu(config['mtu']) @@ -76,9 +82,9 @@ def apply_vlan_config(vlan, config): # enable/disable VLAN interface if config['disable']: - vlan.set_state('down') + vlan.set_admin_state('down') else: - vlan.set_state('up') + vlan.set_admin_state('up') # Configure interface address(es) # - not longer required addresses get removed first diff --git a/python/vyos/util.py b/python/vyos/util.py index 67a602f7a..635b11ee5 100644 --- a/python/vyos/util.py +++ b/python/vyos/util.py @@ -15,16 +15,16 @@ import os import re -import getpass -import grp -import time -import subprocess import sys - import psutil import vyos.defaults +from getpass import getuser +from grp import getgrnam +from time import sleep +from subprocess import check_output +from ipaddress import ip_network def read_file(path): """ Read a file to string """ @@ -32,6 +32,7 @@ def read_file(path): data = f.read().strip() return data + def colon_separated_to_dict(data_string, uniquekeys=False): """ Converts a string containing newline-separated entries of colon-separated key-value pairs into a dict. @@ -80,12 +81,14 @@ def colon_separated_to_dict(data_string, uniquekeys=False): return data + def process_running(pid_file): """ Checks if a process with PID in pid_file is running """ with open(pid_file, 'r') as f: pid = f.read().strip() return psutil.pid_exists(int(pid)) + def seconds_to_human(s, separator=""): """ Converts number of seconds passed to a human-readable interval such as 1w4d18h35m59s @@ -125,10 +128,12 @@ def seconds_to_human(s, separator=""): return result + def get_cfg_group_id(): - group_data = grp.getgrnam(vyos.defaults.cfg_group) + group_data = getgrnam(vyos.defaults.cfg_group) return group_data.gr_gid + def file_is_persistent(path): if not re.match(r'^(/config|/opt/vyatta/etc/config)', os.path.dirname(path)): warning = "Warning: file {0} is outside the /config directory\n".format(path) @@ -137,6 +142,7 @@ def file_is_persistent(path): else: return (True, None) + def commit_in_progress(): """ Not to be used in normal op mode scripts! """ @@ -154,7 +160,7 @@ def commit_in_progress(): # Since this will be used in scripts that modify the config outside of the CLI # framework, those knowingly have root permissions. # For everything else, we add a safeguard. - id = subprocess.check_output(['/usr/bin/id', '-u']).decode().strip() + id = check_output(['/usr/bin/id', '-u']).decode().strip() if id != '0': raise OSError("This functions needs root permissions to return correct results") @@ -171,12 +177,14 @@ def commit_in_progress(): # Default case return False + def wait_for_commit_lock(): """ Not to be used in normal op mode scripts! """ # Very synchronous approach to multiprocessing while commit_in_progress(): - time.sleep(1) + sleep(1) + def ask_yes_no(question, default=False) -> bool: """Ask a yes/no question via input() and return their answer.""" @@ -196,6 +204,28 @@ def ask_yes_no(question, default=False) -> bool: def is_admin() -> bool: """Look if current user is in sudo group""" - current_user = getpass.getuser() - (_, _, _, admin_group_members) = grp.getgrnam('sudo') + current_user = getuser() + (_, _, _, admin_group_members) = getgrnam('sudo') return current_user in admin_group_members + + +def mac2eui64(mac, prefix=None): + ''' + Convert a MAC address to a EUI64 address or, with prefix provided, a full + IPv6 address. + Thankfully copied from https://gist.github.com/wido/f5e32576bb57b5cc6f934e177a37a0d3 + ''' + # http://tools.ietf.org/html/rfc4291#section-2.5.1 + eui64 = re.sub(r'[.:-]', '', mac).lower() + eui64 = eui64[0:6] + 'fffe' + eui64[6:] + eui64 = hex(int(eui64[0:2], 16) ^ 2)[2:].zfill(2) + eui64[2:] + + if prefix is None: + return ':'.join(re.findall(r'.{4}', eui64)) + else: + try: + net = ip_network(prefix, strict=False) + euil = int('0x{0}'.format(eui64), 16) + return str(net[euil]) + except: # pylint: disable=bare-except + return |