diff --git a/data/templates/frr/staticd.frr.j2 b/data/templates/frr/staticd.frr.j2 index cf8448f7f..589f03c2c 100644 --- a/data/templates/frr/staticd.frr.j2 +++ b/data/templates/frr/staticd.frr.j2 @@ -1,63 +1,64 @@ {% from 'frr/static_routes_macro.j2' import static_routes %} ! {% set ip_prefix = 'ip' %} {% set ipv6_prefix = 'ipv6' %} {% if vrf is vyos_defined %} {# We need to add an additional whitespace in front of the prefix #} {# when VRFs are in use, thus we use a variable for prefix handling #} {% set ip_prefix = ' ip' %} {% set ipv6_prefix = ' ipv6' %} vrf {{ vrf }} {% endif %} {# IPv4 routing #} {% if route is vyos_defined %} {% for prefix, prefix_config in route.items() %} {{ static_routes(ip_prefix, prefix, prefix_config) }} {% endfor %} {% endif %} {# IPv4 default routes from DHCP interfaces #} {% if dhcp is vyos_defined %} {% for interface, interface_config in dhcp.items() %} -{# PPPoE routes behave a bit different ... #} -{% if interface.startswith('pppoe') and interface_config.default_route is vyos_defined and interface_config.default_route is not vyos_defined('none') %} -{{ ip_prefix }} route 0.0.0.0/0 {{ interface }} tag 210 -{% else %} -{% set next_hop = interface | get_dhcp_router %} -{% if next_hop is vyos_defined %} -{{ ip_prefix }} route 0.0.0.0/0 {{ next_hop }} {{ interface }} tag 210 {{ interface_config.distance if interface_config.distance is vyos_defined }} -{% endif %} +{% set next_hop = interface | get_dhcp_router %} +{% if next_hop is vyos_defined %} +{{ ip_prefix }} route 0.0.0.0/0 {{ next_hop }} {{ interface }} tag 210 {{ interface_config.dhcp_options.default_route_distance if interface_config.dhcp_options.default_route_distance is vyos_defined }} {% endif %} {% endfor %} {% endif %} +{# IPv4 default routes from PPPoE interfaces #} +{% if pppoe is vyos_defined %} +{% for interface, interface_config in pppoe.items() %} +{{ ip_prefix }} route 0.0.0.0/0 {{ interface }} tag 210 {{ interface_config.default_route_distance if interface_config.default_route_distance is vyos_defined }} +{% endfor %} +{% endif %} {# IPv6 routing #} {% if route6 is vyos_defined %} {% for prefix, prefix_config in route6.items() %} {{ static_routes(ipv6_prefix, prefix, prefix_config) }} {% endfor %} {% endif %} {% if vrf is vyos_defined %} exit-vrf {% endif %} ! {# Policy route tables #} {% if table is vyos_defined %} {% for table_id, table_config in table.items() %} {% if table_config.route is vyos_defined %} {% for prefix, prefix_config in table_config.route.items() %} {{ static_routes('ip', prefix, prefix_config, table_id) }} {% endfor %} {% endif %} ! {% if table_config.route6 is vyos_defined %} {% for prefix, prefix_config in table_config.route6.items() %} {{ static_routes('ipv6', prefix, prefix_config, table_id) }} {% endfor %} {% endif %} ! {% endfor %} {% endif %} ! {% if route_map is vyos_defined %} ip protocol static route-map {{ route_map }} ! {% endif %} diff --git a/interface-definitions/include/version/interfaces-version.xml.i b/interface-definitions/include/version/interfaces-version.xml.i index b97971531..0a209bc3a 100644 --- a/interface-definitions/include/version/interfaces-version.xml.i +++ b/interface-definitions/include/version/interfaces-version.xml.i @@ -1,3 +1,3 @@ <!-- include start from include/version/interfaces-version.xml.i --> -<syntaxVersion component='interfaces' version='25'></syntaxVersion> +<syntaxVersion component='interfaces' version='26'></syntaxVersion> <!-- include end --> diff --git a/interface-definitions/interfaces-pppoe.xml.in b/interface-definitions/interfaces-pppoe.xml.in index 3a0b7a40c..8cd8f8c86 100644 --- a/interface-definitions/interfaces-pppoe.xml.in +++ b/interface-definitions/interfaces-pppoe.xml.in @@ -1,143 +1,120 @@ <?xml version="1.0"?> <interfaceDefinition> <node name="interfaces"> <children> <tagNode name="pppoe" owner="${vyos_conf_scripts_dir}/interfaces-pppoe.py"> <properties> <help>Point-to-Point Protocol over Ethernet (PPPoE)</help> <priority>322</priority> <constraint> <regex>^pppoe[0-9]+$</regex> </constraint> <constraintErrorMessage>PPPoE interface must be named pppoeN</constraintErrorMessage> <valueHelp> <format>pppoeN</format> <description>PPPoE dialer interface name</description> </valueHelp> </properties> <children> #include <include/pppoe-access-concentrator.xml.i> #include <include/interface/authentication.xml.i> #include <include/interface/dial-on-demand.xml.i> #include <include/interface/interface-firewall.xml.i> #include <include/interface/interface-policy.xml.i> - <leafNode name="default-route"> - <properties> - <help>Default route insertion behaviour</help> - <completionHelp> - <list>auto none force</list> - </completionHelp> - <constraint> - <regex>^(auto|none|force)$</regex> - </constraint> - <constraintErrorMessage>PPPoE default-route option must be 'auto', 'none', or 'force'</constraintErrorMessage> - <valueHelp> - <format>auto</format> - <description>Automatically install a default route</description> - </valueHelp> - <valueHelp> - <format>none</format> - <description>Do not install a default route</description> - </valueHelp> - <valueHelp> - <format>force</format> - <description>Replace existing default route</description> - </valueHelp> - </properties> - <defaultValue>auto</defaultValue> - </leafNode> + #include <include/interface/no-default-route.xml.i> + #include <include/interface/default-route-distance.xml.i> #include <include/interface/dhcpv6-options.xml.i> #include <include/interface/description.xml.i> #include <include/interface/disable.xml.i> <leafNode name="idle-timeout"> <properties> <help>Delay before disconnecting idle session (in seconds)</help> <valueHelp> <format>u32:0-86400</format> <description>Idle timeout in seconds</description> </valueHelp> <constraint> <validator name="numeric" argument="--range 0-86400"/> </constraint> <constraintErrorMessage>Timeout must be in range 0 to 86400</constraintErrorMessage> </properties> </leafNode> <node name="ip"> <properties> <help>IPv4 routing parameters</help> </properties> <children> #include <include/interface/adjust-mss.xml.i> #include <include/interface/disable-forwarding.xml.i> #include <include/interface/source-validation.xml.i> </children> </node> <node name="ipv6"> <properties> <help>IPv6 routing parameters</help> </properties> <children> <node name="address"> <properties> <help>IPv6 address configuration modes</help> </properties> <children> #include <include/interface/ipv6-address-autoconf.xml.i> </children> </node> #include <include/interface/adjust-mss.xml.i> #include <include/interface/disable-forwarding.xml.i> </children> </node> #include <include/source-interface.xml.i> <leafNode name="local-address"> <properties> <help>IPv4 address of local end of the PPPoE link</help> <valueHelp> <format>ipv4</format> <description>Address of local end of the PPPoE link</description> </valueHelp> <constraint> <validator name="ipv4-address"/> </constraint> </properties> </leafNode> #include <include/interface/mirror.xml.i> #include <include/interface/mtu-68-1500.xml.i> <leafNode name="mtu"> <defaultValue>1492</defaultValue> </leafNode> <leafNode name="no-peer-dns"> <properties> <help>Do not use DNS servers provided by the peer</help> <valueless/> </properties> </leafNode> <leafNode name="remote-address"> <properties> <help>IPv4 address of remote end of the PPPoE link</help> <valueHelp> <format>ipv4</format> <description>Address of remote end of the PPPoE link</description> </valueHelp> <constraint> <validator name="ipv4-address"/> </constraint> </properties> </leafNode> <leafNode name="service-name"> <properties> <help>Service name, only connect to access concentrators advertising this</help> <constraint> <regex>[a-zA-Z0-9]+$</regex> </constraint> <constraintErrorMessage>Service name must be alphanumeric only</constraintErrorMessage> </properties> </leafNode> #include <include/interface/redirect.xml.i> #include <include/interface/vrf.xml.i> </children> </tagNode> </children> </node> </interfaceDefinition> diff --git a/python/vyos/configdict.py b/python/vyos/configdict.py index df4c80f23..399ac6feb 100644 --- a/python/vyos/configdict.py +++ b/python/vyos/configdict.py @@ -1,707 +1,741 @@ # 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/>. """ A library for retrieving value dicts from VyOS configs in a declarative fashion. """ import os import json from vyos.util import dict_search from vyos.xml import defaults from vyos.util import cmd def retrieve_config(path_hash, base_path, config): """ Retrieves a VyOS config as a dict according to a declarative description The description dict, passed in the first argument, must follow this format: ``field_name : <path, type, [inner_options_dict]>``. Supported types are: ``str`` (for normal nodes), ``list`` (returns a list of strings, for multi nodes), ``bool`` (returns True if valueless node exists), ``dict`` (for tag nodes, returns a dict indexed by node names, according to description in the third item of the tuple). Args: path_hash (dict): Declarative description of the config to retrieve base_path (list): A base path to prepend to all option paths config (vyos.config.Config): A VyOS config object Returns: dict: config dict """ config_hash = {} for k in path_hash: if type(path_hash[k]) != tuple: raise ValueError("In field {0}: expected a tuple, got a value {1}".format(k, str(path_hash[k]))) if len(path_hash[k]) < 2: raise ValueError("In field {0}: field description must be a tuple of at least two items, path (list) and type".format(k)) path = path_hash[k][0] if type(path) != list: raise ValueError("In field {0}: path must be a list, not a {1}".format(k, type(path))) typ = path_hash[k][1] if type(typ) != type: raise ValueError("In field {0}: type must be a type, not a {1}".format(k, type(typ))) path = base_path + path path_str = " ".join(path) if typ == str: config_hash[k] = config.return_value(path_str) elif typ == list: config_hash[k] = config.return_values(path_str) elif typ == bool: config_hash[k] = config.exists(path_str) elif typ == dict: try: inner_hash = path_hash[k][2] except IndexError: raise ValueError("The type of the \'{0}\' field is dict, but inner options hash is missing from the tuple".format(k)) config_hash[k] = {} nodes = config.list_nodes(path_str) for node in nodes: config_hash[k][node] = retrieve_config(inner_hash, path + [node], config) return config_hash def dict_merge(source, destination): """ Merge two dictionaries. Only keys which are not present in destination will be copied from source, anything else will be kept untouched. Function will return a new dict which has the merged key/value pairs. """ from copy import deepcopy tmp = deepcopy(destination) for key, value in source.items(): if key not in tmp: tmp[key] = value elif isinstance(source[key], dict): tmp[key] = dict_merge(source[key], tmp[key]) return tmp 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 is_node_changed(conf, path): from vyos.configdiff import get_config_diff D = get_config_diff(conf, key_mangling=('-', '_')) D.set_level(conf.get_level()) return D.is_node_changed(path) def leaf_node_changed(conf, path): """ Check if a leaf node was altered. If it has been altered - values has been changed, or it was added/removed, we will return a list containing the old value(s). If nothing has been changed, None is returned. NOTE: path must use the real CLI node name (e.g. with a hyphen!) """ from vyos.configdiff import get_config_diff D = get_config_diff(conf, key_mangling=('-', '_')) D.set_level(conf.get_level()) (new, old) = D.get_value_diff(path) if new != old: if isinstance(old, dict): # valueLess nodes return {} if node is deleted return True if old is None and isinstance(new, dict): # valueLess nodes return {} if node was added return True if old is None: return [] if isinstance(old, str): return [old] if isinstance(old, list): if isinstance(new, str): new = [new] elif isinstance(new, type(None)): new = [] return list_diff(old, new) return None def node_changed(conf, path, key_mangling=None, recursive=False): """ Check if a leaf node was altered. If it has been altered - values has been changed, or it was added/removed, we will return the old value. If nothing has been changed, None is returned """ from vyos.configdiff import get_config_diff, Diff D = get_config_diff(conf, key_mangling) D.set_level(conf.get_level()) # get_child_nodes() will return dict_keys(), mangle this into a list with PEP448 keys = D.get_child_nodes_diff(path, expand_nodes=Diff.DELETE, recursive=recursive)['delete'].keys() return list(keys) def get_removed_vlans(conf, dict): """ Common function to parse a dictionary retrieved via get_config_dict() and determine any added/removed VLAN interfaces - be it 802.1q or Q-in-Q. """ from vyos.configdiff import get_config_diff, Diff # Check vif, vif-s/vif-c VLAN interfaces for removal D = get_config_diff(conf, key_mangling=('-', '_')) D.set_level(conf.get_level()) # get_child_nodes() will return dict_keys(), mangle this into a list with PEP448 keys = D.get_child_nodes_diff(['vif'], expand_nodes=Diff.DELETE)['delete'].keys() if keys: dict['vif_remove'] = [*keys] # get_child_nodes() will return dict_keys(), mangle this into a list with PEP448 keys = D.get_child_nodes_diff(['vif-s'], expand_nodes=Diff.DELETE)['delete'].keys() if keys: dict['vif_s_remove'] = [*keys] for vif in dict.get('vif_s', {}).keys(): keys = D.get_child_nodes_diff(['vif-s', vif, 'vif-c'], expand_nodes=Diff.DELETE)['delete'].keys() if keys: dict['vif_s'][vif]['vif_c_remove'] = [*keys] return dict def T2665_set_dhcpv6pd_defaults(config_dict): """ Properly configure DHCPv6 default options in the dictionary. If there is no DHCPv6 configured at all, it is safe to remove the entire configuration. """ # As this is the same for every interface type it is safe to assume this # for ethernet pd_defaults = defaults(['interfaces', 'ethernet', 'dhcpv6-options', 'pd']) # Implant default dictionary for DHCPv6-PD instances if dict_search('dhcpv6_options.pd.length', config_dict): del config_dict['dhcpv6_options']['pd']['length'] for pd in (dict_search('dhcpv6_options.pd', config_dict) or []): config_dict['dhcpv6_options']['pd'][pd] = dict_merge(pd_defaults, config_dict['dhcpv6_options']['pd'][pd]) return config_dict def is_member(conf, interface, intftype=None): """ 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 = {} intftypes = ['bonding', 'bridge'] if intftype not in intftypes + [None]: raise ValueError(( f'unknown interface type "{intftype}" or it cannot ' f'have member interfaces')) intftype = intftypes if intftype == None else [intftype] # set config level to root old_level = conf.get_level() conf.set_level([]) for iftype in intftype: base = ['interfaces', iftype] for intf in conf.list_nodes(base): member = base + [intf, 'member', 'interface', interface] if conf.exists(member): tmp = conf.get_config_dict(member, key_mangling=('-', '_'), get_first_key=True, no_tag_node_value_mangle=True) ret_val.update({intf : tmp}) old_level = conf.set_level(old_level) return ret_val def is_mirror_intf(conf, interface, direction=None): """ Check whether the passed interface is used for port mirroring. Direction is optional, if not passed it will search all known direction (currently ingress and egress) Returns: None -> Interface is not a monitor interface Array() -> This interface is a monitor interface of interfaces """ from vyos.ifconfig import Section directions = ['ingress', 'egress'] if direction not in directions + [None]: raise ValueError(f'Unknown interface mirror direction "{direction}"') direction = directions if direction == None else [direction] ret_val = None old_level = conf.get_level() conf.set_level([]) base = ['interfaces'] for dir in direction: for iftype in conf.list_nodes(base): iftype_base = base + [iftype] for intf in conf.list_nodes(iftype_base): mirror = iftype_base + [intf, 'mirror', dir, interface] if conf.exists(mirror): path = ['interfaces', Section.section(intf), intf] tmp = conf.get_config_dict(path, key_mangling=('-', '_'), get_first_key=True) ret_val = {intf : tmp} old_level = conf.set_level(old_level) return ret_val def has_vlan_subinterface_configured(conf, intf): """ Checks if interface has an VLAN subinterface configured. Checks the following config nodes: 'vif', 'vif-s' Return True if interface has VLAN subinterface configured. """ from vyos.ifconfig import Section ret = False old_level = conf.get_level() conf.set_level([]) intfpath = ['interfaces', Section.section(intf), intf] if ( conf.exists(intfpath + ['vif']) or conf.exists(intfpath + ['vif-s'])): ret = True conf.set_level(old_level) return ret def is_source_interface(conf, interface, intftype=None): """ Checks if passed interface is configured as source-interface of other interfaces of specified type. intftype is optional, if not passed it will search all known types (currently pppoe, macsec, pseudo-ethernet, tunnel and vxlan) 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 intftypes = ['macsec', 'pppoe', 'pseudo-ethernet', 'tunnel', 'vxlan'] if intftype not in intftypes + [None]: raise ValueError(f'unknown interface type "{intftype}" or it can not ' 'have a source-interface') intftype = intftypes if intftype == None else [intftype] # set config level to root old_level = conf.get_level() conf.set_level([]) for it in intftype: base = ['interfaces', it] for intf in conf.list_nodes(base): lower_intf = base + [intf, 'source-interface'] if conf.exists(lower_intf) and interface in conf.return_values(lower_intf): ret_val = intf break old_level = conf.set_level(old_level) return ret_val def get_dhcp_interfaces(conf, vrf=None): """ Common helper functions to retrieve all interfaces from current CLI sessions that have DHCP configured. """ + # Cache and reset config level + old_level = conf.get_level() + conf.set_level([]) + dhcp_interfaces = {} dict = conf.get_config_dict(['interfaces'], get_first_key=True) if not dict: return dhcp_interfaces - def check_dhcp(config, ifname): + def check_dhcp(config): + ifname = config['ifname'] tmp = {} - if dict_search('address', config) == 'dhcp' or dict_search('default_route', config) != None: + if 'address' in config and 'dhcp' in config['address']: options = {} if dict_search('dhcp_options.default_route_distance', config) != None: - options.update({'distance' : config['dhcp_options']['default_route_distance']}) - if dict_search('default_route', config) != None: - options.update({'distance' : config['default_route']}) + options.update({'dhcp_options' : config['dhcp_options']}) if 'vrf' in config: if vrf is config['vrf']: tmp.update({ifname : options}) else: tmp.update({ifname : options}) + return tmp for section, interface in dict.items(): for ifname in interface: - # always reset config level + # always reset config level, as get_interface_dict() will alter it conf.set_level([]) # we already have a dict representation of the config from get_config_dict(), # but with the extended information from get_interface_dict() we also # get the DHCP client default-route-distance default option if not specified. ifconfig = get_interface_dict(conf, ['interfaces', section], ifname) - tmp = check_dhcp(ifconfig, ifname) + tmp = check_dhcp(ifconfig) dhcp_interfaces.update(tmp) # check per VLAN interfaces for vif, vif_config in ifconfig.get('vif', {}).items(): - tmp = check_dhcp(vif_config, f'{ifname}.{vif}') + tmp = check_dhcp(vif_config) dhcp_interfaces.update(tmp) # check QinQ VLAN interfaces - for vif_s, vif_s_config in ifconfig.get('vif-s', {}).items(): - tmp = check_dhcp(vif_s_config, f'{ifname}.{vif_s}') + for vif_s, vif_s_config in ifconfig.get('vif_s', {}).items(): + tmp = check_dhcp(vif_s_config) dhcp_interfaces.update(tmp) - for vif_c, vif_c_config in vif_s_config.get('vif-c', {}).items(): - tmp = check_dhcp(vif_c_config, f'{ifname}.{vif_s}.{vif_c}') + for vif_c, vif_c_config in vif_s_config.get('vif_c', {}).items(): + tmp = check_dhcp(vif_c_config) dhcp_interfaces.update(tmp) + # reset old config level return dhcp_interfaces +def get_pppoe_interfaces(conf, vrf=None): + """ Common helper functions to retrieve all interfaces from current CLI + sessions that have DHCP configured. """ + # Cache and reset config level + old_level = conf.get_level() + conf.set_level([]) + + pppoe_interfaces = {} + for ifname in conf.list_nodes(['interfaces', 'pppoe']): + # always reset config level, as get_interface_dict() will alter it + conf.set_level([]) + # we already have a dict representation of the config from get_config_dict(), + # but with the extended information from get_interface_dict() we also + # get the DHCP client default-route-distance default option if not specified. + ifconfig = get_interface_dict(conf, ['interfaces', 'pppoe'], ifname) + + options = {} + if 'default_route_distance' in ifconfig: + options.update({'default_route_distance' : ifconfig['default_route_distance']}) + if 'no_default_route' in ifconfig: + options.update({'no_default_route' : {}}) + if 'vrf' in ifconfig: + if vrf is ifconfig['vrf']: pppoe_interfaces.update({ifname : options}) + else: pppoe_interfaces.update({ifname : options}) + + # reset old config level + conf.set_level(old_level) + return pppoe_interfaces + def get_interface_dict(config, base, ifname=''): """ Common utility function to retrieve and mangle the interfaces configuration from the CLI input nodes. All interfaces have a common base where value retrival is identical. This function must be used whenever possible when working on the interfaces node! Return a dictionary with the necessary interface config keys. """ if not ifname: from vyos import ConfigError # determine tagNode instance if 'VYOS_TAGNODE_VALUE' not in os.environ: raise ConfigError('Interface (VYOS_TAGNODE_VALUE) not specified') ifname = os.environ['VYOS_TAGNODE_VALUE'] # retrieve interface default values default_values = defaults(base) # We take care about VLAN (vif, vif-s, vif-c) default values later on when # parsing vlans in default dict and merge the "proper" values in correctly, # see T2665. for vif in ['vif', 'vif_s']: if vif in default_values: del default_values[vif] # setup config level which is extracted in get_removed_vlans() config.set_level(base + [ifname]) dict = config.get_config_dict([], key_mangling=('-', '_'), get_first_key=True, no_tag_node_value_mangle=True) # Check if interface has been removed. We must use exists() as # get_config_dict() will always return {} - even when an empty interface # node like the following exists. # +macsec macsec1 { # +} if not config.exists([]): dict.update({'deleted' : ''}) # Add interface instance name into dictionary dict.update({'ifname': ifname}) # XXX: T2665: When there is no DHCPv6-PD configuration given, we can safely # remove the default values from the dict. if 'dhcpv6_options' not in dict: if 'dhcpv6_options' in default_values: del default_values['dhcpv6_options'] # We have gathered the dict representation of the CLI, but there are # default options which we need to update into the dictionary retrived. # But we should only add them when interface is not deleted - as this might # confuse parsers if 'deleted' not in dict: dict = dict_merge(default_values, dict) # If interface does not request an IPv4 DHCP address there is no need # to keep the dhcp-options key if 'address' not in dict or 'dhcp' not in dict['address']: if 'dhcp_options' in dict: del dict['dhcp_options'] # XXX: T2665: blend in proper DHCPv6-PD default values dict = T2665_set_dhcpv6pd_defaults(dict) address = leaf_node_changed(config, ['address']) if address: dict.update({'address_old' : address}) # Check if we are a member of a bridge device bridge = is_member(config, ifname, 'bridge') if bridge: dict.update({'is_bridge_member' : bridge}) # Check if it is a monitor interface mirror = is_mirror_intf(config, ifname) if mirror: dict.update({'is_mirror_intf' : mirror}) # Check if we are a member of a bond device bond = is_member(config, ifname, 'bonding') if bond: dict.update({'is_bond_member' : bond}) # Check if any DHCP options changed which require a client restat dhcp = node_changed(config, ['dhcp-options'], recursive=True) if dhcp: dict.update({'dhcp_options_changed' : ''}) # Some interfaces come with a source_interface which must also not be part # of any other bond or bridge interface as it is exclusivly assigned as the # Kernels "lower" interface to this new "virtual/upper" interface. if 'source_interface' in dict: # Check if source interface is member of another bridge tmp = is_member(config, dict['source_interface'], 'bridge') if tmp: dict.update({'source_interface_is_bridge_member' : tmp}) # Check if source interface is member of another bridge tmp = is_member(config, dict['source_interface'], 'bonding') if tmp: dict.update({'source_interface_is_bond_member' : tmp}) mac = leaf_node_changed(config, ['mac']) if mac: dict.update({'mac_old' : mac}) eui64 = leaf_node_changed(config, ['ipv6', 'address', 'eui64']) if eui64: tmp = dict_search('ipv6.address', dict) if not tmp: dict.update({'ipv6': {'address': {'eui64_old': eui64}}}) else: dict['ipv6']['address'].update({'eui64_old': eui64}) # Implant default dictionary in vif/vif-s VLAN interfaces. Values are # identical for all types of VLAN interfaces as they all include the same # XML definitions which hold the defaults. for vif, vif_config in dict.get('vif', {}).items(): # Add subinterface name to dictionary dict['vif'][vif].update({'ifname' : f'{ifname}.{vif}'}) default_vif_values = defaults(base + ['vif']) # XXX: T2665: When there is no DHCPv6-PD configuration given, we can safely # remove the default values from the dict. if not 'dhcpv6_options' in vif_config: del default_vif_values['dhcpv6_options'] # Only add defaults if interface is not about to be deleted - this is # to keep a cleaner config dict. if 'deleted' not in dict: address = leaf_node_changed(config, ['vif', vif, 'address']) if address: dict['vif'][vif].update({'address_old' : address}) dict['vif'][vif] = dict_merge(default_vif_values, dict['vif'][vif]) # XXX: T2665: blend in proper DHCPv6-PD default values dict['vif'][vif] = T2665_set_dhcpv6pd_defaults(dict['vif'][vif]) # If interface does not request an IPv4 DHCP address there is no need # to keep the dhcp-options key if 'address' not in dict['vif'][vif] or 'dhcp' not in dict['vif'][vif]['address']: if 'dhcp_options' in dict['vif'][vif]: del dict['vif'][vif]['dhcp_options'] # Check if we are a member of a bridge device bridge = is_member(config, f'{ifname}.{vif}', 'bridge') if bridge: dict['vif'][vif].update({'is_bridge_member' : bridge}) # Check if any DHCP options changed which require a client restat dhcp = node_changed(config, ['vif', vif, 'dhcp-options'], recursive=True) if dhcp: dict['vif'][vif].update({'dhcp_options_changed' : ''}) for vif_s, vif_s_config in dict.get('vif_s', {}).items(): # Add subinterface name to dictionary dict['vif_s'][vif_s].update({'ifname' : f'{ifname}.{vif_s}'}) default_vif_s_values = defaults(base + ['vif-s']) # XXX: T2665: we only wan't the vif-s defaults - do not care about vif-c if 'vif_c' in default_vif_s_values: del default_vif_s_values['vif_c'] # XXX: T2665: When there is no DHCPv6-PD configuration given, we can safely # remove the default values from the dict. if not 'dhcpv6_options' in vif_s_config: del default_vif_s_values['dhcpv6_options'] # Only add defaults if interface is not about to be deleted - this is # to keep a cleaner config dict. if 'deleted' not in dict: address = leaf_node_changed(config, ['vif-s', vif_s, 'address']) if address: dict['vif_s'][vif_s].update({'address_old' : address}) dict['vif_s'][vif_s] = dict_merge(default_vif_s_values, dict['vif_s'][vif_s]) # XXX: T2665: blend in proper DHCPv6-PD default values dict['vif_s'][vif_s] = T2665_set_dhcpv6pd_defaults(dict['vif_s'][vif_s]) # If interface does not request an IPv4 DHCP address there is no need # to keep the dhcp-options key if 'address' not in dict['vif_s'][vif_s] or 'dhcp' not in \ dict['vif_s'][vif_s]['address']: if 'dhcp_options' in dict['vif_s'][vif_s]: del dict['vif_s'][vif_s]['dhcp_options'] # Check if we are a member of a bridge device bridge = is_member(config, f'{ifname}.{vif_s}', 'bridge') if bridge: dict['vif_s'][vif_s].update({'is_bridge_member' : bridge}) # Check if any DHCP options changed which require a client restat dhcp = node_changed(config, ['vif-s', vif_s, 'dhcp-options'], recursive=True) if dhcp: dict['vif_s'][vif_s].update({'dhcp_options_changed' : ''}) for vif_c, vif_c_config in vif_s_config.get('vif_c', {}).items(): # Add subinterface name to dictionary dict['vif_s'][vif_s]['vif_c'][vif_c].update({'ifname' : f'{ifname}.{vif_s}.{vif_c}'}) default_vif_c_values = defaults(base + ['vif-s', 'vif-c']) # XXX: T2665: When there is no DHCPv6-PD configuration given, we can safely # remove the default values from the dict. if not 'dhcpv6_options' in vif_c_config: del default_vif_c_values['dhcpv6_options'] # Only add defaults if interface is not about to be deleted - this is # to keep a cleaner config dict. if 'deleted' not in dict: address = leaf_node_changed(config, ['vif-s', vif_s, 'vif-c', vif_c, 'address']) if address: dict['vif_s'][vif_s]['vif_c'][vif_c].update( {'address_old' : address}) dict['vif_s'][vif_s]['vif_c'][vif_c] = dict_merge( default_vif_c_values, dict['vif_s'][vif_s]['vif_c'][vif_c]) # XXX: T2665: blend in proper DHCPv6-PD default values dict['vif_s'][vif_s]['vif_c'][vif_c] = T2665_set_dhcpv6pd_defaults( dict['vif_s'][vif_s]['vif_c'][vif_c]) # If interface does not request an IPv4 DHCP address there is no need # to keep the dhcp-options key if 'address' not in dict['vif_s'][vif_s]['vif_c'][vif_c] or 'dhcp' \ not in dict['vif_s'][vif_s]['vif_c'][vif_c]['address']: if 'dhcp_options' in dict['vif_s'][vif_s]['vif_c'][vif_c]: del dict['vif_s'][vif_s]['vif_c'][vif_c]['dhcp_options'] # Check if we are a member of a bridge device bridge = is_member(config, f'{ifname}.{vif_s}.{vif_c}', 'bridge') if bridge: dict['vif_s'][vif_s]['vif_c'][vif_c].update( {'is_bridge_member' : bridge}) # Check if any DHCP options changed which require a client restat dhcp = node_changed(config, ['vif-s', vif_s, 'vif-c', vif_c, 'dhcp-options'], recursive=True) if dhcp: dict['vif_s'][vif_s]['vif_c'][vif_c].update({'dhcp_options_changed' : ''}) # Check vif, vif-s/vif-c VLAN interfaces for removal dict = get_removed_vlans(config, dict) return dict def get_vlan_ids(interface): """ Get the VLAN ID of the interface bound to the bridge """ vlan_ids = set() bridge_status = cmd('bridge -j vlan show', shell=True) vlan_filter_status = json.loads(bridge_status) if vlan_filter_status is not None: for interface_status in vlan_filter_status: ifname = interface_status['ifname'] if interface == ifname: vlans_status = interface_status['vlans'] for vlan_status in vlans_status: vlan_id = vlan_status['vlan'] vlan_ids.add(vlan_id) return vlan_ids def get_accel_dict(config, base, chap_secrets): """ Common utility function to retrieve and mangle the Accel-PPP configuration from different CLI input nodes. All Accel-PPP services have a common base where value retrival is identical. This function must be used whenever possible when working with Accel-PPP services! Return a dictionary with the necessary interface config keys. """ from vyos.util import get_half_cpus from vyos.template import is_ipv4 dict = config.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True, no_tag_node_value_mangle=True) # We have gathered the dict representation of the CLI, but there are default # options which we need to update into the dictionary retrived. default_values = defaults(base) # T2665: defaults include RADIUS server specifics per TAG node which need to # be added to individual RADIUS servers instead - so we can simply delete them if dict_search('authentication.radius.server', default_values): del default_values['authentication']['radius']['server'] # T2665: defaults include static-ip address per TAG node which need to be # added to individual local users instead - so we can simply delete them if dict_search('authentication.local_users.username', default_values): del default_values['authentication']['local_users']['username'] # T2665: defaults include IPv6 client-pool mask per TAG node which need to be # added to individual local users instead - so we can simply delete them if dict_search('client_ipv6_pool.prefix.mask', default_values): del default_values['client_ipv6_pool']['prefix']['mask'] dict = dict_merge(default_values, dict) # set CPUs cores to process requests dict.update({'thread_count' : get_half_cpus()}) # we need to store the path to the secrets file dict.update({'chap_secrets_file' : chap_secrets}) # We can only have two IPv4 and three IPv6 nameservers - also they are # configured in a different way in the configuration, this is why we split # the configuration if 'name_server' in dict: ns_v4 = [] ns_v6 = [] for ns in dict['name_server']: if is_ipv4(ns): ns_v4.append(ns) else: ns_v6.append(ns) dict.update({'name_server_ipv4' : ns_v4, 'name_server_ipv6' : ns_v6}) del dict['name_server'] # Add individual RADIUS server default values if dict_search('authentication.radius.server', dict): # T2665 default_values = defaults(base + ['authentication', 'radius', 'server']) for server in dict_search('authentication.radius.server', dict): dict['authentication']['radius']['server'][server] = dict_merge( default_values, dict['authentication']['radius']['server'][server]) # Check option "disable-accounting" per server and replace default value from '1813' to '0' # set vpn sstp authentication radius server x.x.x.x disable-accounting if 'disable_accounting' in dict['authentication']['radius']['server'][server]: dict['authentication']['radius']['server'][server]['acct_port'] = '0' # Add individual local-user default values if dict_search('authentication.local_users.username', dict): # T2665 default_values = defaults(base + ['authentication', 'local-users', 'username']) for username in dict_search('authentication.local_users.username', dict): dict['authentication']['local_users']['username'][username] = dict_merge( default_values, dict['authentication']['local_users']['username'][username]) # Add individual IPv6 client-pool default mask if required if dict_search('client_ipv6_pool.prefix', dict): # T2665 default_values = defaults(base + ['client-ipv6-pool', 'prefix']) for prefix in dict_search('client_ipv6_pool.prefix', dict): dict['client_ipv6_pool']['prefix'][prefix] = dict_merge( default_values, dict['client_ipv6_pool']['prefix'][prefix]) return dict diff --git a/python/vyos/ifconfig/pppoe.py b/python/vyos/ifconfig/pppoe.py index 1d13264bf..63ffc8069 100644 --- a/python/vyos/ifconfig/pppoe.py +++ b/python/vyos/ifconfig/pppoe.py @@ -1,151 +1,115 @@ # Copyright 2020-2021 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/>. from vyos.ifconfig.interface import Interface from vyos.util import get_interface_config @Interface.register class PPPoEIf(Interface): iftype = 'pppoe' definition = { **Interface.definition, **{ 'section': 'pppoe', 'prefixes': ['pppoe', ], }, } - def _remove_routes(self, vrf=''): + def _remove_routes(self, vrf=None): # Always delete default routes when interface is removed + vrf_cmd = '' if vrf: - vrf = f'-c "vrf {vrf}"' - self._cmd(f'vtysh -c "conf t" {vrf} -c "no ip route 0.0.0.0/0 {self.ifname} tag 210"') - self._cmd(f'vtysh -c "conf t" {vrf} -c "no ipv6 route ::/0 {self.ifname} tag 210"') + vrf_cmd = f'-c "vrf {vrf}"' + self._cmd(f'vtysh -c "conf t" {vrf_cmd} -c "no ip route 0.0.0.0/0 {self.ifname} tag 210"') + self._cmd(f'vtysh -c "conf t" {vrf_cmd} -c "no ipv6 route ::/0 {self.ifname} tag 210"') 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('pppoe0') >>> i.remove() """ - + vrf = None tmp = get_interface_config(self.ifname) - vrf = '' if 'master' in tmp: - self._remove_routes(tmp['master']) + vrf = tmp['master'] + self._remove_routes(vrf) # remove bond master which places members in disabled state super().remove() def _create(self): # we can not create this interface as it is managed outside pass def _delete(self): # we can not create this interface as it is managed outside pass def del_addr(self, addr): # we can not create this interface as it is managed outside pass def get_mac(self): """ Get a synthetic MAC address. """ return self.get_mac_synthetic() def update(self, config): """ General helper function which works on a dictionary retrived by get_config_dict(). It's main intention is to consolidate the scattered interface setup code and provide a single point of entry when workin on any interface. """ # Cache the configuration - it will be reused inside e.g. DHCP handler # XXX: maybe pass the option via __init__ in the future and rename this # method to apply()? # # We need to copy this from super().update() as we utilize self.set_dhcpv6() # before this is done by the base class. self._config = config # remove old routes from an e.g. old VRF assignment - vrf = '' - if 'vrf_old' in config: - vrf = config['vrf_old'] - self._remove_routes(vrf) + if 'shutdown_required': + vrf = None + tmp = get_interface_config(self.ifname) + if 'master' in tmp: + vrf = tmp['master'] + self._remove_routes(vrf) # DHCPv6 PD handling is a bit different on PPPoE interfaces, as we do # not require an 'address dhcpv6' CLI option as with other interfaces if 'dhcpv6_options' in config and 'pd' in config['dhcpv6_options']: self.set_dhcpv6(True) else: self.set_dhcpv6(False) super().update(config) - if 'default_route' not in config or config['default_route'] == 'none': - return - - # - # Set default routes pointing to pppoe interface - # - vrf = '' - sed_opt = '^ip route' - - install_v4 = True - install_v6 = True - # generate proper configuration string when VRFs are in use + vrf = '' if 'vrf' in config: tmp = config['vrf'] vrf = f'-c "vrf {tmp}"' - sed_opt = f'vrf {tmp}' - - if config['default_route'] == 'auto': - # only add route if there is no default route present - tmp = self._cmd(f'vtysh -c "show running-config staticd no-header" | sed -n "/{sed_opt}/,/!/p"') - for line in tmp.splitlines(): - line = line.lstrip() - if line.startswith('ip route 0.0.0.0/0'): - install_v4 = False - continue - - if 'ipv6' in config and line.startswith('ipv6 route ::/0'): - install_v6 = False - continue - - elif config['default_route'] == 'force': - # Force means that all static routes are replaced with the ones from this interface - tmp = self._cmd(f'vtysh -c "show running-config staticd no-header" | sed -n "/{sed_opt}/,/!/p"') - for line in tmp.splitlines(): - if self.ifname in line: - # It makes no sense to remove a route with our interface and the later re-add it. - # This will only make traffic disappear - which is a no-no! - continue - - line = line.lstrip() - if line.startswith('ip route 0.0.0.0/0'): - self._cmd(f'vtysh -c "conf t" {vrf} -c "no {line}"') - - if 'ipv6' in config and line.startswith('ipv6 route ::/0'): - self._cmd(f'vtysh -c "conf t" {vrf} -c "no {line}"') - - if install_v4: - self._cmd(f'vtysh -c "conf t" {vrf} -c "ip route 0.0.0.0/0 {self.ifname} tag 210"') - if install_v6 and 'ipv6' in config: - self._cmd(f'vtysh -c "conf t" {vrf} -c "ipv6 route ::/0 {self.ifname} tag 210"') + + if 'no_default_route' not in config: + # Set default route(s) pointing to PPPoE interface + distance = config['default_route_distance'] + self._cmd(f'vtysh -c "conf t" {vrf} -c "ip route 0.0.0.0/0 {self.ifname} tag 210 {distance}"') + if 'ipv6' in config: + self._cmd(f'vtysh -c "conf t" {vrf} -c "ipv6 route ::/0 {self.ifname} tag 210 {distance}"') diff --git a/smoketest/scripts/cli/test_interfaces_pppoe.py b/smoketest/scripts/cli/test_interfaces_pppoe.py index 4f1e1ee99..16f6e542b 100755 --- a/smoketest/scripts/cli/test_interfaces_pppoe.py +++ b/smoketest/scripts/cli/test_interfaces_pppoe.py @@ -1,164 +1,163 @@ #!/usr/bin/env python3 # -# Copyright (C) 2019-2021 VyOS maintainers and contributors +# Copyright (C) 2019-2022 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as # published by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. import re import unittest from psutil import process_iter from base_vyostest_shim import VyOSUnitTestSHIM from vyos.configsession import ConfigSessionError config_file = '/etc/ppp/peers/{}' base_path = ['interfaces', 'pppoe'] def get_config_value(interface, key): with open(config_file.format(interface), 'r') as f: for line in f: if line.startswith(key): return list(line.split()) return [] # add a classmethod to setup a temporaray PPPoE server for "proper" validation class PPPoEInterfaceTest(VyOSUnitTestSHIM.TestCase): def setUp(self): self._interfaces = ['pppoe10', 'pppoe20', 'pppoe30'] self._source_interface = 'eth0' def tearDown(self): # Validate PPPoE client process for interface in self._interfaces: running = False for proc in process_iter(): if interface in proc.cmdline(): running = True break self.assertTrue(running) self.cli_delete(base_path) self.cli_commit() def test_01_pppoe_client(self): # Check if PPPoE dialer can be configured and runs for interface in self._interfaces: user = 'VyOS-user-' + interface passwd = 'VyOS-passwd-' + interface mtu = '1400' self.cli_set(base_path + [interface, 'authentication', 'user', user]) self.cli_set(base_path + [interface, 'authentication', 'password', passwd]) - self.cli_set(base_path + [interface, 'default-route', 'auto']) self.cli_set(base_path + [interface, 'mtu', mtu]) self.cli_set(base_path + [interface, 'no-peer-dns']) # check validate() - a source-interface is required with self.assertRaises(ConfigSessionError): self.cli_commit() self.cli_set(base_path + [interface, 'source-interface', self._source_interface]) # commit changes self.cli_commit() # verify configuration file(s) for interface in self._interfaces: user = 'VyOS-user-' + interface password = 'VyOS-passwd-' + interface tmp = get_config_value(interface, 'mtu')[1] self.assertEqual(tmp, mtu) tmp = get_config_value(interface, 'user')[1].replace('"', '') self.assertEqual(tmp, user) tmp = get_config_value(interface, 'password')[1].replace('"', '') self.assertEqual(tmp, password) tmp = get_config_value(interface, 'ifname')[1] self.assertEqual(tmp, interface) def test_02_pppoe_client_disabled_interface(self): # Check if PPPoE Client can be disabled for interface in self._interfaces: self.cli_set(base_path + [interface, 'authentication', 'user', 'vyos']) self.cli_set(base_path + [interface, 'authentication', 'password', 'vyos']) self.cli_set(base_path + [interface, 'source-interface', self._source_interface]) self.cli_set(base_path + [interface, 'disable']) self.cli_commit() # Validate PPPoE client process - must not run as interfaces are disabled for interface in self._interfaces: running = False for proc in process_iter(): if interface in proc.cmdline(): running = True break self.assertFalse(running) # enable PPPoE interfaces for interface in self._interfaces: self.cli_delete(base_path + [interface, 'disable']) self.cli_commit() def test_03_pppoe_authentication(self): # When username or password is set - so must be the other for interface in self._interfaces: self.cli_set(base_path + [interface, 'authentication', 'user', 'vyos']) self.cli_set(base_path + [interface, 'source-interface', self._source_interface]) self.cli_set(base_path + [interface, 'ipv6', 'address', 'autoconf']) # check validate() - if user is set, so must be the password with self.assertRaises(ConfigSessionError): self.cli_commit() self.cli_set(base_path + [interface, 'authentication', 'password', 'vyos']) self.cli_commit() def test_04_pppoe_dhcpv6pd(self): # Check if PPPoE dialer can be configured with DHCPv6-PD address = '1' sla_id = '0' sla_len = '8' for interface in self._interfaces: self.cli_set(base_path + [interface, 'authentication', 'user', 'vyos']) self.cli_set(base_path + [interface, 'authentication', 'password', 'vyos']) - self.cli_set(base_path + [interface, 'default-route', 'none']) + self.cli_set(base_path + [interface, 'no-default-route']) self.cli_set(base_path + [interface, 'no-peer-dns']) self.cli_set(base_path + [interface, 'source-interface', self._source_interface]) self.cli_set(base_path + [interface, 'ipv6', 'address', 'autoconf']) # prefix delegation stuff dhcpv6_pd_base = base_path + [interface, 'dhcpv6-options', 'pd', '0'] self.cli_set(dhcpv6_pd_base + ['length', '56']) self.cli_set(dhcpv6_pd_base + ['interface', self._source_interface, 'address', address]) self.cli_set(dhcpv6_pd_base + ['interface', self._source_interface, 'sla-id', sla_id]) # commit changes self.cli_commit() # verify "normal" PPPoE value - 1492 is default MTU tmp = get_config_value(interface, 'mtu')[1] self.assertEqual(tmp, '1492') tmp = get_config_value(interface, 'user')[1].replace('"', '') self.assertEqual(tmp, 'vyos') tmp = get_config_value(interface, 'password')[1].replace('"', '') self.assertEqual(tmp, 'vyos') tmp = get_config_value(interface, '+ipv6 ipv6cp-use-ipaddr') self.assertListEqual(tmp, ['+ipv6', 'ipv6cp-use-ipaddr']) if __name__ == '__main__': unittest.main(verbosity=2) diff --git a/src/conf_mode/interfaces-pppoe.py b/src/conf_mode/interfaces-pppoe.py index bfb1fadd5..279369a32 100755 --- a/src/conf_mode/interfaces-pppoe.py +++ b/src/conf_mode/interfaces-pppoe.py @@ -1,148 +1,137 @@ #!/usr/bin/env python3 # # Copyright (C) 2019-2021 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as # published by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. import os from sys import exit from copy import deepcopy from netifaces import interfaces from vyos.config import Config from vyos.configdict import get_interface_dict +from vyos.configdict import is_node_changed from vyos.configdict import leaf_node_changed +from vyos.configdict import get_pppoe_interfaces from vyos.configverify import verify_authentication from vyos.configverify import verify_source_interface from vyos.configverify import verify_interface_exists from vyos.configverify import verify_vrf from vyos.configverify import verify_mtu_ipv6 from vyos.configverify import verify_mirror_redirect from vyos.ifconfig import PPPoEIf from vyos.template import render from vyos.util import call from vyos.util import is_systemd_service_running from vyos import ConfigError from vyos import airbag airbag.enable() def get_config(config=None): """ Retrive CLI config as dictionary. Dictionary can never be empty, as at least the interface name will be added or a deleted flag """ if config: conf = config else: conf = Config() base = ['interfaces', 'pppoe'] pppoe = get_interface_dict(conf, base) # We should only terminate the PPPoE session if critical parameters change. # All parameters that can be changed on-the-fly (like interface description) # should not lead to a reconnect! - tmp = leaf_node_changed(conf, ['access-concentrator']) - if tmp: pppoe.update({'shutdown_required': {}}) - - tmp = leaf_node_changed(conf, ['connect-on-demand']) - if tmp: pppoe.update({'shutdown_required': {}}) - - tmp = leaf_node_changed(conf, ['service-name']) - if tmp: pppoe.update({'shutdown_required': {}}) - - tmp = leaf_node_changed(conf, ['source-interface']) - if tmp: pppoe.update({'shutdown_required': {}}) - - tmp = leaf_node_changed(conf, ['vrf']) - # leaf_node_changed() returns a list, as VRF is a non-multi node, there - # will be only one list element - if tmp: pppoe.update({'vrf_old': tmp[0]}) - - tmp = leaf_node_changed(conf, ['authentication', 'user']) - if tmp: pppoe.update({'shutdown_required': {}}) - - tmp = leaf_node_changed(conf, ['authentication', 'password']) - if tmp: pppoe.update({'shutdown_required': {}}) + for options in ['access-concentrator', 'connect-on-demand', 'service-name', + 'source-interface', 'vrf', 'no-default-route', 'authentication']: + if is_node_changed(conf, options): + pppoe.update({'shutdown_required': {}}) + # bail out early - no need to further process other nodes + break return pppoe def verify(pppoe): if 'deleted' in pppoe: # bail out early return None verify_source_interface(pppoe) verify_authentication(pppoe) verify_vrf(pppoe) verify_mtu_ipv6(pppoe) verify_mirror_redirect(pppoe) if {'connect_on_demand', 'vrf'} <= set(pppoe): raise ConfigError('On-demand dialing and VRF can not be used at the same time') return None def generate(pppoe): # set up configuration file path variables where our templates will be # rendered into ifname = pppoe['ifname'] config_pppoe = f'/etc/ppp/peers/{ifname}' if 'deleted' in pppoe or 'disable' in pppoe: if os.path.exists(config_pppoe): os.unlink(config_pppoe) return None # Create PPP configuration files render(config_pppoe, 'pppoe/peer.tmpl', pppoe, permission=0o640) return None def apply(pppoe): ifname = pppoe['ifname'] if 'deleted' in pppoe or 'disable' in pppoe: if os.path.isdir(f'/sys/class/net/{ifname}'): p = PPPoEIf(ifname) p.remove() call(f'systemctl stop ppp@{ifname}.service') return None # reconnect should only be necessary when certain config options change, - # like ACS name, authentication, no-peer-dns, source-interface + # like ACS name, authentication ... (see get_config() for details) if ((not is_systemd_service_running(f'ppp@{ifname}.service')) or 'shutdown_required' in pppoe): # cleanup system (e.g. FRR routes first) if os.path.isdir(f'/sys/class/net/{ifname}'): p = PPPoEIf(ifname) p.remove() call(f'systemctl restart ppp@{ifname}.service') + # When interface comes "live" a hook is called: + # /etc/ppp/ip-up.d/99-vyos-pppoe-callback + # which triggers PPPoEIf.update() else: if os.path.isdir(f'/sys/class/net/{ifname}'): p = PPPoEIf(ifname) p.update(pppoe) return None if __name__ == '__main__': try: c = get_config() verify(c) generate(c) apply(c) except ConfigError as e: print(e) exit(1) diff --git a/src/conf_mode/protocols_static.py b/src/conf_mode/protocols_static.py index 87432bc1c..58e202928 100755 --- a/src/conf_mode/protocols_static.py +++ b/src/conf_mode/protocols_static.py @@ -1,131 +1,134 @@ #!/usr/bin/env python3 # # Copyright (C) 2021 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as # published by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. import os from sys import exit from sys import argv from vyos.config import Config from vyos.configdict import dict_merge from vyos.configdict import get_dhcp_interfaces +from vyos.configdict import get_pppoe_interfaces from vyos.configverify import verify_common_route_maps from vyos.configverify import verify_vrf from vyos.template import render_to_string from vyos import ConfigError from vyos import frr from vyos import airbag airbag.enable() def get_config(config=None): if config: conf = config else: conf = Config() vrf = None if len(argv) > 1: vrf = argv[1] base_path = ['protocols', 'static'] # eqivalent of the C foo ? 'a' : 'b' statement base = vrf and ['vrf', 'name', vrf, 'protocols', 'static'] or base_path static = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True) # Assign the name of our VRF context if vrf: static['vrf'] = vrf # We also need some additional information from the config, prefix-lists # and route-maps for instance. They will be used in verify(). # # XXX: one MUST always call this without the key_mangling() option! See # vyos.configverify.verify_common_route_maps() for more information. tmp = conf.get_config_dict(['policy']) # Merge policy dict into "regular" config dict static = dict_merge(tmp, static) # T3680 - get a list of all interfaces currently configured to use DHCP tmp = get_dhcp_interfaces(conf, vrf) - if tmp: static['dhcp'] = tmp + if tmp: static.update({'dhcp' : tmp}) + tmp = get_pppoe_interfaces(conf, vrf) + if tmp: static.update({'pppoe' : tmp}) return static def verify(static): verify_common_route_maps(static) for route in ['route', 'route6']: # if there is no route(6) key in the dictionary we can immediately # bail out early if route not in static: continue # When leaking routes to other VRFs we must ensure that the destination # VRF exists for prefix, prefix_options in static[route].items(): # both the interface and next-hop CLI node can have a VRF subnode, # thus we check this using a for loop for type in ['interface', 'next_hop']: if type in prefix_options: for interface, interface_config in prefix_options[type].items(): verify_vrf(interface_config) if {'blackhole', 'reject'} <= set(prefix_options): raise ConfigError(f'Can not use both blackhole and reject for '\ 'prefix "{prefix}"!') return None def generate(static): if not static: return None static['new_frr_config'] = render_to_string('frr/staticd.frr.j2', static) return None def apply(static): static_daemon = 'staticd' zebra_daemon = 'zebra' # Save original configuration prior to starting any commit actions frr_cfg = frr.FRRConfig() # The route-map used for the FIB (zebra) is part of the zebra daemon frr_cfg.load_configuration(zebra_daemon) frr_cfg.modify_section(r'^ip protocol static route-map [-a-zA-Z0-9.]+', '') frr_cfg.commit_configuration(zebra_daemon) frr_cfg.load_configuration(static_daemon) if 'vrf' in static: vrf = static['vrf'] frr_cfg.modify_section(f'^vrf {vrf}', stop_pattern='^exit', remove_stop_mark=True) else: frr_cfg.modify_section(r'^ip route .*') frr_cfg.modify_section(r'^ipv6 route .*') if 'new_frr_config' in static: frr_cfg.add_before(frr.default_add_before, static['new_frr_config']) frr_cfg.commit_configuration(static_daemon) return None if __name__ == '__main__': try: c = get_config() verify(c) generate(c) apply(c) except ConfigError as e: print(e) exit(1) diff --git a/src/etc/ppp/ip-up.d/99-vyos-pppoe-callback b/src/etc/ppp/ip-up.d/99-vyos-pppoe-callback index bb918a468..78ca09010 100755 --- a/src/etc/ppp/ip-up.d/99-vyos-pppoe-callback +++ b/src/etc/ppp/ip-up.d/99-vyos-pppoe-callback @@ -1,59 +1,59 @@ #!/usr/bin/env python3 # -# Copyright (C) 2021 VyOS maintainers and contributors +# Copyright (C) 2021-2022 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as # published by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. # This is a Python hook script which is invoked whenever a PPPoE session goes # "ip-up". It will call into our vyos.ifconfig library and will then execute # common tasks for the PPPoE interface. The reason we have to "hook" this is # that we can not create a pppoeX interface in advance in linux and then connect # pppd to this already existing interface. from sys import argv from sys import exit from syslog import syslog from syslog import openlog from syslog import LOG_PID from syslog import LOG_INFO -from vyos.configquery import ConfigTreeQuery +from vyos.configquery import Config +from vyos.configdict import get_interface_dict from vyos.ifconfig import PPPoEIf from vyos.util import read_file # When the ppp link comes up, this script is called with the following # parameters # $1 the interface name used by pppd (e.g. ppp3) # $2 the tty device name # $3 the tty device speed # $4 the local IP address for the interface # $5 the remote IP address # $6 the parameter specified by the 'ipparam' option to pppd if (len(argv) < 7): exit(1) interface = argv[6] dialer_pid = read_file(f'/var/run/{interface}.pid') openlog(ident=f'pppd[{dialer_pid}]', facility=LOG_INFO) syslog('executing ' + argv[0]) -conf = ConfigTreeQuery() -pppoe = conf.get_config_dict(['interfaces', 'pppoe', argv[6]], - get_first_key=True, key_mangling=('-', '_')) -pppoe['ifname'] = argv[6] +conf = Config() +pppoe = get_interface_dict(conf, ['interfaces', 'pppoe'], argv[6]) +# Update the config p = PPPoEIf(pppoe['ifname']) p.update(pppoe) diff --git a/src/migration-scripts/interfaces/25-to-26 b/src/migration-scripts/interfaces/25-to-26 new file mode 100755 index 000000000..a8936235e --- /dev/null +++ b/src/migration-scripts/interfaces/25-to-26 @@ -0,0 +1,54 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2022 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +# T4384: pppoe: replace default-route CLI option with common CLI nodes already +# present for DHCP + +from sys import argv + +from vyos.ethtool import Ethtool +from vyos.configtree import ConfigTree + +if (len(argv) < 1): + print("Must specify file name!") + exit(1) + +file_name = argv[1] +with open(file_name, 'r') as f: + config_file = f.read() + +base = ['interfaces', 'pppoe'] +config = ConfigTree(config_file) + +if not config.exists(base): + exit(0) + +for ifname in config.list_nodes(base): + tmp_config = base + [ifname, 'default-route'] + if config.exists(tmp_config): + # Retrieve current config value + value = config.return_value(tmp_config) + # Delete old Config node + config.delete(tmp_config) + if value == 'none': + config.set(base + [ifname, 'no-default-route']) + +try: + with open(file_name, 'w') as f: + f.write(config.to_string()) +except OSError as e: + print(f'Failed to save the modified config: {e}') + exit(1)