diff options
Diffstat (limited to 'python/vyos')
-rw-r--r-- | python/vyos/airbag.py | 17 | ||||
-rw-r--r-- | python/vyos/component_versions.py | 2 | ||||
-rw-r--r-- | python/vyos/config.py | 66 | ||||
-rw-r--r-- | python/vyos/configdict.py | 425 | ||||
-rw-r--r-- | python/vyos/debug.py | 37 | ||||
-rw-r--r-- | python/vyos/dicts.py | 3 | ||||
-rw-r--r-- | python/vyos/ifconfig/dhcp.py | 129 | ||||
-rw-r--r-- | python/vyos/ifconfig/interface.py | 161 | ||||
-rw-r--r-- | python/vyos/ifconfig/section.py | 21 | ||||
-rw-r--r-- | python/vyos/ifconfig/vlan.py | 40 | ||||
-rw-r--r-- | python/vyos/ifconfig/wireguard.py | 2 | ||||
-rw-r--r-- | python/vyos/ifconfig_vlan.py | 159 | ||||
-rw-r--r-- | python/vyos/template.py | 10 | ||||
-rw-r--r-- | python/vyos/util.py | 167 | ||||
-rw-r--r-- | python/vyos/validate.py | 68 |
15 files changed, 915 insertions, 392 deletions
diff --git a/python/vyos/airbag.py b/python/vyos/airbag.py index 6698aa404..b7838d8a2 100644 --- a/python/vyos/airbag.py +++ b/python/vyos/airbag.py @@ -26,6 +26,17 @@ from vyos.version import get_full_version_data DISABLE = False +_noteworthy = [] + +def noteworthy(msg): + """ + noteworthy can be use to take note things which we may not want to + report to the user may but be worth including in bug report + if something goes wrong later on + """ + _noteworthy.append(msg) + + # emulate a file object class _IO(object): def __init__(self, std, log): @@ -58,11 +69,16 @@ def bug_report(dtype, value, trace): information = get_full_version_data() trace = '\n'.join(format_exception(dtype, value, trace)).replace('\n\n','\n') + note = '' + if _noteworthy: + note = 'noteworthy:\n' + note += '\n'.join(_noteworthy) information.update({ 'date': datetime.now().strftime('%Y-%m-%d %H:%M:%S'), 'trace': trace, 'instructions': COMMUNITY if 'rolling' in get_version() else SUPPORTED, + 'note': note, }) sys.stdout.write(INTRO.format(**information)) @@ -145,6 +161,7 @@ Hardware S/N: {hardware_serial} Hardware UUID: {hardware_uuid} {trace} +{note} """ INTRO = """\ diff --git a/python/vyos/component_versions.py b/python/vyos/component_versions.py index ec54a1576..90b458aae 100644 --- a/python/vyos/component_versions.py +++ b/python/vyos/component_versions.py @@ -51,7 +51,7 @@ def get_component_versions_from_file(config_file_name='/opt/vyatta/etc/config/co """ f = open(config_file_name, 'r') for line_in_config in f: - component_version = return_version(line_in_config) + component_version = get_component_version(line_in_config) if component_version: return component_version raise ValueError("no config string in file:", config_file_name) diff --git a/python/vyos/config.py b/python/vyos/config.py index 75055a603..54cb518c3 100644 --- a/python/vyos/config.py +++ b/python/vyos/config.py @@ -97,12 +97,12 @@ class Config(object): self.__session_env = None # Running config can be obtained either from op or conf mode, it always succeeds - # (if config system is initialized at all). + # once the config system is initialized during boot; + # before initialization, set to empty string if os.path.isfile('/tmp/vyos-config-status'): running_config_text = self._run([self._cli_shell_api, '--show-active-only', '--show-show-defaults', '--show-ignore-edit', 'showConfig']) else: - with open('/opt/vyatta/etc/config/config.boot') as f: - running_config_text = f.read() + running_config_text = '' # Session config ("active") only exists in conf mode. # In op mode, we'll just use the same running config for both active and session configs. @@ -112,7 +112,10 @@ class Config(object): session_config_text = running_config_text self._session_config = vyos.configtree.ConfigTree(session_config_text) - self._running_config = vyos.configtree.ConfigTree(running_config_text) + if running_config_text: + self._running_config = vyos.configtree.ConfigTree(running_config_text) + else: + self._running_config = None def _make_command(self, op, path): args = path.split() @@ -155,7 +158,7 @@ class Config(object): ``exists("system name-server"`` without ``set_level``. Args: - path (str): relative config path + path (str|list): relative config path """ # Make sure there's always a space between default path (level) # and path supplied as method argument @@ -166,7 +169,7 @@ class Config(object): else: self._level = [] elif isinstance(path, list): - self._level = path + self._level = path.copy() else: raise TypeError("Level path must be either a whitespace-separated string or a list") @@ -177,7 +180,7 @@ class Config(object): Returns: str: current edit level """ - return(self._level) + return(self._level.copy()) def exists(self, path): """ @@ -278,8 +281,12 @@ class Config(object): Returns: a dict representation of the config """ res = self.show_config(self._make_path(path), effective=effective) - config_tree = vyos.configtree.ConfigTree(res) - config_dict = json.loads(config_tree.to_json()) + if res: + config_tree = vyos.configtree.ConfigTree(res) + config_dict = json.loads(config_tree.to_json()) + else: + config_dict = {} + return config_dict def is_multi(self, path): @@ -386,7 +393,7 @@ class Config(object): values = [] if not values: - return(default) + return(default.copy()) else: return(values) @@ -407,7 +414,7 @@ class Config(object): nodes = [] if not nodes: - return(default) + return(default.copy()) else: return(nodes) @@ -425,7 +432,10 @@ class Config(object): This function is safe to use in operational mode. In configuration mode, it ignores uncommited changes. """ - return(self._running_config.exists(self._make_path(path))) + if self._running_config: + return(self._running_config.exists(self._make_path(path))) + + return False def return_effective_value(self, path, default=None): """ @@ -438,9 +448,12 @@ class Config(object): Returns: str: Node value """ - try: - value = self._running_config.return_value(self._make_path(path)) - except vyos.configtree.ConfigTreeError: + if self._running_config: + try: + value = self._running_config.return_value(self._make_path(path)) + except vyos.configtree.ConfigTreeError: + value = None + else: value = None if not value: @@ -448,7 +461,6 @@ class Config(object): else: return(value) - def return_effective_values(self, path, default=[]): """ Retrieve all values of a multi-value node in a running (effective) config @@ -459,13 +471,16 @@ class Config(object): Returns: str list: A list of values """ - try: - values = self._running_config.return_values(self._make_path(path)) - except vyos.configtree.ConfigTreeError: + if self._running_config: + try: + values = self._running_config.return_values(self._make_path(path)) + except vyos.configtree.ConfigTreeError: + values = [] + else: values = [] if not values: - return(default) + return(default.copy()) else: return(values) @@ -482,12 +497,15 @@ class Config(object): Raises: VyOSError: if the node is not a tag node """ - try: - nodes = self._running_config.list_nodes(self._make_path(path)) - except vyos.configtree.ConfigTreeError: + if self._running_config: + try: + nodes = self._running_config.list_nodes(self._make_path(path)) + except vyos.configtree.ConfigTreeError: + nodes = [] + else: nodes = [] if not nodes: - return(default) + return(default.copy()) else: return(nodes) diff --git a/python/vyos/configdict.py b/python/vyos/configdict.py index 24fe174d2..eec64e964 100644 --- a/python/vyos/configdict.py +++ b/python/vyos/configdict.py @@ -18,7 +18,13 @@ A library for retrieving value dicts from VyOS configs in a declarative fashion. """ +from enum import Enum +from copy import deepcopy + from vyos import ConfigError +from vyos.ifconfig import Interface +from vyos.validate import is_member +from vyos.util import ifname_from_config def retrieve_config(path_hash, base_path, config): """ @@ -97,172 +103,359 @@ def get_ethertype(ethertype_val): else: raise ConfigError('invalid ethertype "{}"'.format(ethertype_val)) +interface_default_data = { + 'address': [], + 'address_remove': [], + 'description': '', + 'dhcp_client_id': '', + 'dhcp_hostname': '', + 'dhcp_vendor_class_id': '', + 'dhcpv6_prm_only': False, + 'dhcpv6_temporary': False, + 'dhcpv6_pd': [], + 'disable': False, + 'disable_link_detect': 1, + 'ip_disable_arp_filter': 1, + 'ip_enable_arp_accept': 0, + 'ip_enable_arp_announce': 0, + 'ip_enable_arp_ignore': 0, + 'ip_proxy_arp': 0, + 'ipv6_accept_ra': 1, + 'ipv6_autoconf': 0, + 'ipv6_eui64_prefix': [], + 'ipv6_eui64_prefix_remove': [], + 'ipv6_forwarding': 1, + 'ipv6_dup_addr_detect': 1, + 'is_bridge_member': False, + 'mac': '', + 'mtu': 1500, + 'vrf': '' +} + +vlan_default = { + **interface_default_data, + 'egress_qos': '', + 'egress_qos_changed': False, + 'ingress_qos': '', + 'ingress_qos_changed': False, + 'vif_c': {}, + 'vif_c_remove': [] +} + +# see: https://docs.python.org/3/library/enum.html#functional-api +disable = Enum('disable','none was now both') + +def disable_state(conf, check=[3,5,7]): + """ + return if and how a particual section of the configuration is has disable'd + using "disable" including if it was disabled by one of its parent. + + check: a list of the level we should check, here 7,5 and 3 + interfaces ethernet eth1 vif-s 1 vif-c 2 disable + interfaces ethernet eth1 vif 1 disable + interfaces ethernet eth1 disable + + it returns an enum (none, was, now, both) + """ + + # save where we are in the config + current_level = conf.get_level() + + # logic to figure out if the interface (or one of it parent is disabled) + eff_disable = False + act_disable = False + + levels = check[:] + working_level = current_level[:] + + while levels: + position = len(working_level) + if not position: + break + if position not in levels: + working_level = working_level[:-1] + continue + + levels.remove(position) + conf.set_level(working_level) + working_level = working_level[:-1] + + eff_disable = eff_disable or conf.exists_effective('disable') + act_disable = act_disable or conf.exists('disable') -def vlan_to_dict(conf): + conf.set_level(current_level) + + # how the disabling changed + if eff_disable and act_disable: + return disable.both + if eff_disable and not eff_disable: + return disable.was + if not eff_disable and act_disable: + return disable.now + return disable.none + + +def intf_to_dict(conf, default): """ 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()[-1], # get the '100' in 'interfaces bonding bond0 vif-s 100' - 'address': [], - 'address_remove': [], - 'description': '', - 'dhcp_client_id': '', - 'dhcp_hostname': '', - 'dhcp_vendor_class_id': '', - 'dhcpv6_prm_only': False, - 'dhcpv6_temporary': False, - 'disable': False, - 'disable_link_detect': 1, - 'egress_qos': '', - 'egress_qos_changed': False, - 'ip_disable_arp_filter': 1, - 'ip_enable_arp_accept': 0, - 'ip_enable_arp_announce': 0, - 'ip_enable_arp_ignore': 0, - 'ip_proxy_arp': 0, - 'ipv6_autoconf': 0, - 'ipv6_forwarding': 1, - 'ipv6_dup_addr_detect': 1, - 'ingress_qos': '', - 'ingress_qos_changed': False, - 'mac': '', - 'mtu': 1500, - 'vrf': '' - } - # 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) + + intf = deepcopy(default) + intf['intf'] = ifname_from_config(conf) # retrieve interface description - if conf.exists('description'): - vlan['description'] = conf.return_value('description') + if conf.exists(['description']): + intf['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') + if conf.exists(['dhcp-options', 'client-id']): + intf['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') + if conf.exists(['dhcp-options', 'host-name']): + intf['dhcp_hostname'] = conf.return_value(['dhcp-options', 'host-name']) # DHCP client vendor identifier - if conf.exists('dhcp-options vendor-class-id'): - vlan['dhcp_vendor_class_id'] = conf.return_value('dhcp-options vendor-class-id') + if conf.exists(['dhcp-options', 'vendor-class-id']): + intf['dhcp_vendor_class_id'] = conf.return_value( + ['dhcp-options', 'vendor-class-id']) # DHCPv6 only acquire config parameters, no address - if conf.exists('dhcpv6-options parameters-only'): - vlan['dhcpv6_prm_only'] = True + if conf.exists(['dhcpv6-options', 'parameters-only']): + intf['dhcpv6_prm_only'] = True + + # DHCPv6 prefix delegation (RFC3633) + current_level = conf.get_level() + if conf.exists(['dhcpv6-options', 'delegate']): + for interface in conf.list_nodes(['dhcpv6-options', 'delegate']): + conf.set_level(current_level + ['dhcpv6-options', 'delegate', interface]) + pd = { + 'ifname': interface, + 'sla_id': '', + 'sla_len': '', + 'if_id': '' + } + + if conf.exists(['sla-id']): + pd['sla_id'] = conf.return_value(['sla-id']) + + if conf.exists(['sla-len']): + pd['sla_len'] = conf.return_value(['sla-len']) + + if conf.exists(['interface-id']): + pd['if_id'] = conf.return_value(['interface-id']) + + intf['dhcpv6_pd'].append(pd) + + # re-set config level + conf.set_level(current_level) # DHCPv6 temporary IPv6 address - if conf.exists('dhcpv6-options temporary'): - vlan['dhcpv6_temporary'] = True + if conf.exists(['dhcpv6-options', 'temporary']): + intf['dhcpv6_temporary'] = True # ignore link state changes - if conf.exists('disable-link-detect'): - vlan['disable_link_detect'] = 2 - - # disable VLAN interface - if conf.exists('disable'): - vlan['disable'] = True + if conf.exists(['disable-link-detect']): + intf['disable_link_detect'] = 2 # ARP filter configuration - if conf.exists('ip disable-arp-filter'): - vlan['ip_disable_arp_filter'] = 0 + if conf.exists(['ip', 'disable-arp-filter']): + intf['ip_disable_arp_filter'] = 0 # ARP enable accept - if conf.exists('ip enable-arp-accept'): - vlan['ip_enable_arp_accept'] = 1 + if conf.exists(['ip', 'enable-arp-accept']): + intf['ip_enable_arp_accept'] = 1 # ARP enable announce - if conf.exists('ip enable-arp-announce'): - vlan['ip_enable_arp_announce'] = 1 + if conf.exists(['ip', 'enable-arp-announce']): + intf['ip_enable_arp_announce'] = 1 # ARP enable ignore - if conf.exists('ip enable-arp-ignore'): - vlan['ip_enable_arp_ignore'] = 1 + if conf.exists(['ip', 'enable-arp-ignore']): + intf['ip_enable_arp_ignore'] = 1 # Enable Proxy ARP - if conf.exists('ip enable-proxy-arp'): - vlan['ip_proxy_arp'] = 1 + if conf.exists(['ip', 'enable-proxy-arp']): + intf['ip_proxy_arp'] = 1 # Enable acquisition of IPv6 address using stateless autoconfig (SLAAC) - if conf.exists('ipv6 address autoconf'): - vlan['ipv6_autoconf'] = 1 + if conf.exists(['ipv6', 'address', 'autoconf']): + intf['ipv6_autoconf'] = 1 # Disable IPv6 forwarding on this interface - if conf.exists('ipv6 disable-forwarding'): - vlan['ipv6_forwarding'] = 0 + if conf.exists(['ipv6', 'disable-forwarding']): + intf['ipv6_forwarding'] = 0 + + # check if interface is member of a bridge + intf['is_bridge_member'] = is_member(conf, intf['intf'], 'bridge') # 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')) + if conf.exists(['ipv6', 'dup-addr-detect-transmits']): + intf['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') + if conf.exists(['mac']): + intf['mac'] = conf.return_value(['mac']) # Maximum Transmission Unit (MTU) - if conf.exists('mtu'): - vlan['mtu'] = int(conf.return_value('mtu')) + if conf.exists(['mtu']): + intf['mtu'] = int(conf.return_value(['mtu'])) # retrieve VRF instance - if conf.exists('vrf'): - vlan['vrf'] = conf.return_value('vrf') + if conf.exists(['vrf']): + intf['vrf'] = conf.return_value(['vrf']) - # VLAN egress QoS - if conf.exists('egress-qos'): - vlan['egress_qos'] = conf.return_value('egress-qos') + # egress QoS + if conf.exists(['egress-qos']): + intf['egress_qos'] = conf.return_value(['egress-qos']) # egress changes QoS require VLAN interface recreation - if conf.return_effective_value('egress-qos'): - if vlan['egress_qos'] != conf.return_effective_value('egress-qos'): - vlan['egress_qos_changed'] = True + if conf.return_effective_value(['egress-qos']): + if intf['egress_qos'] != conf.return_effective_value(['egress-qos']): + intf['egress_qos_changed'] = True - # VLAN ingress QoS - if conf.exists('ingress-qos'): - vlan['ingress_qos'] = conf.return_value('ingress-qos') + # ingress QoS + if conf.exists(['ingress-qos']): + intf['ingress_qos'] = conf.return_value(['ingress-qos']) # ingress changes QoS require VLAN interface recreation - if conf.return_effective_value('ingress-qos'): - if vlan['ingress_qos'] != conf.return_effective_value('ingress-qos'): - vlan['ingress_qos_changed'] = True + if conf.return_effective_value(['ingress-qos']): + if intf['ingress_qos'] != conf.return_effective_value(['ingress-qos']): + intf['ingress_qos_changed'] = True + + # Get the interface addresses + intf['address'] = conf.return_values(['address']) + + # addresses to remove - difference between effective and working config + intf['address_remove'] = list_diff( + conf.return_effective_values(['address']), intf['address']) + + # Get prefixes for IPv6 addressing based on MAC address (EUI-64) + intf['ipv6_eui64_prefix'] = conf.return_values(['ipv6', 'address', 'eui64']) + + # EUI64 to remove - difference between effective and working config + intf['ipv6_eui64_prefix_remove'] = list_diff( + conf.return_effective_values(['ipv6', 'address', 'eui64']), + intf['ipv6_eui64_prefix']) + + # Determine if the interface should be disabled + disabled = disable_state(conf) + if disabled == disable.both: + # was and is still disabled + intf['disable'] = True + elif disabled == disable.now: + # it is now disable but was not before + intf['disable'] = True + elif disabled == disable.was: + # it was disable but not anymore + intf['disable'] = False + else: + # normal change + intf['disable'] = False + + # Remove the default link-local address if no-default-link-local is set, + # if member of a bridge or if disabled (it may not have a MAC if it's down) + if ( conf.exists(['ipv6', 'address', 'no-default-link-local']) + or intf.get('is_bridge_member') or intf['disable'] ): + intf['ipv6_eui64_prefix_remove'].append('fe80::/64') + else: + # add the link-local by default to make IPv6 work + intf['ipv6_eui64_prefix'].append('fe80::/64') - # 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()[-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 + # If MAC has changed, remove and re-add all IPv6 EUI64 addresses + try: + interface = Interface(intf['intf'], create=False) + if intf['mac'] and intf['mac'] != interface.get_mac(): + intf['ipv6_eui64_prefix_remove'] += intf['ipv6_eui64_prefix'] + except Exception: + # If the interface does not exist, it could not have changed + pass + + # to make IPv6 SLAAC and DHCPv6 work with forwarding=1, + # accept_ra must be 2 + if intf['ipv6_autoconf'] or 'dhcpv6' in intf['address']: + intf['ipv6_accept_ra'] = 2 + + return intf, disable + + + +def add_to_dict(conf, disabled, ifdict, section, key): + """ + parse a section of vif/vif-s/vif-c and add them to the dict + follow the convention to: + * use the "key" for what to add + * use the "key" what what to remove + + conf: is the Config() already at the level we need to parse + disabled: is a disable enum so we know how to handle to data + intf: if the interface dictionary + section: is the section name to parse (vif/vif-s/vif-c) + key: is the dict key to use (vif/vifs/vifc) + """ + + if not conf.exists(section): + return ifdict + + effect = conf.list_effective_nodes(section) + active = conf.list_nodes(section) + + # the section to parse for vlan + sections = [] + + # determine which interfaces to add or remove based on disable state + if disabled == disable.both: + # was and is still disabled + ifdict[f'{key}_remove'] = [] + elif disabled == disable.now: + # it is now disable but was not before + ifdict[f'{key}_remove'] = effect + elif disabled == disable.was: + # it was disable but not anymore + ifdict[f'{key}_remove'] = [] + sections = active + else: + # normal change + # get interfaces (currently effective) - to determine which # 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)) + ifdict[f'{key}_remove'] = list_diff(effect, active) + sections = active + + current_level = conf.get_level() + + # add each section, the key must already exists + for s in sections: + # set config level to vif interface + conf.set_level(current_level + [section, s]) + # add the vlan config as a key (vlan id) - value (config) pair + ifdict[key][s] = vlan_to_dict(conf) + + # re-set configuration level to leave things as found + conf.set_level(current_level) + + return ifdict + + +def vlan_to_dict(conf, default=vlan_default): + vlan, disabled = intf_to_dict(conf, default) + + # if this is a not within vif-s node, we are done + if conf.get_level()[-2] != 'vif-s': + return vlan + + # ethertype is mandatory on vif-s nodes and only exists here! + # ethertype uses a default of 0x88A8 + tmp = '0x88A8' + if conf.exists('ethertype'): + tmp = conf.return_value('ethertype') + vlan['ethertype'] = get_ethertype(tmp) + + # check if there is a Q-in-Q vlan customer interface + # and call this function recursively + add_to_dict(conf, disable, vlan, 'vif-c', 'vif_c') return vlan diff --git a/python/vyos/debug.py b/python/vyos/debug.py index 20090fb85..6ce42b173 100644 --- a/python/vyos/debug.py +++ b/python/vyos/debug.py @@ -15,7 +15,7 @@ import os import sys - +from datetime import datetime def message(message, flag='', destination=sys.stdout): """ @@ -41,11 +41,12 @@ def message(message, flag='', destination=sys.stdout): try: # at boot the file is created as root:vyattacfg # at runtime the file is created as user:vyattacfg - # the default permission are 644 - mask = os.umask(0o113) + # but the helper scripts are not run as this so it + # need the default permission to be 666 (an not 660) + mask = os.umask(0o111) with open(logfile, 'a') as f: - f.write(_format('log', message)) + f.write(_timed(_format('log', message))) finally: os.umask(mask) @@ -80,10 +81,22 @@ def enabled(flag): return _fromenv(flag) or _fromfile(flag) +def _timed(message): + now = datetime.now().strftime('%Y-%m-%d %H:%M:%S') + return f'{now} {message}' + + +def _remove_invisible(string): + for char in ('\0', '\a', '\b', '\f', '\v'): + string = string.replace(char, '') + return string + + def _format(flag, message): """ format a log message """ + message = _remove_invisible(message) return f'DEBUG/{flag.upper():<7} {message}\n' @@ -133,7 +146,7 @@ def _contentenv(flag): return os.environ.get(f'VYOS_{flag.upper()}_DEBUG', '').strip() -def _contentfile(flag): +def _contentfile(flag, default=''): """ Check if debug exist for a given debug flag name @@ -153,7 +166,8 @@ def _contentfile(flag): if not os.path.isfile(flagfile): continue with open(flagfile) as f: - return f.readline().strip() + content = f.readline().strip() + return content or default return '' @@ -166,7 +180,7 @@ def _logfile(flag, default): """ # For log we return the location of the log file - log_location = _contentenv(flag) or _contentfile(flag) + log_location = _contentenv(flag) or _contentfile(flag, default) # it was not set if not log_location: @@ -177,6 +191,15 @@ def _logfile(flag, default): not log_location.startswith('/config/') and \ not log_location.startswith('/var/log/'): return default + # Do not allow to escape the folders if '..' in log_location: return default + + if not os.path.exists(log_location): + return log_location + + # this permission is unique the the config and var folder + stat = os.stat(log_location).st_mode + if stat != 0o100666: + return default return log_location diff --git a/python/vyos/dicts.py b/python/vyos/dicts.py index 79cab4a08..b12cda40f 100644 --- a/python/vyos/dicts.py +++ b/python/vyos/dicts.py @@ -15,6 +15,9 @@ # along with this program. If not, see <http://www.gnu.org/licenses/>. +from vyos import ConfigError + + class FixedDict(dict): """ FixedDict: A dictionnary not allowing new keys to be created after initialisation. diff --git a/python/vyos/ifconfig/dhcp.py b/python/vyos/ifconfig/dhcp.py index d4ff9c2cd..f8fdeb6a9 100644 --- a/python/vyos/ifconfig/dhcp.py +++ b/python/vyos/ifconfig/dhcp.py @@ -19,28 +19,19 @@ from vyos.dicts import FixedDict from vyos.ifconfig.control import Control from vyos.template import render - -class _DHCP (Control): - client_base = r'/var/lib/dhcp/dhclient_' - - def __init__(self, ifname, version, **kargs): - super().__init__(**kargs) - self.version = version - self.file = { - 'ifname': ifname, - 'conf': self.client_base + ifname + '.' + version + 'conf', - 'pid': self.client_base + ifname + '.' + version + 'pid', - 'lease': self.client_base + ifname + '.' + version + 'leases', - } - -class _DHCPv4 (_DHCP): +class _DHCPv4 (Control): def __init__(self, ifname): - super().__init__(ifname, '') + super().__init__() + config_base = r'/var/lib/dhcp/dhclient_' self.options = FixedDict(**{ 'ifname': ifname, 'hostname': '', 'client_id': '', - 'vendor_class_id': '' + 'vendor_class_id': '', + 'conf_file': config_base + f'{ifname}.conf', + 'options_file': config_base + f'{ifname}.options', + 'pid_file': config_base + f'{ifname}.pid', + 'lease_file': config_base + f'{ifname}.leases', }) # replace dhcpv4/v6 with systemd.networkd? @@ -55,25 +46,16 @@ class _DHCPv4 (_DHCP): >>> j = Interface('eth0') >>> j.dhcp.v4.set() """ - if not self.options['hostname']: # read configured system hostname. # maybe change to vyos hostd client ??? with open('/etc/hostname', 'r') as f: self.options['hostname'] = f.read().rstrip('\n') - render(self.file['conf'], 'dhcp-client/ipv4.tmpl' ,self.options) + render(self.options['options_file'], 'dhcp-client/daemon-options.tmpl', self.options) + render(self.options['conf_file'], 'dhcp-client/ipv4.tmpl', self.options) - cmd = 'start-stop-daemon' - cmd += ' --start' - cmd += ' --oknodo' - cmd += ' --quiet' - cmd += ' --pidfile {pid}' - cmd += ' --exec /sbin/dhclient' - cmd += ' --' - # now pass arguments to dhclient binary - cmd += ' -4 -nw -cf {conf} -pf {pid} -lf {lease} {ifname}' - return self._cmd(cmd.format(**self.file)) + return self._cmd('systemctl restart dhclient@{ifname}.service'.format(**self.options)) def delete(self): """ @@ -86,44 +68,27 @@ class _DHCPv4 (_DHCP): >>> j = Interface('eth0') >>> j.dhcp.v4.delete() """ - if not os.path.isfile(self.file['pid']): + if not os.path.isfile(self.options['pid_file']): self._debug_msg('No DHCP client PID found') return None - # with open(self.file['pid'], 'r') as f: - # pid = int(f.read()) - - # stop dhclient, we need to call dhclient and tell it should release the - # aquired IP address. tcpdump tells me: - # 172.16.35.103.68 > 172.16.35.254.67: [bad udp cksum 0xa0cb -> 0xb943!] BOOTP/DHCP, Request from 00:50:56:9d:11:df, length 300, xid 0x620e6946, Flags [none] (0x0000) - # Client-IP 172.16.35.103 - # Client-Ethernet-Address 00:50:56:9d:11:df - # Vendor-rfc1048 Extensions - # Magic Cookie 0x63825363 - # DHCP-Message Option 53, length 1: Release - # Server-ID Option 54, length 4: 172.16.35.254 - # Hostname Option 12, length 10: "vyos" - # - cmd = '/sbin/dhclient -cf {conf} -pf {pid} -lf {lease} -r {ifname}' - self._cmd(cmd.format(**self.file)) + self._cmd('systemctl stop dhclient@{ifname}.service'.format(**self.options)) # cleanup old config files - for name in ('conf', 'pid', 'lease'): - if os.path.isfile(self.file[name]): - os.remove(self.file[name]) - + for name in ('conf_file', 'options_file', 'pid_file', 'lease_file'): + if os.path.isfile(self.options[name]): + os.remove(self.options[name]) -class _DHCPv6 (_DHCP): +class _DHCPv6 (Control): def __init__(self, ifname): - super().__init__(ifname, 'v6') + super().__init__() self.options = FixedDict(**{ 'ifname': ifname, 'dhcpv6_prm_only': False, 'dhcpv6_temporary': False, + 'dhcpv6_pd': [], }) - self.file.update({ - 'accept_ra': f'/proc/sys/net/ipv6/conf/{ifname}/accept_ra', - }) + self._conf_file = f'/run/dhcp6c/dhcp6c.{ifname}.conf' def set(self): """ @@ -134,7 +99,7 @@ class _DHCPv6 (_DHCP): >>> from vyos.ifconfig import Interface >>> j = Interface('eth0') - >>> j.set_dhcpv6() + >>> j.dhcp.v6.set() """ # better save then sorry .. should be checked in interface script @@ -143,29 +108,8 @@ class _DHCPv6 (_DHCP): raise Exception( 'DHCPv6 temporary and parameters-only options are mutually exclusive!') - render(self.file['conf'], 'dhcp-client/ipv6.tmpl', self.options) - - # no longer accept router announcements on this interface - self._write_sysfs(self.file['accept_ra'], 0) - - # assemble command-line to start DHCPv6 client (dhclient) - cmd = 'start-stop-daemon' - cmd += ' --start' - cmd += ' --oknodo' - cmd += ' --quiet' - cmd += ' --pidfile {pid}' - cmd += ' --exec /sbin/dhclient' - cmd += ' --' - # now pass arguments to dhclient binary - cmd += ' -6 -nw -cf {conf} -pf {pid} -lf {lease}' - # add optional arguments - if self.options['dhcpv6_prm_only']: - cmd += ' -S' - if self.options['dhcpv6_temporary']: - cmd += ' -T' - cmd += ' {ifname}' - - return self._cmd(cmd.format(**self.file)) + render(self._conf_file, 'dhcp-client/ipv6.tmpl', self.options, trim_blocks=True) + return self._cmd('systemctl restart dhcp6c@{ifname}.service'.format(**self.options)) def delete(self): """ @@ -176,33 +120,16 @@ class _DHCPv6 (_DHCP): >>> from vyos.ifconfig import Interface >>> j = Interface('eth0') - >>> j.del_dhcpv6() + >>> j.dhcp.v6.delete() """ - if not os.path.isfile(self.file['pid']): - self._debug_msg('No DHCPv6 client PID found') - return None - - # with open(self.file['pid'], 'r') as f: - # pid = int(f.read()) - - # stop dhclient - cmd = 'start-stop-daemon' - cmd += ' --start' - cmd += ' --oknodo' - cmd += ' --quiet' - cmd += ' --pidfile {pid}' - self._cmd(cmd.format(**self.file)) - - # accept router announcements on this interface - self._write_sysfs(self.options['accept_ra'], 1) + self._cmd('systemctl stop dhcp6c@{ifname}.service'.format(**self.options)) # cleanup old config files - for name in ('conf', 'pid', 'lease'): - if os.path.isfile(self.file[name]): - os.remove(self.file[name]) + if os.path.isfile(self._conf_file): + os.remove(self._conf_file) -class DHCP (object): +class DHCP(object): def __init__(self, ifname): self.v4 = _DHCPv4(ifname) self.v6 = _DHCPv6(ifname) diff --git a/python/vyos/ifconfig/interface.py b/python/vyos/ifconfig/interface.py index 62c30dbf7..61f2c6482 100644 --- a/python/vyos/ifconfig/interface.py +++ b/python/vyos/ifconfig/interface.py @@ -1,4 +1,4 @@ -# Copyright 2019 VyOS maintainers and contributors <maintainers@vyos.io> +# Copyright 2019-2020 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 @@ -42,6 +42,7 @@ from vyos.ifconfig.control import Control from vyos.ifconfig.dhcp import DHCP from vyos.ifconfig.vrrp import VRRP from vyos.ifconfig.operational import Operational +from vyos.ifconfig import Section class Interface(Control): @@ -133,8 +134,12 @@ class Interface(Control): 'validate': assert_boolean, 'location': '/proc/sys/net/ipv4/conf/{ifname}/arp_ignore', }, + 'ipv6_accept_ra': { + 'validate': lambda ara: assert_range(ara,0,3), + 'location': '/proc/sys/net/ipv6/conf/{ifname}/accept_ra', + }, 'ipv6_autoconf': { - 'validate': lambda fwd: assert_range(fwd,0,2), + 'validate': lambda aco: assert_range(aco,0,2), 'location': '/proc/sys/net/ipv6/conf/{ifname}/autoconf', }, 'ipv6_forwarding': { @@ -219,7 +224,7 @@ class Interface(Control): else: raise Exception('interface "{}" not found'.format(self.config['ifname'])) - # list of assigned IP addresses + # temporary list of assigned IP addresses self._addr = [] self.operational = self.OperationalClass(ifname) @@ -240,15 +245,11 @@ class Interface(Control): >>> i = Interface('eth0') >>> i.remove() """ - # stop DHCP(v6) if running - self.dhcp.v4.delete() - self.dhcp.v6.delete() # remove all assigned IP addresses from interface - this is a bit redundant # as the kernel will remove all addresses on interface deletion, but we # can not delete ALL interfaces, see below - for addr in self.get_addr(): - self.del_addr(addr) + self.flush_addrs() # --------------------------------------------------------------------- # Any class can define an eternal regex in its definition @@ -412,6 +413,21 @@ class Interface(Control): """ return self.set_interface('arp_ignore', arp_ignore) + def set_ipv6_accept_ra(self, accept_ra): + """ + Accept Router Advertisements; autoconfigure using them. + + It also determines whether or not to transmit Router Solicitations. + If and only if the functional setting is to accept Router + Advertisements, Router Solicitations will be transmitted. + + 0 - Do not accept Router Advertisements. + 1 - (default) Accept Router Advertisements if forwarding is disabled. + 2 - Overrule forwarding behaviour. Accept Router Advertisements even if + forwarding is enabled. + """ + return self.set_interface('ipv6_accept_ra', accept_ra) + def set_ipv6_autoconf(self, autoconf): """ Autoconfigure addresses using Prefix Information in Router @@ -419,39 +435,28 @@ class Interface(Control): """ return self.set_interface('ipv6_autoconf', autoconf) - def set_ipv6_eui64_address(self, prefix): + def add_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. + assign itself 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. + Calculate the EUI64 from the interface's MAC, then assign it + with the given prefix to the 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) + prefixlen = prefix.split('/')[1] + self.add_addr(f'{eui64}/{prefixlen}') - 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 + def del_ipv6_eui64_address(self, prefix): + """ + Delete the address based on the interface's MAC-based EUI64 + combined with the prefix address. + """ + eui64 = mac2eui64(self.get_mac(), prefix) + prefixlen = prefix.split('/')[1] + self.del_addr(f'{eui64}/{prefixlen}') - # 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): """ @@ -632,7 +637,8 @@ class Interface(Control): def add_addr(self, addr): """ Add IP(v6) address to interface. Address is only added if it is not - already assigned to that interface. + already assigned to that interface. Address format must be validated + and compressed/normalized before calling this function. addr: can be an IPv4 address, IPv6 address, dhcp or dhcpv6! IPv4: add IPv4 address to interface @@ -640,6 +646,7 @@ class Interface(Control): dhcp: start dhclient (IPv4) on interface dhcpv6: start dhclient (IPv6) on interface + Returns False if address is already assigned and wasn't re-added. Example: >>> from vyos.ifconfig import Interface >>> j = Interface('eth0') @@ -648,32 +655,41 @@ class Interface(Control): >>> j.get_addr() ['192.0.2.1/24', '2001:db8::ffff/64'] """ + # XXX: normalize/compress with ipaddress if calling functions don't? + # is subnet mask always passed, and in the same way? - # cache new IP address which is assigned to interface - self._addr.append(addr) + # do not add same address twice + if addr in self._addr: + return False - # we can not have both DHCP and static IPv4 addresses assigned to an interface - if 'dhcp' in self._addr: - for addr in self._addr: - # do not change below 'if' ordering esle you will get an exception as: - # ValueError: 'dhcp' does not appear to be an IPv4 or IPv6 address - if addr != 'dhcp' and is_ipv4(addr): - raise ConfigError( - "Can't configure both static IPv4 and DHCP address on the same interface") + # we can't have both DHCP and static IPv4 addresses assigned + for a in self._addr: + if ( ( addr == 'dhcp' and a != 'dhcpv6' and is_ipv4(a) ) or + ( a == 'dhcp' and addr != 'dhcpv6' and is_ipv4(addr) ) ): + raise ConfigError(( + "Can't configure both static IPv4 and DHCP address " + "on the same interface")) + # add to interface if addr == 'dhcp': self.dhcp.v4.set() elif addr == 'dhcpv6': self.dhcp.v6.set() + elif not is_intf_addr_assigned(self.ifname, addr): + self._cmd(f'ip addr add "{addr}" dev "{self.ifname}"') else: - if not is_intf_addr_assigned(self.config['ifname'], addr): - cmd = 'ip addr add "{}" dev "{}"'.format(addr, self.config['ifname']) - return self._cmd(cmd) + return False + + # add to cache + self._addr.append(addr) + + return True def del_addr(self, addr): """ - Delete IP(v6) address to interface. Address is only added if it is - assigned to that interface. + Delete IP(v6) address from interface. Address is only deleted if it is + assigned to that interface. Address format must be exactly the same as + was used when adding the address. addr: can be an IPv4 address, IPv6 address, dhcp or dhcpv6! IPv4: delete IPv4 address from interface @@ -681,6 +697,7 @@ class Interface(Control): dhcp: stop dhclient (IPv4) on interface dhcpv6: stop dhclient (IPv6) on interface + Returns False if address isn't already assigned and wasn't deleted. Example: >>> from vyos.ifconfig import Interface >>> j = Interface('eth0') @@ -692,11 +709,51 @@ class Interface(Control): >>> j.get_addr() ['2001:db8::ffff/64'] """ + + # remove from interface if addr == 'dhcp': self.dhcp.v4.delete() elif addr == 'dhcpv6': self.dhcp.v6.delete() + elif is_intf_addr_assigned(self.ifname, addr): + self._cmd(f'ip addr del "{addr}" dev "{self.ifname}"') else: - if is_intf_addr_assigned(self.config['ifname'], addr): - cmd = 'ip addr del "{}" dev "{}"'.format(addr, self.config['ifname']) - return self._cmd(cmd) + return False + + # remove from cache + if addr in self._addr: + self._addr.remove(addr) + + return True + + def flush_addrs(self): + """ + Flush all addresses from an interface, including DHCP. + + Will raise an exception on error. + """ + # stop DHCP(v6) if running + self.dhcp.v4.delete() + self.dhcp.v6.delete() + + # flush all addresses + self._cmd(f'ip addr flush dev "{self.ifname}"') + + def add_to_bridge(self, br): + """ + Adds the interface to the bridge with the passed port config. + + Returns False if bridge doesn't exist. + """ + + # check if the bridge exists (on boot it doesn't) + if br not in Section.interfaces('bridge'): + return False + + self.flush_addrs() + # add interface to bridge - use Section.klass to get BridgeIf class + Section.klass(br)(br, create=False).add_port(self.ifname) + + # TODO: port config (STP) + + return True diff --git a/python/vyos/ifconfig/section.py b/python/vyos/ifconfig/section.py index 092236fef..926c22e8a 100644 --- a/python/vyos/ifconfig/section.py +++ b/python/vyos/ifconfig/section.py @@ -54,7 +54,7 @@ class Section: name = name.rstrip('0123456789') name = name.rstrip('.') if vlan: - name = name.rstrip('0123456789') + name = name.rstrip('0123456789.') return name @classmethod @@ -137,3 +137,22 @@ class Section: eth, lo, vxlan, dum, ... """ return list(cls._prefixes.keys()) + + @classmethod + def get_config_path(cls, name): + """ + get config path to interface with .vif or .vif-s.vif-c + example: eth0.1.2 -> 'ethernet eth0 vif-s 1 vif-c 2' + Returns False if interface name is invalid (not found in sections) + """ + sect = cls.section(name) + if sect: + splinterface = name.split('.') + intfpath = f'{sect} {splinterface[0]}' + if len(splinterface) == 2: + intfpath += f' vif {splinterface[1]}' + elif len(splinterface) == 3: + intfpath += f' vif-s {splinterface[1]} vif-c {splinterface[2]}' + return intfpath + else: + return False diff --git a/python/vyos/ifconfig/vlan.py b/python/vyos/ifconfig/vlan.py index 7b1e00d87..d68e8f6cd 100644 --- a/python/vyos/ifconfig/vlan.py +++ b/python/vyos/ifconfig/vlan.py @@ -101,26 +101,26 @@ class VLAN: >>> i.add_vlan(10) """ vlan_ifname = self.config['ifname'] + '.' + str(vlan_id) - if not os.path.exists(f'/sys/class/net/{vlan_ifname}'): - self._vlan_id = int(vlan_id) - - if ethertype: - self._ethertype = ethertype - ethertype = 'proto {}'.format(ethertype) - - # Optional ingress QOS mapping - opt_i = '' - if ingress_qos: - opt_i = 'ingress-qos-map ' + ingress_qos - # Optional egress QOS mapping - opt_e = '' - if egress_qos: - opt_e = 'egress-qos-map ' + egress_qos - - # create interface in the system - cmd = 'ip link add link {ifname} name {ifname}.{vlan} type vlan {proto} id {vlan} {opt_e} {opt_i}' \ - .format(ifname=self.config['ifname'], vlan=self._vlan_id, proto=ethertype, opt_e=opt_e, opt_i=opt_i) - self._cmd(cmd) + if os.path.exists(f'/sys/class/net/{vlan_ifname}'): + return self.__class__(vlan_ifname) + + if ethertype: + self._ethertype = ethertype + ethertype = 'proto {}'.format(ethertype) + + # Optional ingress QOS mapping + opt_i = '' + if ingress_qos: + opt_i = 'ingress-qos-map ' + ingress_qos + # Optional egress QOS mapping + opt_e = '' + if egress_qos: + opt_e = 'egress-qos-map ' + egress_qos + + # create interface in the system + cmd = 'ip link add link {ifname} name {ifname}.{vlan} type vlan {proto} id {vlan} {opt_e} {opt_i}' \ + .format(ifname=self.ifname, vlan=vlan_id, proto=ethertype, opt_e=opt_e, opt_i=opt_i) + 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 diff --git a/python/vyos/ifconfig/wireguard.py b/python/vyos/ifconfig/wireguard.py index fdf5d9347..027b5ea8c 100644 --- a/python/vyos/ifconfig/wireguard.py +++ b/python/vyos/ifconfig/wireguard.py @@ -208,7 +208,7 @@ class WireGuardIf(Interface): else: cmd += aip if self.config['endpoint']: - cmd += " endpoint {}".format(self.config['endpoint']) + cmd += " endpoint '{}'".format(self.config['endpoint']) cmd += " persistent-keepalive {}".format(self.config['keepalive']) self._cmd(cmd) diff --git a/python/vyos/ifconfig_vlan.py b/python/vyos/ifconfig_vlan.py index 899fd17da..a53136ebf 100644 --- a/python/vyos/ifconfig_vlan.py +++ b/python/vyos/ifconfig_vlan.py @@ -16,6 +16,53 @@ from netifaces import interfaces from vyos import ConfigError +def apply_all_vlans(intf, intfconfig): + """ + Function applies all VLANs to the passed interface. + + intf: object of Interface class + intfconfig: dict with interface configuration + """ + # remove no longer required service VLAN interfaces (vif-s) + for vif_s in intfconfig['vif_s_remove']: + intf.del_vlan(vif_s) + + # create service VLAN interfaces (vif-s) + for vif_s_id, vif_s in intfconfig['vif_s'].items(): + s_vlan = intf.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 intfconfig['vif_c_remove']: + s_vlan.del_vlan(vif_c) + + # create client VLAN interfaces (vif-c) + # on lower service VLAN interface + for vif_c_id, vif_c in vif_s['vif_c'].items(): + 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 intfconfig['vif_remove']: + intf.del_vlan(vif) + + # create VLAN interfaces (vif) + for vif_id, vif in intfconfig['vif'].items(): + # QoS priority mapping can only be set during interface creation + # so we delete the interface first if required. + if vif['egress_qos_changed'] or vif['ingress_qos_changed']: + try: + # on system bootup the above condition is true but the interface + # does not exists, which throws an exception, but that's legal + intf.del_vlan(vif_id) + except: + pass + + vlan = intf.add_vlan(vif_id, ingress_qos=vif['ingress_qos'], egress_qos=vif['egress_qos']) + apply_vlan_config(vlan, vif) + + def apply_vlan_config(vlan, config): """ Generic function to apply a VLAN configuration from a dictionary @@ -40,6 +87,9 @@ def apply_vlan_config(vlan, config): if config['dhcpv6_temporary']: vlan.dhcp.v6.options['dhcpv6_temporary'] = True + if config['dhcpv6_pd']: + vlan.dhcp.v6.options['dhcpv6_pd'] = config['dhcpv6_pd'] + # update interface description used e.g. within SNMP vlan.set_alias(config['description']) # ignore link state changes @@ -54,6 +104,8 @@ def apply_vlan_config(vlan, config): vlan.set_arp_ignore(config['ip_enable_arp_ignore']) # configure Proxy ARP vlan.set_proxy_arp(config['ip_proxy_arp']) + # IPv6 accept RA + vlan.set_ipv6_accept_ra(config['ipv6_accept_ra']) # IPv6 address autoconfiguration vlan.set_ipv6_autoconf(config['ipv6_autoconf']) # IPv6 forwarding @@ -63,13 +115,23 @@ def apply_vlan_config(vlan, config): # Maximum Transmission Unit (MTU) vlan.set_mtu(config['mtu']) - # assign/remove VRF - vlan.set_vrf(config['vrf']) + # assign/remove VRF (ONLY when not a member of a bridge, + # otherwise 'nomaster' removes it from it) + if not config['is_bridge_member']: + vlan.set_vrf(config['vrf']) + + # Delete old IPv6 EUI64 addresses before changing MAC + for addr in config['ipv6_eui64_prefix_remove']: + vlan.del_ipv6_eui64_address(addr) # Change VLAN interface MAC address if config['mac']: vlan.set_mac(config['mac']) + # Add IPv6 EUI-based addresses + for addr in config['ipv6_eui64_prefix']: + vlan.add_ipv6_eui64_address(addr) + # enable/disable VLAN interface if config['disable']: vlan.set_admin_state('down') @@ -84,46 +146,97 @@ def apply_vlan_config(vlan, config): for addr in config['address']: vlan.add_addr(addr) + # re-add ourselves to any bridge we might have fallen out of + if config['is_bridge_member']: + vlan.add_to_bridge(config['is_bridge_member']) + def verify_vlan_config(config): """ Generic function to verify VLAN config consistency. Instead of re- implementing this function in multiple places use single source \o/ """ - for vif in config['vif']: + # config['vif'] is a dict with ids as keys and config dicts as values + for vif in config['vif'].values(): # DHCPv6 parameters-only and temporary address are mutually exclusive if vif['dhcpv6_prm_only'] and vif['dhcpv6_temporary']: raise ConfigError('DHCPv6 temporary and parameters-only options are mutually exclusive!') - vrf_name = vif['vrf'] - if vrf_name and vrf_name not in interfaces(): - raise ConfigError(f'VRF "{vrf_name}" does not exist') + if ( vif['is_bridge_member'] + and ( vif['address'] + or vif['ipv6_eui64_prefix'] + or vif['ipv6_autoconf'] ) ): + raise ConfigError(( + f'Cannot assign address to vif interface {vif["intf"]} ' + f'which is a member of bridge {vif["is_bridge_member"]}')) + + if vif['vrf']: + if vif['vrf'] not in interfaces(): + raise ConfigError(f'VRF "{vif["vrf"]}" does not exist') + + if vif['is_bridge_member']: + raise ConfigError(( + f'vif {vif["intf"]} cannot be member of VRF {vif["vrf"]} ' + f'and bridge {vif["is_bridge_member"]} at the same time!')) # e.g. wireless interface has no vif_s support # thus we bail out eraly. if 'vif_s' not in config.keys(): return - for vif_s in config['vif_s']: - for vif in config['vif']: - if vif['id'] == vif_s['id']: - raise ConfigError('Can not use identical ID on vif and vif-s interface') + # config['vif_s'] is a dict with ids as keys and config dicts as values + for vif_s_id, vif_s in config['vif_s'].items(): + for vif_id, vif in config['vif'].items(): + if vif_id == vif_s_id: + raise ConfigError(( + f'Cannot use identical ID on vif "{vif["intf"]}" ' + f'and vif-s "{vif_s["intf"]}"')) # DHCPv6 parameters-only and temporary address are mutually exclusive if vif_s['dhcpv6_prm_only'] and vif_s['dhcpv6_temporary']: - raise ConfigError('DHCPv6 temporary and parameters-only options are mutually exclusive!') - - vrf_name = vif_s['vrf'] - if vrf_name and vrf_name not in interfaces(): - raise ConfigError(f'VRF "{vrf_name}" does not exist') - - for vif_c in vif_s['vif_c']: + raise ConfigError(( + 'DHCPv6 temporary and parameters-only options are mutually ' + 'exclusive!')) + + if ( vif_s['is_bridge_member'] + and ( vif_s['address'] + or vif_s['ipv6_eui64_prefix'] + or vif_s['ipv6_autoconf'] ) ): + raise ConfigError(( + f'Cannot assign address to vif-s interface {vif_s["intf"]} ' + f'which is a member of bridge {vif_s["is_bridge_member"]}')) + + if vif_s['vrf']: + if vif_s['vrf'] not in interfaces(): + raise ConfigError(f'VRF "{vif_s["vrf"]}" does not exist') + + if vif_s['is_bridge_member']: + raise ConfigError(( + f'vif-s {vif_s["intf"]} cannot be member of VRF {vif_s["vrf"]} ' + f'and bridge {vif_s["is_bridge_member"]} at the same time!')) + + # vif_c is a dict with ids as keys and config dicts as values + for vif_c in vif_s['vif_c'].values(): # DHCPv6 parameters-only and temporary address are mutually exclusive if vif_c['dhcpv6_prm_only'] and vif_c['dhcpv6_temporary']: - raise ConfigError('DHCPv6 temporary and parameters-only options are mutually exclusive!') - - vrf_name = vif_c['vrf'] - if vrf_name and vrf_name not in interfaces(): - raise ConfigError(f'VRF "{vrf_name}" does not exist') - + raise ConfigError(( + 'DHCPv6 temporary and parameters-only options are ' + 'mutually exclusive!')) + + if ( vif_c['is_bridge_member'] + and ( vif_c['address'] + or vif_c['ipv6_eui64_prefix'] + or vif_c['ipv6_autoconf'] ) ): + raise ConfigError(( + f'Cannot assign address to vif-c interface {vif_c["intf"]} ' + f'which is a member of bridge {vif_c["is_bridge_member"]}')) + + if vif_c['vrf']: + if vif_c['vrf'] not in interfaces(): + raise ConfigError(f'VRF "{vif_c["vrf"]}" does not exist') + + if vif_c['is_bridge_member']: + raise ConfigError(( + f'vif-c {vif_c["intf"]} cannot be member of VRF {vif_c["vrf"]} ' + f'and bridge {vif_c["is_bridge_member"]} at the same time!')) diff --git a/python/vyos/template.py b/python/vyos/template.py index 6c73ce753..e4b253ed3 100644 --- a/python/vyos/template.py +++ b/python/vyos/template.py @@ -19,6 +19,7 @@ from jinja2 import Environment from jinja2 import FileSystemLoader from vyos.defaults import directories +from vyos.util import chmod, chown, makedir # reuse the same Environment to improve performance @@ -32,7 +33,7 @@ _templates_mem = { } -def render(destination, template, content, trim_blocks=False, formater=None): +def render(destination, template, content, trim_blocks=False, formater=None, permission=None, user=None, group=None): """ render a template from the template directory, it will raise on any errors destination: the file where the rendered template must be saved @@ -46,6 +47,10 @@ def render(destination, template, content, trim_blocks=False, formater=None): (recovering the load time and overhead caused by having the file out of the code) """ + # Create the directory if it does not exists + folder = os.path.dirname(destination) + makedir(folder, user, group) + # Setup a renderer for the given template # This is cached and re-used for performance if template not in _templates_mem[trim_blocks]: @@ -63,3 +68,6 @@ def render(destination, template, content, trim_blocks=False, formater=None): # Write client config file with open(destination, 'w') as f: f.write(content) + + chmod(destination, permission) + chown(destination, user, group) diff --git a/python/vyos/util.py b/python/vyos/util.py index 4340332d3..381cd0358 100644 --- a/python/vyos/util.py +++ b/python/vyos/util.py @@ -14,6 +14,7 @@ # License along with this library. If not, see <http://www.gnu.org/licenses/>. import os +import sys # # NOTE: Do not import full classes here, move your import to the function @@ -25,7 +26,7 @@ import os # which all have slighty different behaviour from subprocess import Popen, PIPE, STDOUT, DEVNULL def popen(command, flag='', shell=None, input=None, timeout=None, env=None, - stdout=PIPE, stderr=None, decode=None): + stdout=PIPE, stderr=PIPE, decode='utf-8'): """ popen is a wrapper helper aound subprocess.Popen with it default setting it will return a tuple (out, err) @@ -48,12 +49,14 @@ def popen(command, flag='', shell=None, input=None, timeout=None, env=None, - STDOUT, send the data to be merged with stdout - DEVNULL, discard the output decode: specify the expected text encoding (utf-8, ascii, ...) + the default is explicitely utf-8 which is python's own default usage: to get both stdout, and stderr: popen('command', stdout=PIPE, stderr=STDOUT) to discard stdout and get stderr: popen('command', stdout=DEVNUL, stderr=PIPE) """ from vyos import debug + from vyos import airbag # log if the flag is set, otherwise log if command is set if not debug.enabled(flag): flag = 'command' @@ -77,27 +80,39 @@ def popen(command, flag='', shell=None, input=None, timeout=None, env=None, stdin=stdin, stdout=stdout, stderr=stderr, env=env, shell=use_shell, ) - tmp = p.communicate(input, timeout) - out1 = b'' - out2 = b'' + + pipe = p.communicate(input, timeout) + + pipe_out = b'' if stdout == PIPE: - out1 = tmp[0] + pipe_out = pipe[0] + + pipe_err = b'' if stderr == PIPE: - out2 += tmp[1] - decoded1 = out1.decode(decode) if decode else out1.decode() - decoded2 = out2.decode(decode) if decode else out2.decode() - decoded1 = decoded1.replace('\r\n', '\n').strip() - decoded2 = decoded2.replace('\r\n', '\n').strip() - nl = '\n' if decoded1 and decoded2 else '' - decoded = decoded1 + nl + decoded2 - if decoded: - ret_msg = f"returned:\n{decoded}" - debug.message(ret_msg, flag) - return decoded, p.returncode + pipe_err = pipe[1] + + str_out = pipe_out.decode(decode).replace('\r\n', '\n').strip() + str_err = pipe_err.decode(decode).replace('\r\n', '\n').strip() + + out_msg = f"returned (out):\n{str_out}" + if str_out: + debug.message(out_msg, flag) + + if str_err: + err_msg = f"returned (err):\n{str_err}" + # this message will also be send to syslog via airbag + debug.message(err_msg, flag, destination=sys.stderr) + + # should something go wrong, report this too via airbag + airbag.noteworthy(cmd_msg) + airbag.noteworthy(out_msg) + airbag.noteworthy(err_msg) + + return str_out, p.returncode def run(command, flag='', shell=None, input=None, timeout=None, env=None, - stdout=DEVNULL, stderr=None, decode=None): + stdout=DEVNULL, stderr=PIPE, decode='utf-8'): """ A wrapper around vyos.util.popen, which discard the stdout and will return the error code of a command @@ -113,14 +128,15 @@ def run(command, flag='', shell=None, input=None, timeout=None, env=None, def cmd(command, flag='', shell=None, input=None, timeout=None, env=None, - stdout=PIPE, stderr=None, decode=None, - raising=None, message=''): + stdout=PIPE, stderr=PIPE, decode='utf-8', + raising=None, message='', expect=[0]): """ A wrapper around vyos.util.popen, which returns the stdout and will raise the error code of a command raising: specify which call should be used when raising (default is OSError) the class should only require a string as parameter + expect: a list of error codes to consider as normal """ decoded, code = popen( command, flag, @@ -129,7 +145,7 @@ def cmd(command, flag='', shell=None, input=None, timeout=None, env=None, env=env, shell=shell, decode=decode, ) - if code != 0: + if code not in expect: feedback = message + '\n' if message else '' feedback += f'failed to run command: {command}\n' feedback += f'returned: {decoded}\n' @@ -143,7 +159,7 @@ def cmd(command, flag='', shell=None, input=None, timeout=None, env=None, def call(command, flag='', shell=None, input=None, timeout=None, env=None, - stdout=PIPE, stderr=None, decode=None): + stdout=PIPE, stderr=PIPE, decode='utf-8'): """ A wrapper around vyos.util.popen, which print the stdout and will return the error code of a command @@ -197,10 +213,24 @@ def chown(path, user, group): from pwd import getpwnam from grp import getgrnam - if os.path.exists(path): - uid = getpwnam(user).pw_uid - gid = getgrnam(group).gr_gid - os.chown(path, uid, gid) + if user is None or group is None: + return False + + if not os.path.exists(path): + return False + + uid = getpwnam(user).pw_uid + gid = getgrnam(group).gr_gid + os.chown(path, uid, gid) + return True + + +def chmod(path, bitmask): + if not os.path.exists(path): + return + if bitmask is None: + return + os.chmod(path, bitmask) def chmod_600(path): @@ -231,6 +261,13 @@ def chmod_755(path): os.chmod(path, bitmask) +def makedir(path, user=None, group=None): + if os.path.exists(path): + return + os.mkdir(path) + chown(path, user, group) + + 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. @@ -352,12 +389,9 @@ def get_cfg_group_id(): def file_is_persistent(path): import re - 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) - warning += "It will not be automatically migrated to a new image on system update" - return (False, warning) - else: - return (True, None) + location = r'^(/config|/opt/vyatta/etc/config)' + absolute = os.path.abspath(os.path.dirname(path)) + return re.match(location,absolute) def commit_in_progress(): @@ -461,3 +495,74 @@ def get_half_cpus(): if cpu > 1: cpu /= 2 return int(cpu) + +def ifname_from_config(conf): + """ + Gets interface name with VLANs from current config level. + Level must be at the interface whose name we want. + + Example: + >>> from vyos.util import ifname_from_config + >>> from vyos.config import Config + >>> conf = Config() + >>> conf.set_level('interfaces ethernet eth0 vif-s 1 vif-c 2') + >>> ifname_from_config(conf) + 'eth0.1.2' + """ + level = conf.get_level() + + # vlans + if level[-2] == 'vif' or level[-2] == 'vif-s': + return level[-3] + '.' + level[-1] + if level[-2] == 'vif-c': + return level[-5] + '.' + level[-3] + '.' + level[-1] + + # no vlans + return level[-1] + +def get_bridge_member_config(conf, br, intf): + """ + Gets bridge port (member) configuration + + Arguments: + conf: Config + br: bridge name + intf: interface name + + Returns: + dict with the configuration + False if bridge or bridge port doesn't exist + """ + old_level = conf.get_level() + conf.set_level([]) + + bridge = f'interfaces bridge {br}' + member = f'{bridge} member interface {intf}' + if not ( conf.exists(bridge) and conf.exists(member) ): + return False + + # default bridge port configuration + # 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 + memberconf = { + 'cost': 100, + 'priority': 32, + 'arp_cache_tmo': 30, + 'disable_link_detect': 1, + } + + if conf.exists(f'{member} cost'): + memberconf['cost'] = int(conf.return_value(f'{member} cost')) + + if conf.exists(f'{member} priority'): + memberconf['priority'] = int(conf.return_value(f'{member} priority')) + + if conf.exists(f'{bridge} ip arp-cache-timeout'): + memberconf['arp_cache_tmo'] = int(conf.return_value(f'{bridge} ip arp-cache-timeout')) + + if conf.exists(f'{bridge} disable-link-detect'): + memberconf['disable_link_detect'] = 2 + + conf.set_level(old_level) + return memberconf diff --git a/python/vyos/validate.py b/python/vyos/validate.py index 446f6e4ca..6304fa8de 100644 --- a/python/vyos/validate.py +++ b/python/vyos/validate.py @@ -205,7 +205,7 @@ def assert_number(n): def assert_positive(n, smaller=0): assert_number(n) if int(n) < smaller: - raise ValueError(f'{n} is smaller than {limit}') + raise ValueError(f'{n} is smaller than {smaller}') def assert_mtu(mtu, min=68, max=9000): @@ -241,26 +241,66 @@ def assert_mac(m): if octets[:5] == (0, 0, 94, 0, 1): raise ValueError(f'{m} is a VRRP MAC address') -def is_bridge_member(conf, interface): +def is_member(conf, interface, intftype=None): """ - Checks if passed interfaces is part of a bridge device or not. - - Returns a tuple: - None -> Interface not a bridge member - Bridge -> Interface is a member of this bridge + Checks if passed interface is member of other interface of specified type. + intftype is optional, if not passed it will search all known types + (currently bridge and bonding) + + Returns: + None -> Interface is not a member + interface name -> Interface is a member of this interface + False -> interface type cannot have members """ ret_val = None - old_level = conf.get_level() + + if intftype not in ['bonding', 'bridge', None]: + raise ValueError(( + f'unknown interface type "{intftype}" or it cannot ' + f'have member interfaces')) + + intftype = ['bonding', 'bridge'] if intftype == None else [intftype] # set config level to root + old_level = conf.get_level() conf.set_level([]) - base = ['interfaces', 'bridge'] - for bridge in conf.list_nodes(base): - members = conf.list_nodes(base + [bridge, 'member', 'interface']) - if interface in members: - ret_val = bridge - break + + for it in intftype: + base = 'interfaces ' + it + for intf in conf.list_nodes(base): + memberintf = f'{base} {intf} member interface' + if conf.is_tag(memberintf): + if interface in conf.list_nodes(memberintf): + ret_val = intf + break + elif conf.is_leaf(memberintf): + if ( conf.exists(memberintf) and + interface in conf.return_values(memberintf) ): + ret_val = intf + break old_level = conf.set_level(old_level) return ret_val +def has_address_configured(conf, intf): + """ + Checks if interface has an address configured. + Checks the following config nodes: + 'address', 'ipv6 address eui64', 'ipv6 address autoconf' + + Returns True if interface has address configured, False if it doesn't. + """ + from vyos.ifconfig import Section + ret = False + + old_level = conf.get_level() + conf.set_level([]) + + intfpath = 'interfaces ' + Section.get_config_path(intf) + if ( conf.exists(f'{intfpath} address') or + conf.exists(f'{intfpath} ipv6 address autoconf') or + conf.exists(f'{intfpath} ipv6 address eui64') ): + ret = True + + conf.set_level(old_level) + return ret |