diff options
Diffstat (limited to 'python/vyos')
-rw-r--r-- | python/vyos/configdict.py | 116 | ||||
-rw-r--r-- | python/vyos/configinterface.py | 153 | ||||
-rw-r--r-- | python/vyos/configsession.py | 3 | ||||
-rw-r--r-- | python/vyos/configtree.py | 8 | ||||
-rw-r--r-- | python/vyos/defaults.py | 2 | ||||
-rw-r--r-- | python/vyos/hostsd_client.py | 69 | ||||
-rw-r--r-- | python/vyos/ifconfig.py | 1449 | ||||
-rw-r--r-- | python/vyos/interfaceconfig.py | 376 | ||||
-rw-r--r-- | python/vyos/interfaces.py | 11 | ||||
-rw-r--r-- | python/vyos/util.py | 23 | ||||
-rw-r--r-- | python/vyos/validate.py | 6 |
11 files changed, 1686 insertions, 530 deletions
diff --git a/python/vyos/configdict.py b/python/vyos/configdict.py index 157011839..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): """ @@ -78,3 +79,118 @@ 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] + + +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/python/vyos/configinterface.py b/python/vyos/configinterface.py deleted file mode 100644 index 0f5b0842c..000000000 --- a/python/vyos/configinterface.py +++ /dev/null @@ -1,153 +0,0 @@ -# Copyright 2019 VyOS maintainers and contributors <maintainers@vyos.io> -# -# 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 <http://www.gnu.org/licenses/>. - -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): - 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)) - - 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 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/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() 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/python/vyos/hostsd_client.py b/python/vyos/hostsd_client.py new file mode 100644 index 000000000..f009aba98 --- /dev/null +++ b/python/vyos/hostsd_client.py @@ -0,0 +1,69 @@ +import json + +import zmq + + +SOCKET_PATH = "ipc:///run/vyos-hostsd.sock" + + +class VyOSHostsdError(Exception): + pass + + +class Client(object): + def __init__(self): + 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): + 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']) + else: + return reply["data"] + except zmq.error.Again: + raise VyOSHostsdError("Could not connect to vyos-hostsd") + + 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) + + def get_name_servers(self, tag): + msg = {'type': 'name_servers', 'op': 'get', 'tag': tag} + return self._communicate(msg) + diff --git a/python/vyos/ifconfig.py b/python/vyos/ifconfig.py new file mode 100644 index 000000000..62bf94d79 --- /dev/null +++ b/python/vyos/ifconfig.py @@ -0,0 +1,1449 @@ +# Copyright 2019 VyOS maintainers and contributors <maintainers@vyos.io> +# +# 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 <http://www.gnu.org/licenses/>. + +import os +import re +import subprocess +import jinja2 + +from vyos.validate import * +from ipaddress import IPv4Network, IPv6Address +from netifaces import ifaddresses, AF_INET, AF_INET6 +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, type=None): + """ + 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. + + 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') + """ + 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)) + + 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 + 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 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 + deconfigures all assigned IP addresses and clear possible DHCP(v6) + client processes. + + Example: + >>> from vyos.ifconfig import Interface + >>> i = Interface('eth0') + >>> 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() + + # 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("cmd '{}'".format(command)) + + 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. + """ + value = None + with open(filename, 'r') as f: + value = f.read().rstrip('\n') + + 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. + """ + self._debug_msg("write '{}' > '{}'".format(value, filename)) + with open(filename, 'w') as f: + f.write(str(value)) + + return None + + @property + def mtu(self): + """ + Get/set interface mtu in bytes. + + Example: + >>> from vyos.ifconfig import Interface + >>> Interface('eth0').mtu + '1500' + """ + return self._read_sysfs('/sys/class/net/{0}/mtu' + .format(self._ifname)) + + @mtu.setter + 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)) + + return self._write_sysfs('/sys/class/net/{0}/mtu' + .format(self._ifname), mtu) + + @property + def mac(self): + """ + Get/set interface mac address + + Example: + >>> from vyos.ifconfig import Interface + >>> Interface('eth0').mac + '00:0c:29:11:aa:cc' + """ + return self._read_sysfs('/sys/class/net/{0}/address' + .format(self._ifname)) + + @mac.setter + 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 + '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 arp_cache_tmo(self): + """ + 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 + '30' + """ + 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): + """ + Set ARP cache timeout value in seconds. Internal Kernel representation + is in milliseconds. + + Example: + >>> from vyos.ifconfig import Interface + >>> Interface('eth0').arp_cache_tmo = '40' + """ + 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): + """ + How does the kernel act when receiving packets on 'down' interfaces + + Example: + >>> from vyos.ifconfig import Interface + >>> Interface('eth0').link_detect + '0' + """ + return self._read_sysfs('/proc/sys/net/ipv4/conf/{0}/link_filter' + .format(self._ifname)) + + @link_detect.setter + def link_detect(self, link_filter): + """ + 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' + """ + 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) + else: + raise ValueError("Value out of range") + + @property + def ifalias(self): + """ + Get/set interface alias name + + Example: + + >>> from vyos.ifconfig import Interface + >>> Interface('eth0').ifalias + '' + """ + return self._read_sysfs('/sys/class/net/{0}/ifalias' + .format(self._ifname)) + + @ifalias.setter + def ifalias(self, ifalias=None): + """ + Get/set interface alias name + + Example: + >>> from vyos.ifconfig import Interface + >>> Interface('eth0').ifalias = 'VyOS upstream interface' + >>> Interface('eth0').ifalias + 'VyOS upstream interface' + + to clear interface alias e.g. delete it use: + + >>> Interface('eth0').ifalias = '' + >>> Interface('eth0').ifalias + '' + """ + if not ifalias: + # clear interface alias + ifalias = '\0' + + self._write_sysfs('/sys/class/net/{0}/ifalias' + .format(self._ifname), ifalias) + + @property + def state(self): + """ + Enable (up) / Disable (down) an interface + + Example: + >>> from vyos.ifconfig import Interface + >>> Interface('eth0').state + 'up' + """ + return self._read_sysfs('/sys/class/net/{0}/operstate' + .format(self._ifname)) + + @state.setter + 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"') + + 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) + 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): + """ + 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('eth0').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): + """ + 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 + >>> j = Interface('eth0') + >>> 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 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): + """ + 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 + >>> j = Interface('eth0') + >>> 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 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): + """ + Configure interface as DHCP client. The dhclient binary is automatically + started in background! + + 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: + 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) + + 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): + """ + 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: + 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): + """ + 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) + + 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) + + # 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): + """ + 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 + + # 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 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') + + @property + def ageing_time(self): + """ + 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 + '300' + """ + return (self._read_sysfs('/sys/class/net/{0}/bridge/ageing_time' + .format(self._ifname)) / 100) + + @ageing_time.setter + def ageing_time(self, time): + """ + 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 + """ + time = int(time) * 100 + 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. Internal Kernel representation + is in centiseconds. + + Example: + >>> from vyos.ifconfig import Interface + >>> BridgeIf('br0').ageing_time + '3' + """ + return (self._read_sysfs('/sys/class/net/{0}/bridge/forward_delay' + .format(self._ifname)) / 100) + + @forward_delay.setter + def forward_delay(self, time): + """ + Set bridge forwarding delay in seconds. Internal Kernel representation + is in centiseconds. + + Example: + >>> from vyos.ifconfig import Interface + >>> BridgeIf('br0').forward_delay = 15 + """ + 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. Internal Kernel representation + is in centiseconds. + + Example: + >>> from vyos.ifconfig import Interface + >>> BridgeIf('br0').hello_time + '2' + """ + return (self._read_sysfs('/sys/class/net/{0}/bridge/hello_time' + .format(self._ifname)) / 100) + + @hello_time.setter + def hello_time(self, time): + """ + Set bridge hello time in seconds. Internal Kernel representation + is in centiseconds. + + Example: + >>> from vyos.ifconfig import Interface + >>> BridgeIf('br0').hello_time = 2 + """ + 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. Internal Kernel representation + is in centiseconds. + + Example: + >>> from vyos.ifconfig import Interface + >>> BridgeIf('br0').max_age + '20' + """ + + return (self._read_sysfs('/sys/class/net/{0}/bridge/max_age' + .format(self._ifname)) / 100) + + @max_age.setter + def max_age(self, time): + """ + Set bridge max message age in seconds. Internal Kernel representation + is in centiseconds. + + Example: + >>> from vyos.ifconfig import Interface + >>> BridgeIf('br0').max_age = 30 + """ + return self._write_sysfs('/sys/class/net/{0}/bridge/max_age' + .format(self._ifname), (int(time) * 100)) + + @property + def priority(self): + """ + Get bridge max aging time in seconds. + + Example: + >>> from vyos.ifconfig import Interface + >>> BridgeIf('br0').priority + '32768' + """ + return self._read_sysfs('/sys/class/net/{0}/bridge/priority' + .format(self._ifname)) + + @priority.setter + def priority(self, priority): + """ + Set bridge max aging time in seconds. + + Example: + >>> from vyos.ifconfig import Interface + >>> BridgeIf('br0').priority = 8192 + """ + return self._write_sysfs('/sys/class/net/{0}/bridge/priority' + .format(self._ifname), priority) + + @property + def stp_state(self): + """ + Get current bridge STP (Spanning Tree) 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): + """ + 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: + return self._write_sysfs('/sys/class/net/{0}/bridge/stp_state' + .format(self._ifname), state) + else: + raise ValueError("Value out of range") + + @property + def multicast_querier(self): + """ + Get bridge multicast querier membership state. + + Example: + >>> from vyos.ifconfig import Interface + >>> BridgeIf('br0').multicast_querier + '0' + """ + return self._read_sysfs('/sys/class/net/{0}/bridge/multicast_querier' + .format(self._ifname)) + + @multicast_querier.setter + 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 + 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: + return self._write_sysfs('/sys/class/net/{0}/bridge/multicast_querier' + .format(self._ifname), enable) + else: + raise ValueError("Value out of range") + + def add_port(self, interface): + """ + Add physical interface to bridge (member port) + + Example: + >>> from vyos.ifconfig import Interface + >>> BridgeIf('br0').add_port('eth0') + >>> BridgeIf('br0').add_port('eth1') + """ + cmd = 'ip link set dev {} master {}'.format(interface, self._ifname) + self._cmd(cmd) + + def del_port(self, interface): + """ + Remove member port from bridge instance. + + Example: + >>> from vyos.ifconfig import Interface + >>> BridgeIf('br0').del_port('eth1') + """ + 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 + + Example: + + >>> 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) + + def set_priority(self, interface, priority): + """ + Set interface path priority, only relevant for STP enabled interfaces + + Example: + + >>> from vyos.ifconfig import Interface + >>> Interface('eth0').priority(4) + """ + return self._write_sysfs('/sys/class/net/{}/brif/{}/priority' + .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 + 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') + + @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("Value out of range") + 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) + + @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), 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') + """ + # 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), '+' + interface) + + 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), '-' + 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())) + + @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) + + +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.config = { + 'port': 0, + 'private-key': None, + 'pubkey': None, + 'psk': '/dev/null', + 'allowed-ips': [], + 'fwmark': 0x00, + 'endpoint': None, + 'keepalive': 0 + } + + 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.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.config['allowed-ips']: + if aip != self.config['allowed-ips'][-1]: + cmd += aip + "," + else: + cmd += aip + 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.config['psk'] != '/dev/null': + if os.path.exists(self.config['psk']): + os.remove(self.config['psk']) + + + 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 = "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: + 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']) + + 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. + + 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': '' + } + return config diff --git a/python/vyos/interfaceconfig.py b/python/vyos/interfaceconfig.py deleted file mode 100644 index b8bfb707e..000000000 --- a/python/vyos/interfaceconfig.py +++ /dev/null @@ -1,376 +0,0 @@ -#!/usr/bin/python3 - -# Copyright 2019 VyOS maintainers and contributors <maintainers@vyos.io> -# -# 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 <http://www.gnu.org/licenses/>. - -import sys -import os -import re -import json -import socket -import subprocess - -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) - - self._ifname = str(ifname) - - - @property - def mtu(self): - return self._mtu - - @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): - return self._macaddr - - @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 self._ifalias - - @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): - return self._linkstate - - @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): - 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): - 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): - 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): - """ - returns either up/down or None if it can't find the state - """ - 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_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_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) - 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/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)) diff --git a/python/vyos/util.py b/python/vyos/util.py index 6ab606983..67a602f7a 100644 --- a/python/vyos/util.py +++ b/python/vyos/util.py @@ -15,9 +15,11 @@ import os import re +import getpass import grp import time import subprocess +import sys import psutil @@ -176,3 +178,24 @@ 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") + + +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/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 |