diff --git a/src/conf_mode/interfaces_bonding.py b/src/conf_mode/interfaces_bonding.py index 5f839b33c..0844d2913 100755 --- a/src/conf_mode/interfaces_bonding.py +++ b/src/conf_mode/interfaces_bonding.py @@ -1,297 +1,299 @@ #!/usr/bin/env python3 # # Copyright (C) 2019-2024 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/>. from sys import exit from vyos.config import Config from vyos.configdict import get_frrender_dict 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 is_member from vyos.configdict import is_source_interface from vyos.configverify import verify_address from vyos.configverify import verify_bridge_delete from vyos.configverify import verify_dhcpv6 from vyos.configverify import verify_eapol from vyos.configverify import verify_mirror_redirect from vyos.configverify import verify_mtu_ipv6 from vyos.configverify import verify_vlan_config from vyos.configverify import verify_vrf from vyos.frrender import FRRender from vyos.ifconfig import BondIf from vyos.ifconfig.ethernet import EthernetIf from vyos.ifconfig import Section from vyos.utils.assertion import assert_mac from vyos.utils.dict import dict_search from vyos.utils.dict import dict_to_paths_values from vyos.utils.network import interface_exists +from vyos.utils.process import is_systemd_service_running from vyos.configdict import has_address_configured from vyos.configdict import has_vrf_configured -from vyos.configdep import set_dependents, call_dependents +from vyos.configdep import set_dependents +from vyos.configdep import call_dependents from vyos import ConfigError from vyos import airbag airbag.enable() def get_bond_mode(mode): if mode == 'round-robin': return 'balance-rr' elif mode == 'active-backup': return 'active-backup' elif mode == 'xor-hash': return 'balance-xor' elif mode == 'broadcast': return 'broadcast' elif mode == '802.3ad': return '802.3ad' elif mode == 'transmit-load-balance': return 'balance-tlb' elif mode == 'adaptive-load-balance': return 'balance-alb' else: raise ConfigError(f'invalid bond mode "{mode}"') 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', 'bonding'] ifname, bond = get_interface_dict(conf, base, with_pki=True) # To make our own life easier transfor the list of member interfaces # into a dictionary - we will use this to add additional information # later on for each member if 'member' in bond and 'interface' in bond['member']: # convert list of member interfaces to a dictionary bond['member']['interface'] = {k: {} for k in bond['member']['interface']} if 'mode' in bond: bond['mode'] = get_bond_mode(bond['mode']) tmp = is_node_changed(conf, base + [ifname, 'mode']) if tmp: bond.update({'shutdown_required' : {}}) tmp = is_node_changed(conf, base + [ifname, 'lacp-rate']) if tmp: bond.update({'shutdown_required' : {}}) tmp = is_node_changed(conf, base + [ifname, 'evpn']) if tmp: bond.update({'frr_dict' : get_frrender_dict(conf)}) # determine which members have been removed interfaces_removed = leaf_node_changed(conf, base + [ifname, 'member', 'interface']) # Reset config level to interfaces old_level = conf.get_level() conf.set_level(['interfaces']) if interfaces_removed: bond['shutdown_required'] = {} if 'member' not in bond: bond['member'] = {} tmp = {} for interface in interfaces_removed: # if member is deleted from bond, add dependencies to call # ethernet commit again in apply function # to apply options under ethernet section set_dependents('ethernet', conf, interface) section = Section.section(interface) # this will be 'ethernet' for 'eth0' if conf.exists([section, interface, 'disable']): tmp[interface] = {'disable': ''} else: tmp[interface] = {} # also present the interfaces to be removed from the bond as dictionary bond['member']['interface_remove'] = tmp # Restore existing config level conf.set_level(old_level) if dict_search('member.interface', bond): for interface, interface_config in bond['member']['interface'].items(): interface_ethernet_config = conf.get_config_dict( ['interfaces', 'ethernet', interface], key_mangling=('-', '_'), get_first_key=True, no_tag_node_value_mangle=True, with_defaults=False, with_recursive_defaults=False) interface_config['config_paths'] = dict_to_paths_values(interface_ethernet_config) # Check if member interface is a new member if not conf.exists_effective(base + [ifname, 'member', 'interface', interface]): bond['shutdown_required'] = {} interface_config['new_added'] = {} # Check if member interface is disabled conf.set_level(['interfaces']) section = Section.section(interface) # this will be 'ethernet' for 'eth0' if conf.exists([section, interface, 'disable']): interface_config['disable'] = '' conf.set_level(old_level) # Check if member interface is already member of another bridge tmp = is_member(conf, interface, 'bridge') if tmp: interface_config['is_bridge_member'] = tmp # Check if member interface is already member of a bond tmp = is_member(conf, interface, 'bonding') for tmp in is_member(conf, interface, 'bonding'): if bond['ifname'] == tmp: continue interface_config['is_bond_member'] = tmp # Check if member interface is used as source-interface on another interface tmp = is_source_interface(conf, interface) if tmp: interface_config['is_source_interface'] = tmp # bond members must not have an assigned address tmp = has_address_configured(conf, interface) if tmp: interface_config['has_address'] = {} # bond members must not have a VRF attached tmp = has_vrf_configured(conf, interface) if tmp: interface_config['has_vrf'] = {} return bond def verify(bond): if 'deleted' in bond: verify_bridge_delete(bond) return None if 'arp_monitor' in bond: if 'target' in bond['arp_monitor'] and len(bond['arp_monitor']['target']) > 16: raise ConfigError('The maximum number of arp-monitor targets is 16') if 'interval' in bond['arp_monitor'] and int(bond['arp_monitor']['interval']) > 0: if bond['mode'] in ['802.3ad', 'balance-tlb', 'balance-alb']: raise ConfigError('ARP link monitoring does not work for mode 802.3ad, ' \ 'transmit-load-balance or adaptive-load-balance') if 'primary' in bond: if bond['mode'] not in ['active-backup', 'balance-tlb', 'balance-alb']: raise ConfigError('Option primary - mode dependency failed, not' 'supported in mode {mode}!'.format(**bond)) verify_mtu_ipv6(bond) verify_address(bond) verify_dhcpv6(bond) verify_vrf(bond) verify_mirror_redirect(bond) verify_eapol(bond) # use common function to verify VLAN configuration verify_vlan_config(bond) bond_name = bond['ifname'] if dict_search('member.interface', bond): for interface, interface_config in bond['member']['interface'].items(): error_msg = f'Can not add interface "{interface}" to bond, ' if interface == 'lo': raise ConfigError('Loopback interface "lo" can not be added to a bond') if not interface_exists(interface): raise ConfigError(error_msg + 'it does not exist!') if 'is_bridge_member' in interface_config: tmp = next(iter(interface_config['is_bridge_member'])) raise ConfigError(error_msg + f'it is already a member of bridge "{tmp}"!') if 'is_bond_member' in interface_config: tmp = next(iter(interface_config['is_bond_member'])) raise ConfigError(error_msg + f'it is already a member of bond "{tmp}"!') if 'is_source_interface' in interface_config: tmp = interface_config['is_source_interface'] raise ConfigError(error_msg + f'it is the source-interface of "{tmp}"!') if 'has_address' in interface_config: raise ConfigError(error_msg + 'it has an address assigned!') if 'has_vrf' in interface_config: raise ConfigError(error_msg + 'it has a VRF assigned!') if 'new_added' in interface_config and 'config_paths' in interface_config: for option_path, option_value in interface_config['config_paths'].items(): if option_path in EthernetIf.get_bond_member_allowed_options() : continue if option_path in BondIf.get_inherit_bond_options(): continue raise ConfigError(error_msg + f'it has a "{option_path.replace(".", " ")}" assigned!') if 'primary' in bond: if bond['primary'] not in bond['member']['interface']: raise ConfigError(f'Primary interface of bond "{bond_name}" must be a member interface') if bond['mode'] not in ['active-backup', 'balance-tlb', 'balance-alb']: raise ConfigError('primary interface only works for mode active-backup, ' \ 'transmit-load-balance or adaptive-load-balance') if 'system_mac' in bond: if bond['mode'] != '802.3ad': raise ConfigError('Actor MAC address only available in 802.3ad mode!') system_mac = bond['system_mac'] try: assert_mac(system_mac, test_all_zero=False) except: raise ConfigError(f'Cannot use a multicast MAC address "{system_mac}" as system-mac!') return None def generate(bond): - if 'frr_dict' in bond and 'frrender_cls' not in bond['frr_dict']: + if 'frr_dict' in bond and not is_systemd_service_running('vyos-configd.service'): FRRender().generate(bond['frr_dict']) return None def apply(bond): - if 'frr_dict' in bond and 'frrender_cls' not in bond['frr_dict']: + if 'frr_dict' in bond and not is_systemd_service_running('vyos-configd.service'): FRRender().apply() b = BondIf(bond['ifname']) if 'deleted' in bond: b.remove() else: b.update(bond) if dict_search('member.interface_remove', bond): try: call_dependents() except ConfigError: raise ConfigError('Error in updating ethernet interface ' 'after deleting it from bond') 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/interfaces_ethernet.py b/src/conf_mode/interfaces_ethernet.py index accfb6b8e..5024e6982 100755 --- a/src/conf_mode/interfaces_ethernet.py +++ b/src/conf_mode/interfaces_ethernet.py @@ -1,348 +1,347 @@ #!/usr/bin/env python3 # # Copyright (C) 2019-2024 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 vyos.base import Warning from vyos.config import Config from vyos.configdict import get_frrender_dict from vyos.configdict import get_interface_dict from vyos.configdict import is_node_changed from vyos.configverify import verify_address from vyos.configverify import verify_dhcpv6 from vyos.configverify import verify_interface_exists from vyos.configverify import verify_mirror_redirect from vyos.configverify import verify_mtu from vyos.configverify import verify_mtu_ipv6 from vyos.configverify import verify_vlan_config from vyos.configverify import verify_vrf from vyos.configverify import verify_bond_bridge_member from vyos.configverify import verify_eapol from vyos.ethtool import Ethtool from vyos.frrender import FRRender from vyos.ifconfig import EthernetIf from vyos.ifconfig import BondIf from vyos.utils.dict import dict_search from vyos.utils.dict import dict_to_paths_values from vyos.utils.dict import dict_set from vyos.utils.dict import dict_delete +from vyos.utils.process import is_systemd_service_running from vyos import ConfigError from vyos import airbag airbag.enable() def update_bond_options(conf: Config, eth_conf: dict) -> list: """ Return list of blocked options if interface is a bond member :param conf: Config object :type conf: Config :param eth_conf: Ethernet config dictionary :type eth_conf: dict :return: List of blocked options :rtype: list """ blocked_list = [] bond_name = list(eth_conf['is_bond_member'].keys())[0] config_without_defaults = conf.get_config_dict( ['interfaces', 'ethernet', eth_conf['ifname']], key_mangling=('-', '_'), get_first_key=True, no_tag_node_value_mangle=True, with_defaults=False, with_recursive_defaults=False) config_with_defaults = conf.get_config_dict( ['interfaces', 'ethernet', eth_conf['ifname']], key_mangling=('-', '_'), get_first_key=True, no_tag_node_value_mangle=True, with_defaults=True, with_recursive_defaults=True) bond_config_with_defaults = conf.get_config_dict( ['interfaces', 'bonding', bond_name], key_mangling=('-', '_'), get_first_key=True, no_tag_node_value_mangle=True, with_defaults=True, with_recursive_defaults=True) eth_dict_paths = dict_to_paths_values(config_without_defaults) eth_path_base = ['interfaces', 'ethernet', eth_conf['ifname']] #if option is configured under ethernet section for option_path, option_value in eth_dict_paths.items(): bond_option_value = dict_search(option_path, bond_config_with_defaults) #If option is allowed for changing then continue if option_path in EthernetIf.get_bond_member_allowed_options(): continue # if option is inherited from bond then set valued from bond interface if option_path in BondIf.get_inherit_bond_options(): # If option equals to bond option then do nothing if option_value == bond_option_value: continue else: # if ethernet has option and bond interface has # then copy it from bond if bond_option_value is not None: if is_node_changed(conf, eth_path_base + option_path.split('.')): Warning( f'Cannot apply "{option_path.replace(".", " ")}" to "{option_value}".' \ f' Interface "{eth_conf["ifname"]}" is a bond member.' \ f' Option is inherited from bond "{bond_name}"') dict_set(option_path, bond_option_value, eth_conf) continue # if ethernet has option and bond interface does not have # then delete it form dict and do not apply it else: if is_node_changed(conf, eth_path_base + option_path.split('.')): Warning( f'Cannot apply "{option_path.replace(".", " ")}".' \ f' Interface "{eth_conf["ifname"]}" is a bond member.' \ f' Option is inherited from bond "{bond_name}"') dict_delete(option_path, eth_conf) blocked_list.append(option_path) # if inherited option is not configured under ethernet section but configured under bond section for option_path in BondIf.get_inherit_bond_options(): bond_option_value = dict_search(option_path, bond_config_with_defaults) if bond_option_value is not None: if option_path not in eth_dict_paths: if is_node_changed(conf, eth_path_base + option_path.split('.')): Warning( f'Cannot apply "{option_path.replace(".", " ")}" to "{dict_search(option_path, config_with_defaults)}".' \ f' Interface "{eth_conf["ifname"]}" is a bond member. ' \ f'Option is inherited from bond "{bond_name}"') dict_set(option_path, bond_option_value, eth_conf) eth_conf['bond_blocked_changes'] = blocked_list return None 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', 'ethernet'] ifname, ethernet = get_interface_dict(conf, base, with_pki=True) # T5862 - default MTU is not acceptable in some environments # There are cloud environments available where the maximum supported # ethernet MTU is e.g. 1450 bytes, thus we clamp this to the adapters # maximum MTU value or 1500 bytes - whatever is lower if 'mtu' not in ethernet: try: ethernet['mtu'] = '1500' max_mtu = EthernetIf(ifname).get_max_mtu() if max_mtu < int(ethernet['mtu']): ethernet['mtu'] = str(max_mtu) except: pass if 'is_bond_member' in ethernet: update_bond_options(conf, ethernet) tmp = is_node_changed(conf, base + [ifname, 'speed']) if tmp: ethernet.update({'speed_duplex_changed': {}}) tmp = is_node_changed(conf, base + [ifname, 'duplex']) if tmp: ethernet.update({'speed_duplex_changed': {}}) tmp = is_node_changed(conf, base + [ifname, 'evpn']) if tmp: ethernet.update({'frr_dict' : get_frrender_dict(conf)}) return ethernet def verify_speed_duplex(ethernet: dict, ethtool: Ethtool): """ Verify speed and duplex :param ethernet: dictionary which is received from get_interface_dict :type ethernet: dict :param ethtool: Ethernet object :type ethtool: Ethtool """ if ((ethernet['speed'] == 'auto' and ethernet['duplex'] != 'auto') or (ethernet['speed'] != 'auto' and ethernet['duplex'] == 'auto')): raise ConfigError( 'Speed/Duplex missmatch. Must be both auto or manually configured') if ethernet['speed'] != 'auto' and ethernet['duplex'] != 'auto': # We need to verify if the requested speed and duplex setting is # supported by the underlaying NIC. speed = ethernet['speed'] duplex = ethernet['duplex'] if not ethtool.check_speed_duplex(speed, duplex): raise ConfigError( f'Adapter does not support changing speed ' \ f'and duplex settings to: {speed}/{duplex}!') def verify_flow_control(ethernet: dict, ethtool: Ethtool): """ Verify flow control :param ethernet: dictionary which is received from get_interface_dict :type ethernet: dict :param ethtool: Ethernet object :type ethtool: Ethtool """ if 'disable_flow_control' in ethernet: if not ethtool.check_flow_control(): raise ConfigError( 'Adapter does not support changing flow-control settings!') def verify_ring_buffer(ethernet: dict, ethtool: Ethtool): """ Verify ring buffer :param ethernet: dictionary which is received from get_interface_dict :type ethernet: dict :param ethtool: Ethernet object :type ethtool: Ethtool """ if 'ring_buffer' in ethernet: max_rx = ethtool.get_ring_buffer_max('rx') if not max_rx: raise ConfigError( 'Driver does not support RX ring-buffer configuration!') max_tx = ethtool.get_ring_buffer_max('tx') if not max_tx: raise ConfigError( 'Driver does not support TX ring-buffer configuration!') rx = dict_search('ring_buffer.rx', ethernet) if rx and int(rx) > int(max_rx): raise ConfigError(f'Driver only supports a maximum RX ring-buffer ' \ f'size of "{max_rx}" bytes!') tx = dict_search('ring_buffer.tx', ethernet) if tx and int(tx) > int(max_tx): raise ConfigError(f'Driver only supports a maximum TX ring-buffer ' \ f'size of "{max_tx}" bytes!') def verify_offload(ethernet: dict, ethtool: Ethtool): """ Verify offloading capabilities :param ethernet: dictionary which is received from get_interface_dict :type ethernet: dict :param ethtool: Ethernet object :type ethtool: Ethtool """ if dict_search('offload.rps', ethernet) != None: if not os.path.exists(f'/sys/class/net/{ethernet["ifname"]}/queues/rx-0/rps_cpus'): raise ConfigError('Interface does not suport RPS!') driver = ethtool.get_driver_name() # T3342 - Xen driver requires special treatment if driver == 'vif': if int(ethernet['mtu']) > 1500 and dict_search('offload.sg', ethernet) == None: raise ConfigError('Xen netback drivers requires scatter-gatter offloading '\ 'for MTU size larger then 1500 bytes') def verify_allowedbond_changes(ethernet: dict): """ Verify changed options if interface is in bonding :param ethernet: dictionary which is received from get_interface_dict :type ethernet: dict """ if 'bond_blocked_changes' in ethernet: for option in ethernet['bond_blocked_changes']: raise ConfigError(f'Cannot configure "{option.replace(".", " ")}"' \ f' on interface "{ethernet["ifname"]}".' \ f' Interface is a bond member') def verify(ethernet): if 'deleted' in ethernet: return None if 'is_bond_member' in ethernet: verify_bond_member(ethernet) else: verify_ethernet(ethernet) def verify_bond_member(ethernet): """ Verification function for ethernet interface which is in bonding :param ethernet: dictionary which is received from get_interface_dict :type ethernet: dict """ ifname = ethernet['ifname'] verify_interface_exists(ethernet, ifname) verify_eapol(ethernet) verify_mirror_redirect(ethernet) ethtool = Ethtool(ifname) verify_speed_duplex(ethernet, ethtool) verify_flow_control(ethernet, ethtool) verify_ring_buffer(ethernet, ethtool) verify_offload(ethernet, ethtool) verify_allowedbond_changes(ethernet) def verify_ethernet(ethernet): """ Verification function for simple ethernet interface :param ethernet: dictionary which is received from get_interface_dict :type ethernet: dict """ ifname = ethernet['ifname'] verify_interface_exists(ethernet, ifname) verify_mtu(ethernet) verify_mtu_ipv6(ethernet) verify_dhcpv6(ethernet) verify_address(ethernet) verify_vrf(ethernet) verify_bond_bridge_member(ethernet) verify_eapol(ethernet) verify_mirror_redirect(ethernet) ethtool = Ethtool(ifname) # No need to check speed and duplex keys as both have default values. verify_speed_duplex(ethernet, ethtool) verify_flow_control(ethernet, ethtool) verify_ring_buffer(ethernet, ethtool) verify_offload(ethernet, ethtool) # use common function to verify VLAN configuration verify_vlan_config(ethernet) return None def generate(ethernet): - if 'frr_dict' in ethernet and 'frrender_cls' not in ethernet['frr_dict']: + if 'frr_dict' in ethernet and not is_systemd_service_running('vyos-configd.service'): FRRender().generate(ethernet['frr_dict']) return None def apply(ethernet): - if 'frr_dict' in ethernet and 'frrender_cls' not in ethernet['frr_dict']: + if 'frr_dict' in ethernet and not is_systemd_service_running('vyos-configd.service'): FRRender().apply() - e = EthernetIf(ethernet['ifname']) if 'deleted' in ethernet: e.remove() else: e.update(ethernet) 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/policy.py b/src/conf_mode/policy.py index 4abb150ac..5e71a612d 100755 --- a/src/conf_mode/policy.py +++ b/src/conf_mode/policy.py @@ -1,282 +1,283 @@ #!/usr/bin/env python3 # # Copyright (C) 2021-2024 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/>. from sys import exit from vyos.config import Config from vyos.configdict import get_frrender_dict from vyos.configverify import has_frr_protocol_in_dict from vyos.frrender import FRRender from vyos.frrender import frr_protocols from vyos.utils.dict import dict_search +from vyos.utils.process import is_systemd_service_running from vyos import ConfigError from vyos import airbag airbag.enable() def community_action_compatibility(actions: dict) -> bool: """ Check compatibility of values in community and large community sections :param actions: dictionary with community :type actions: dict :return: true if compatible, false if not :rtype: bool """ if ('none' in actions) and ('replace' in actions or 'add' in actions): return False if 'replace' in actions and 'add' in actions: return False if ('delete' in actions) and ('none' in actions or 'replace' in actions): return False return True def extcommunity_action_compatibility(actions: dict) -> bool: """ Check compatibility of values in extended community sections :param actions: dictionary with community :type actions: dict :return: true if compatible, false if not :rtype: bool """ if ('none' in actions) and ( 'rt' in actions or 'soo' in actions or 'bandwidth' in actions or 'bandwidth_non_transitive' in actions): return False if ('bandwidth_non_transitive' in actions) and ('bandwidth' not in actions): return False return True def routing_policy_find(key, dictionary): # Recursively traverse a dictionary and extract the value assigned to # a given key as generator object. This is made for routing policies, # thus also import/export is checked for k, v in dictionary.items(): if k == key: if isinstance(v, dict): for a, b in v.items(): if a in ['import', 'export']: yield b else: yield v elif isinstance(v, dict): for result in routing_policy_find(key, v): yield result elif isinstance(v, list): for d in v: if isinstance(d, dict): for result in routing_policy_find(key, d): yield result def get_config(config=None): if config: conf = config else: conf = Config() return get_frrender_dict(conf) def verify(config_dict): if not has_frr_protocol_in_dict(config_dict, 'policy'): return None policy_types = ['access_list', 'access_list6', 'as_path_list', 'community_list', 'extcommunity_list', 'large_community_list', 'prefix_list', 'prefix_list6', 'route_map'] policy = config_dict['policy'] for protocol in frr_protocols: if protocol not in config_dict: continue if 'protocol' not in policy: policy.update({'protocol': {}}) policy['protocol'].update({protocol : config_dict[protocol]}) for policy_type in policy_types: # Bail out early and continue with next policy type if policy_type not in policy: continue # instance can be an ACL name/number, prefix-list name or route-map name for instance, instance_config in policy[policy_type].items(): # If no rule was found within the instance ... sad, but we can leave # early as nothing needs to be verified if 'rule' not in instance_config: continue # human readable instance name (hypen instead of underscore) policy_hr = policy_type.replace('_', '-') entries = [] for rule, rule_config in instance_config['rule'].items(): mandatory_error = f'must be specified for "{policy_hr} {instance} rule {rule}"!' if 'action' not in rule_config: raise ConfigError(f'Action {mandatory_error}') if policy_type == 'access_list': if 'source' not in rule_config: raise ConfigError(f'A source {mandatory_error}') if int(instance) in range(100, 200) or int( instance) in range(2000, 2700): if 'destination' not in rule_config: raise ConfigError( f'A destination {mandatory_error}') if policy_type == 'access_list6': if 'source' not in rule_config: raise ConfigError(f'A source {mandatory_error}') if policy_type in ['as_path_list', 'community_list', 'extcommunity_list', 'large_community_list']: if 'regex' not in rule_config: raise ConfigError(f'A regex {mandatory_error}') if policy_type in ['prefix_list', 'prefix_list6']: if 'prefix' not in rule_config: raise ConfigError(f'A prefix {mandatory_error}') if rule_config in entries: raise ConfigError( f'Rule "{rule}" contains a duplicate prefix definition!') entries.append(rule_config) # route-maps tend to be a bit more complex so they get their own verify() section if 'route_map' in policy: for route_map, route_map_config in policy['route_map'].items(): if 'rule' not in route_map_config: continue for rule, rule_config in route_map_config['rule'].items(): # Action 'deny' cannot be used with "continue" or "on-match" # FRR does not validate it T4827, T6676 if rule_config['action'] == 'deny' and ('continue' in rule_config or 'on_match' in rule_config): raise ConfigError(f'rule {rule} "continue" or "on-match" cannot be used with action deny!') # Specified community-list must exist tmp = dict_search('match.community.community_list', rule_config) if tmp and tmp not in policy.get('community_list', []): raise ConfigError(f'community-list {tmp} does not exist!') # Specified extended community-list must exist tmp = dict_search('match.extcommunity', rule_config) if tmp and tmp not in policy.get('extcommunity_list', []): raise ConfigError( f'extcommunity-list {tmp} does not exist!') # Specified large-community-list must exist tmp = dict_search('match.large_community.large_community_list', rule_config) if tmp and tmp not in policy.get('large_community_list', []): raise ConfigError( f'large-community-list {tmp} does not exist!') # Specified prefix-list must exist tmp = dict_search('match.ip.address.prefix_list', rule_config) if tmp and tmp not in policy.get('prefix_list', []): raise ConfigError(f'prefix-list {tmp} does not exist!') # Specified prefix-list must exist tmp = dict_search('match.ipv6.address.prefix_list', rule_config) if tmp and tmp not in policy.get('prefix_list6', []): raise ConfigError(f'prefix-list6 {tmp} does not exist!') # Specified access_list6 in nexthop must exist tmp = dict_search('match.ipv6.nexthop.access_list', rule_config) if tmp and tmp not in policy.get('access_list6', []): raise ConfigError(f'access_list6 {tmp} does not exist!') # Specified prefix-list6 in nexthop must exist tmp = dict_search('match.ipv6.nexthop.prefix_list', rule_config) if tmp and tmp not in policy.get('prefix_list6', []): raise ConfigError(f'prefix-list6 {tmp} does not exist!') tmp = dict_search('set.community.delete', rule_config) if tmp and tmp not in policy.get('community_list', []): raise ConfigError(f'community-list {tmp} does not exist!') tmp = dict_search('set.large_community.delete', rule_config) if tmp and tmp not in policy.get('large_community_list', []): raise ConfigError( f'large-community-list {tmp} does not exist!') if 'set' in rule_config: rule_action = rule_config['set'] if 'community' in rule_action: if not community_action_compatibility( rule_action['community']): raise ConfigError( f'Unexpected combination between action replace, add, delete or none in community') if 'large_community' in rule_action: if not community_action_compatibility( rule_action['large_community']): raise ConfigError( f'Unexpected combination between action replace, add, delete or none in large-community') if 'extcommunity' in rule_action: if not extcommunity_action_compatibility( rule_action['extcommunity']): raise ConfigError( f'Unexpected combination between none, rt, soo, bandwidth, bandwidth-non-transitive in extended-community') # When routing protocols are active some use prefix-lists, route-maps etc. # to apply the systems routing policy to the learned or redistributed routes. # When the "routing policy" changes and policies, route-maps etc. are deleted, # it is our responsibility to verify that the policy can not be deleted if it # is used by any routing protocol # Check if any routing protocol is activated if 'protocol' in policy: for policy_type in policy_types: for policy_name in list(set(routing_policy_find(policy_type, policy['protocol']))): found = False if policy_type in policy and policy_name in policy[policy_type]: found = True # BGP uses prefix-list for selecting both an IPv4 or IPv6 AFI related # list - we need to go the extra mile here and check both prefix-lists if policy_type == 'prefix_list' and 'prefix_list6' in policy and policy_name in \ policy['prefix_list6']: found = True if not found: tmp = policy_type.replace('_', '-') raise ConfigError( f'Can not delete {tmp} "{policy_name}", still in use!') return None def generate(config_dict): - if config_dict and 'frrender_cls' not in config_dict: + if config_dict and not is_systemd_service_running('vyos-configd.service'): FRRender().generate(config_dict) return None def apply(config_dict): - if config_dict and 'frrender_cls' not in config_dict: + if config_dict and not is_systemd_service_running('vyos-configd.service'): FRRender().apply() 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_babel.py b/src/conf_mode/protocols_babel.py index af0751e02..48b7ae734 100755 --- a/src/conf_mode/protocols_babel.py +++ b/src/conf_mode/protocols_babel.py @@ -1,109 +1,110 @@ #!/usr/bin/env python3 # # Copyright (C) 2021-2024 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/>. from sys import exit from vyos.config import Config from vyos.configdict import get_frrender_dict from vyos.configverify import has_frr_protocol_in_dict from vyos.configverify import verify_access_list from vyos.configverify import verify_prefix_list from vyos.frrender import FRRender from vyos.utils.dict import dict_search +from vyos.utils.process import is_systemd_service_running from vyos import ConfigError from vyos import airbag airbag.enable() def get_config(config=None): if config: conf = config else: conf = Config() return get_frrender_dict(conf) def verify(config_dict): if not has_frr_protocol_in_dict(config_dict, 'babel'): return None babel = config_dict['babel'] babel['policy'] = config_dict['policy'] # verify distribute_list if "distribute_list" in babel: acl_keys = { "ipv4": [ "distribute_list.ipv4.access_list.in", "distribute_list.ipv4.access_list.out", ], "ipv6": [ "distribute_list.ipv6.access_list.in", "distribute_list.ipv6.access_list.out", ] } prefix_list_keys = { "ipv4": [ "distribute_list.ipv4.prefix_list.in", "distribute_list.ipv4.prefix_list.out", ], "ipv6":[ "distribute_list.ipv6.prefix_list.in", "distribute_list.ipv6.prefix_list.out", ] } for address_family in ["ipv4", "ipv6"]: for iface_key in babel["distribute_list"].get(address_family, {}).get("interface", {}).keys(): acl_keys[address_family].extend([ f"distribute_list.{address_family}.interface.{iface_key}.access_list.in", f"distribute_list.{address_family}.interface.{iface_key}.access_list.out" ]) prefix_list_keys[address_family].extend([ f"distribute_list.{address_family}.interface.{iface_key}.prefix_list.in", f"distribute_list.{address_family}.interface.{iface_key}.prefix_list.out" ]) for address_family, keys in acl_keys.items(): for key in keys: acl = dict_search(key, babel) if acl: verify_access_list(acl, babel, version='6' if address_family == 'ipv6' else '') for address_family, keys in prefix_list_keys.items(): for key in keys: prefix_list = dict_search(key, babel) if prefix_list: verify_prefix_list(prefix_list, babel, version='6' if address_family == 'ipv6' else '') def generate(config_dict): - if config_dict and 'frrender_cls' not in config_dict: + if config_dict and not is_systemd_service_running('vyos-configd.service'): FRRender().generate(config_dict) return None def apply(config_dict): - if config_dict and 'frrender_cls' not in config_dict: + if config_dict and not is_systemd_service_running('vyos-configd.service'): FRRender().apply() 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_bfd.py b/src/conf_mode/protocols_bfd.py index 623801897..2e7d40676 100755 --- a/src/conf_mode/protocols_bfd.py +++ b/src/conf_mode/protocols_bfd.py @@ -1,96 +1,97 @@ #!/usr/bin/env python3 # # Copyright (C) 2019-2024 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/>. from vyos.config import Config from vyos.configdict import get_frrender_dict from vyos.configverify import verify_vrf from vyos.configverify import has_frr_protocol_in_dict from vyos.frrender import FRRender from vyos.template import is_ipv6 from vyos.utils.network import is_ipv6_link_local +from vyos.utils.process import is_systemd_service_running from vyos import ConfigError from vyos import airbag airbag.enable() def get_config(config=None): if config: conf = config else: conf = Config() return get_frrender_dict(conf) def verify(config_dict): if not has_frr_protocol_in_dict(config_dict, 'bfd'): return None bfd = config_dict['bfd'] if 'peer' in bfd: for peer, peer_config in bfd['peer'].items(): # IPv6 link local peers require an explicit local address/interface if is_ipv6_link_local(peer): if 'source' not in peer_config or len(peer_config['source']) < 2: raise ConfigError('BFD IPv6 link-local peers require explicit local address and interface setting') # IPv6 peers require an explicit local address if is_ipv6(peer): if 'source' not in peer_config or 'address' not in peer_config['source']: raise ConfigError('BFD IPv6 peers require explicit local address setting') if 'multihop' in peer_config: # multihop require source address if 'source' not in peer_config or 'address' not in peer_config['source']: raise ConfigError('BFD multihop require source address') # multihop and echo-mode cannot be used together if 'echo_mode' in peer_config: raise ConfigError('BFD multihop and echo-mode cannot be used together') # multihop doesn't accept interface names if 'source' in peer_config and 'interface' in peer_config['source']: raise ConfigError('BFD multihop and source interface cannot be used together') if 'minimum_ttl' in peer_config and 'multihop' not in peer_config: raise ConfigError('Minimum TTL is only available for multihop BFD sessions!') if 'profile' in peer_config: profile_name = peer_config['profile'] if 'profile' not in bfd or profile_name not in bfd['profile']: raise ConfigError(f'BFD profile "{profile_name}" does not exist!') if 'vrf' in peer_config: verify_vrf(peer_config) return None def generate(config_dict): - if config_dict and 'frrender_cls' not in config_dict: + if config_dict and not is_systemd_service_running('vyos-configd.service'): FRRender().generate(config_dict) def apply(config_dict): - if config_dict and 'frrender_cls' not in config_dict: + if config_dict and not is_systemd_service_running('vyos-configd.service'): FRRender().apply() 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_bgp.py b/src/conf_mode/protocols_bgp.py index db3123bd3..ae32dd839 100755 --- a/src/conf_mode/protocols_bgp.py +++ b/src/conf_mode/protocols_bgp.py @@ -1,574 +1,575 @@ #!/usr/bin/env python3 # # Copyright (C) 2020-2024 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/>. from sys import exit from sys import argv from vyos.base import Warning from vyos.config import Config from vyos.configdict import get_frrender_dict from vyos.configverify import has_frr_protocol_in_dict from vyos.configverify import verify_prefix_list from vyos.configverify import verify_route_map from vyos.configverify import verify_vrf from vyos.frrender import FRRender from vyos.template import is_ip from vyos.template import is_interface from vyos.utils.dict import dict_search from vyos.utils.network import get_interface_vrf from vyos.utils.network import is_addr_assigned +from vyos.utils.process import is_systemd_service_running from vyos.utils.process import process_named_running from vyos import ConfigError from vyos import airbag airbag.enable() def get_config(config=None): if config: conf = config else: conf = Config() return get_frrender_dict(conf, argv) def verify_vrf_as_import(search_vrf_name: str, afi_name: str, vrfs_config: dict) -> bool: """ :param search_vrf_name: search vrf name in import list :type search_vrf_name: str :param afi_name: afi/safi name :type afi_name: str :param vrfs_config: configuration dependents vrfs :type vrfs_config: dict :return: if vrf in import list retrun true else false :rtype: bool """ for vrf_name, vrf_config in vrfs_config.items(): import_list = dict_search( f'protocols.bgp.address_family.{afi_name}.import.vrf', vrf_config) if import_list: if search_vrf_name in import_list: return True return False def verify_vrf_import_options(afi_config: dict) -> bool: """ Search if afi contains one of options :param afi_config: afi/safi :type afi_config: dict :return: if vrf contains rd and route-target options return true else false :rtype: bool """ options = [ f'rd.vpn.export', f'route_target.vpn.import', f'route_target.vpn.export', f'route_target.vpn.both' ] for option in options: if dict_search(option, afi_config): return True return False def verify_vrf_import(vrf_name: str, vrfs_config: dict, afi_name: str) -> bool: """ Verify if vrf exists and contain options :param vrf_name: name of VRF :type vrf_name: str :param vrfs_config: dependent vrfs config :type vrfs_config: dict :param afi_name: afi/safi name :type afi_name: str :return: if vrf contains rd and route-target options return true else false :rtype: bool """ if vrf_name != 'default': verify_vrf({'vrf': vrf_name}) if dict_search(f'{vrf_name}.protocols.bgp.address_family.{afi_name}', vrfs_config): afi_config = \ vrfs_config[vrf_name]['protocols']['bgp']['address_family'][ afi_name] if verify_vrf_import_options(afi_config): return True return False def verify_vrflist_import(afi_name: str, afi_config: dict, vrfs_config: dict) -> bool: """ Call function to verify if scpecific vrf contains rd and route-target options return true else false :param afi_name: afi/safi name :type afi_name: str :param afi_config: afi/safi configuration :type afi_config: dict :param vrfs_config: dependent vrfs config :type vrfs_config:dict :return: if vrf contains rd and route-target options return true else false :rtype: bool """ for vrf_name in afi_config['import']['vrf']: if verify_vrf_import(vrf_name, vrfs_config, afi_name): return True return False def verify_remote_as(peer_config, bgp_config): if 'remote_as' in peer_config: return peer_config['remote_as'] if 'peer_group' in peer_config: peer_group_name = peer_config['peer_group'] tmp = dict_search(f'peer_group.{peer_group_name}.remote_as', bgp_config) if tmp: return tmp if 'interface' in peer_config: if 'remote_as' in peer_config['interface']: return peer_config['interface']['remote_as'] if 'peer_group' in peer_config['interface']: peer_group_name = peer_config['interface']['peer_group'] tmp = dict_search(f'peer_group.{peer_group_name}.remote_as', bgp_config) if tmp: return tmp if 'v6only' in peer_config['interface']: if 'remote_as' in peer_config['interface']['v6only']: return peer_config['interface']['v6only']['remote_as'] if 'peer_group' in peer_config['interface']['v6only']: peer_group_name = peer_config['interface']['v6only']['peer_group'] tmp = dict_search(f'peer_group.{peer_group_name}.remote_as', bgp_config) if tmp: return tmp return None def verify_afi(peer_config, bgp_config): # If address_family configured under neighboor if 'address_family' in peer_config: return True # If address_family configured under peer-group # if neighbor interface configured peer_group_name = None if dict_search('interface.peer_group', peer_config): peer_group_name = peer_config['interface']['peer_group'] elif dict_search('interface.v6only.peer_group', peer_config): peer_group_name = peer_config['interface']['v6only']['peer_group'] # if neighbor IP configured. if 'peer_group' in peer_config: peer_group_name = peer_config['peer_group'] if peer_group_name: tmp = dict_search(f'peer_group.{peer_group_name}.address_family', bgp_config) if tmp: return True return False def verify(config_dict): print('====== verify() ======') import pprint pprint.pprint(config_dict) if not has_frr_protocol_in_dict(config_dict, 'bgp'): return None vrf = None if 'vrf_context' in config_dict: vrf = config_dict['vrf_context'] # eqivalent of the C foo ? 'a' : 'b' statement bgp = vrf and config_dict['vrf']['name'][vrf]['protocols']['bgp'] or config_dict['bgp'] bgp['policy'] = config_dict['policy'] if 'deleted' in bgp: if vrf: # Cannot delete vrf if it exists in import vrf list in other vrfs for tmp_afi in ['ipv4_unicast', 'ipv6_unicast']: if verify_vrf_as_import(vrf, tmp_afi, bgp['dependent_vrfs']): raise ConfigError(f'Cannot delete VRF instance "{vrf}", ' \ 'unconfigure "import vrf" commands!') else: # We are running in the default VRF context, thus we can not delete # our main BGP instance if there are dependent BGP VRF instances. if 'dependent_vrfs' in bgp: for vrf, vrf_options in bgp['dependent_vrfs'].items(): if vrf != 'default': if dict_search('protocols.bgp', vrf_options): dependent_vrfs = ', '.join(bgp['dependent_vrfs'].keys()) raise ConfigError(f'Cannot delete default BGP instance, ' \ f'dependent VRF instance(s): {dependent_vrfs}') if 'vni' in vrf_options: raise ConfigError('Cannot delete default BGP instance, ' \ 'dependent L3VNI exists!') return None if 'system_as' not in bgp: raise ConfigError('BGP system-as number must be defined!') # Verify BMP if 'bmp' in bgp: # check bmp flag "bgpd -d -F traditional --daemon -A 127.0.0.1 -M rpki -M bmp" if not process_named_running('bgpd', 'bmp'): raise ConfigError( f'"bmp" flag is not found in bgpd. Configure "set system frr bmp" and restart bgp process' ) # check bmp target if 'target' in bgp['bmp']: for target, target_config in bgp['bmp']['target'].items(): if 'address' not in target_config: raise ConfigError(f'BMP target "{target}" address must be defined!') # Verify vrf on interface and bgp section if 'interface' in bgp: for interface in bgp['interface']: error_msg = f'Interface "{interface}" belongs to different VRF instance' tmp = get_interface_vrf(interface) if vrf: if vrf != tmp: raise ConfigError(f'{error_msg} "{vrf}"!') elif tmp != 'default': raise ConfigError(f'{error_msg} "{tmp}"!') peer_groups_context = dict() # Common verification for both peer-group and neighbor statements for neighbor in ['neighbor', 'peer_group']: # bail out early if there is no neighbor or peer-group statement # this also saves one indention level if neighbor not in bgp: continue for peer, peer_config in bgp[neighbor].items(): # Only regular "neighbor" statement can have a peer-group set # Check if the configure peer-group exists if 'peer_group' in peer_config: peer_group = peer_config['peer_group'] if 'peer_group' not in bgp or peer_group not in bgp['peer_group']: raise ConfigError(f'Specified peer-group "{peer_group}" for '\ f'neighbor "{neighbor}" does not exist!') if 'remote_as' in peer_config: is_ibgp = True if peer_config['remote_as'] != 'internal' and \ peer_config['remote_as'] != bgp['system_as']: is_ibgp = False if peer_group not in peer_groups_context: peer_groups_context[peer_group] = is_ibgp elif peer_groups_context[peer_group] != is_ibgp: raise ConfigError(f'Peer-group members must be ' f'all internal or all external') if 'local_role' in peer_config: #Ensure Local Role has only one value. if len(peer_config['local_role']) > 1: raise ConfigError(f'Only one local role can be specified for peer "{peer}"!') if 'local_as' in peer_config: if len(peer_config['local_as']) > 1: raise ConfigError(f'Only one local-as number can be specified for peer "{peer}"!') # Neighbor local-as override can not be the same as the local-as # we use for this BGP instane! asn = list(peer_config['local_as'].keys())[0] if asn == bgp['system_as']: raise ConfigError('Cannot have local-as same as system-as number') # Neighbor AS specified for local-as and remote-as can not be the same if dict_search('remote_as', peer_config) == asn and neighbor != 'peer_group': raise ConfigError(f'Neighbor "{peer}" has local-as specified which is '\ 'the same as remote-as, this is not allowed!') # ttl-security and ebgp-multihop can't be used in the same configration if 'ebgp_multihop' in peer_config and 'ttl_security' in peer_config: raise ConfigError('You can not set both ebgp-multihop and ttl-security hops') # interface and ebgp-multihop can't be used in the same configration if 'ebgp_multihop' in peer_config and 'interface' in peer_config: raise ConfigError(f'Ebgp-multihop can not be used with directly connected '\ f'neighbor "{peer}"') # Check if neighbor has both override capability and strict capability match # configured at the same time. if 'override_capability' in peer_config and 'strict_capability_match' in peer_config: raise ConfigError(f'Neighbor "{peer}" cannot have both override-capability and '\ 'strict-capability-match configured at the same time!') # Check spaces in the password if 'password' in peer_config and ' ' in peer_config['password']: raise ConfigError('Whitespace is not allowed in passwords!') # Some checks can/must only be done on a neighbor and not a peer-group if neighbor == 'neighbor': # remote-as must be either set explicitly for the neighbor # or for the entire peer-group if not verify_remote_as(peer_config, bgp): raise ConfigError(f'Neighbor "{peer}" remote-as must be set!') if not verify_afi(peer_config, bgp): Warning(f'BGP neighbor "{peer}" requires address-family!') # Peer-group member cannot override remote-as of peer-group if 'peer_group' in peer_config: peer_group = peer_config['peer_group'] if 'remote_as' in peer_config and 'remote_as' in bgp['peer_group'][peer_group]: raise ConfigError(f'Peer-group member "{peer}" cannot override remote-as of peer-group "{peer_group}"!') if 'interface' in peer_config: if 'peer_group' in peer_config['interface']: peer_group = peer_config['interface']['peer_group'] if 'remote_as' in peer_config['interface'] and 'remote_as' in bgp['peer_group'][peer_group]: raise ConfigError(f'Peer-group member "{peer}" cannot override remote-as of peer-group "{peer_group}"!') if 'v6only' in peer_config['interface']: if 'peer_group' in peer_config['interface']['v6only']: peer_group = peer_config['interface']['v6only']['peer_group'] if 'remote_as' in peer_config['interface']['v6only'] and 'remote_as' in bgp['peer_group'][peer_group]: raise ConfigError(f'Peer-group member "{peer}" cannot override remote-as of peer-group "{peer_group}"!') # Only checks for ipv4 and ipv6 neighbors # Check if neighbor address is assigned as system interface address vrf_error_msg = f' in default VRF!' if vrf: vrf_error_msg = f' in VRF "{vrf}"!' if is_ip(peer) and is_addr_assigned(peer, vrf): raise ConfigError(f'Can not configure local address as neighbor "{peer}"{vrf_error_msg}') elif is_interface(peer): if 'peer_group' in peer_config: raise ConfigError(f'peer-group must be set under the interface node of "{peer}"') if 'remote_as' in peer_config: raise ConfigError(f'remote-as must be set under the interface node of "{peer}"') if 'source_interface' in peer_config['interface']: raise ConfigError(f'"source-interface" option not allowed for neighbor "{peer}"') # Local-AS allowed only for EBGP peers if 'local_as' in peer_config: remote_as = verify_remote_as(peer_config, bgp) if remote_as == bgp['system_as']: raise ConfigError(f'local-as configured for "{peer}", allowed only for eBGP peers!') for afi in ['ipv4_unicast', 'ipv4_multicast', 'ipv4_labeled_unicast', 'ipv4_flowspec', 'ipv6_unicast', 'ipv6_multicast', 'ipv6_labeled_unicast', 'ipv6_flowspec', 'l2vpn_evpn']: # Bail out early if address family is not configured if 'address_family' not in peer_config or afi not in peer_config['address_family']: continue # Check if neighbor has both ipv4 unicast and ipv4 labeled unicast configured at the same time. if 'ipv4_unicast' in peer_config['address_family'] and 'ipv4_labeled_unicast' in peer_config['address_family']: raise ConfigError(f'Neighbor "{peer}" cannot have both ipv4-unicast and ipv4-labeled-unicast configured at the same time!') # Check if neighbor has both ipv6 unicast and ipv6 labeled unicast configured at the same time. if 'ipv6_unicast' in peer_config['address_family'] and 'ipv6_labeled_unicast' in peer_config['address_family']: raise ConfigError(f'Neighbor "{peer}" cannot have both ipv6-unicast and ipv6-labeled-unicast configured at the same time!') afi_config = peer_config['address_family'][afi] if 'conditionally_advertise' in afi_config: if 'advertise_map' not in afi_config['conditionally_advertise']: raise ConfigError('Must speficy advertise-map when conditionally-advertise is in use!') # Verify advertise-map (which is a route-map) exists verify_route_map(afi_config['conditionally_advertise']['advertise_map'], bgp) if ('exist_map' not in afi_config['conditionally_advertise'] and 'non_exist_map' not in afi_config['conditionally_advertise']): raise ConfigError('Must either speficy exist-map or non-exist-map when ' \ 'conditionally-advertise is in use!') if {'exist_map', 'non_exist_map'} <= set(afi_config['conditionally_advertise']): raise ConfigError('Can not specify both exist-map and non-exist-map for ' \ 'conditionally-advertise!') if 'exist_map' in afi_config['conditionally_advertise']: verify_route_map(afi_config['conditionally_advertise']['exist_map'], bgp) if 'non_exist_map' in afi_config['conditionally_advertise']: verify_route_map(afi_config['conditionally_advertise']['non_exist_map'], bgp) # T4332: bgp deterministic-med cannot be disabled while addpath-tx-bestpath-per-AS is in use if 'addpath_tx_per_as' in afi_config: if dict_search('parameters.deterministic_med', bgp) == None: raise ConfigError('addpath-tx-per-as requires BGP deterministic-med paramtere to be set!') # Validate if configured Prefix list exists if 'prefix_list' in afi_config: for tmp in ['import', 'export']: if tmp not in afi_config['prefix_list']: # bail out early continue if afi == 'ipv4_unicast': verify_prefix_list(afi_config['prefix_list'][tmp], bgp) elif afi == 'ipv6_unicast': verify_prefix_list(afi_config['prefix_list'][tmp], bgp, version='6') if 'route_map' in afi_config: for tmp in ['import', 'export']: if tmp in afi_config['route_map']: verify_route_map(afi_config['route_map'][tmp], bgp) if 'route_reflector_client' in afi_config: peer_group_as = peer_config.get('remote_as') if peer_group_as is None or (peer_group_as != 'internal' and peer_group_as != bgp['system_as']): raise ConfigError('route-reflector-client only supported for iBGP peers') else: if 'peer_group' in peer_config: peer_group_as = dict_search(f'peer_group.{peer_group}.remote_as', bgp) if peer_group_as is None or (peer_group_as != 'internal' and peer_group_as != bgp['system_as']): raise ConfigError('route-reflector-client only supported for iBGP peers') # T5833 not all AFIs are supported for VRF if 'vrf' in bgp and 'address_family' in peer_config: unsupported_vrf_afi = { 'ipv4_flowspec', 'ipv6_flowspec', 'ipv4_labeled_unicast', 'ipv6_labeled_unicast', 'ipv4_vpn', 'ipv6_vpn', } for afi in peer_config['address_family']: if afi in unsupported_vrf_afi: raise ConfigError( f"VRF is not allowed for address-family '{afi.replace('_', '-')}'" ) # Throw an error if a peer group is not configured for allow range for prefix in dict_search('listen.range', bgp) or []: # we can not use dict_search() here as prefix contains dots ... if 'peer_group' not in bgp['listen']['range'][prefix]: raise ConfigError(f'Listen range for prefix "{prefix}" has no peer group configured.') peer_group = bgp['listen']['range'][prefix]['peer_group'] if 'peer_group' not in bgp or peer_group not in bgp['peer_group']: raise ConfigError(f'Peer-group "{peer_group}" for listen range "{prefix}" does not exist!') if not verify_remote_as(bgp['listen']['range'][prefix], bgp): raise ConfigError(f'Peer-group "{peer_group}" requires remote-as to be set!') # Throw an error if the global administrative distance parameters aren't all filled out. if dict_search('parameters.distance.global', bgp) != None: for key in ['external', 'internal', 'local']: if dict_search(f'parameters.distance.global.{key}', bgp) == None: raise ConfigError('Missing mandatory configuration option for '\ f'global administrative distance {key}!') # TCP keepalive requires all three parameters to be set if dict_search('parameters.tcp_keepalive', bgp) != None: if not {'idle', 'interval', 'probes'} <= set(bgp['parameters']['tcp_keepalive']): raise ConfigError('TCP keepalive incomplete - idle, keepalive and probes must be set') # Address Family specific validation if 'address_family' in bgp: for afi, afi_config in bgp['address_family'].items(): if 'distance' in afi_config: # Throw an error if the address family specific administrative # distance parameters aren't all filled out. for key in ['external', 'internal', 'local']: if key not in afi_config['distance']: raise ConfigError('Missing mandatory configuration option for '\ f'{afi} administrative distance {key}!') if afi in ['ipv4_unicast', 'ipv6_unicast']: vrf_name = vrf if vrf else 'default' # Verify if currant VRF contains rd and route-target options # and does not exist in import list in other VRFs if dict_search(f'rd.vpn.export', afi_config): if verify_vrf_as_import(vrf_name, afi, bgp['dependent_vrfs']): raise ConfigError( 'Command "import vrf" conflicts with "rd vpn export" command!') if not dict_search('parameters.router_id', bgp): Warning(f'BGP "router-id" is required when using "rd" and "route-target"!') if dict_search('route_target.vpn.both', afi_config): if verify_vrf_as_import(vrf_name, afi, bgp['dependent_vrfs']): raise ConfigError( 'Command "import vrf" conflicts with "route-target vpn both" command!') if dict_search('route_target.vpn.export', afi_config): raise ConfigError( 'Command "route-target vpn export" conflicts '\ 'with "route-target vpn both" command!') if dict_search('route_target.vpn.import', afi_config): raise ConfigError( 'Command "route-target vpn import" conflicts '\ 'with "route-target vpn both" command!') if dict_search('route_target.vpn.import', afi_config): if verify_vrf_as_import(vrf_name, afi, bgp['dependent_vrfs']): raise ConfigError( 'Command "import vrf conflicts" with "route-target vpn import" command!') if dict_search('route_target.vpn.export', afi_config): if verify_vrf_as_import(vrf_name, afi, bgp['dependent_vrfs']): raise ConfigError( 'Command "import vrf" conflicts with "route-target vpn export" command!') # Verify if VRFs in import do not contain rd # and route-target options if dict_search('import.vrf', afi_config) is not None: # Verify if VRF with import does not contain rd # and route-target options if verify_vrf_import_options(afi_config): raise ConfigError( 'Please unconfigure "import vrf" commands before using vpn commands in the same VRF!') # Verify if VRFs in import list do not contain rd # and route-target options if verify_vrflist_import(afi, afi_config, bgp['dependent_vrfs']): raise ConfigError( 'Please unconfigure import vrf commands before using vpn commands in dependent VRFs!') # FRR error: please unconfigure vpn to vrf commands before # using import vrf commands if 'vpn' in afi_config['import'] or dict_search('export.vpn', afi_config) != None: raise ConfigError('Please unconfigure VPN to VRF commands before '\ 'using "import vrf" commands!') # Verify that the export/import route-maps do exist for export_import in ['export', 'import']: tmp = dict_search(f'route_map.vpn.{export_import}', afi_config) if tmp: verify_route_map(tmp, bgp) # per-vrf sid and per-af sid are mutually exclusive if 'sid' in afi_config and 'sid' in bgp: raise ConfigError('SID per VRF and SID per address-family are mutually exclusive!') # Checks only required for L2VPN EVPN if afi in ['l2vpn_evpn']: if 'vni' in afi_config: for vni, vni_config in afi_config['vni'].items(): if 'rd' in vni_config and 'advertise_all_vni' not in afi_config: raise ConfigError('BGP EVPN "rd" requires "advertise-all-vni" to be set!') if 'route_target' in vni_config and 'advertise_all_vni' not in afi_config: raise ConfigError('BGP EVPN "route-target" requires "advertise-all-vni" to be set!') return None def generate(config_dict): - if config_dict and 'frrender_cls' not in config_dict: + if config_dict and not is_systemd_service_running('vyos-configd.service'): FRRender().generate(config_dict) return None def apply(config_dict): - if config_dict and 'frrender_cls' not in config_dict: + if config_dict and not is_systemd_service_running('vyos-configd.service'): FRRender().apply() 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_eigrp.py b/src/conf_mode/protocols_eigrp.py index 33812d350..8f49bb151 100755 --- a/src/conf_mode/protocols_eigrp.py +++ b/src/conf_mode/protocols_eigrp.py @@ -1,73 +1,74 @@ #!/usr/bin/env python3 # # Copyright (C) 2022-2024 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/>. from sys import exit from sys import argv from vyos.config import Config from vyos.configdict import get_frrender_dict from vyos.configverify import has_frr_protocol_in_dict from vyos.configverify import verify_vrf +from vyos.utils.process import is_systemd_service_running from vyos.frrender import FRRender from vyos import ConfigError from vyos import airbag airbag.enable() def get_config(config=None): if config: conf = config else: conf = Config() return get_frrender_dict(conf, argv) def verify(config_dict): if not has_frr_protocol_in_dict(config_dict, 'eigrp'): return None vrf = None if 'vrf_context' in config_dict: vrf = config_dict['vrf_context'] # eqivalent of the C foo ? 'a' : 'b' statement eigrp = vrf and config_dict['vrf']['name'][vrf]['protocols']['eigrp'] or config_dict['eigrp'] eigrp['policy'] = config_dict['policy'] if 'system_as' not in eigrp: raise ConfigError('EIGRP system-as must be defined!') if vrf: verify_vrf({'vrf': vrf}) def generate(config_dict): - if config_dict and 'frrender_cls' not in config_dict: + if config_dict and not is_systemd_service_running('vyos-configd.service'): FRRender().generate(config_dict) return None def apply(config_dict): - if config_dict and 'frrender_cls' not in config_dict: + if config_dict and not is_systemd_service_running('vyos-configd.service'): FRRender().apply() 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_isis.py b/src/conf_mode/protocols_isis.py index 7a54685bb..1e5f0d6e8 100755 --- a/src/conf_mode/protocols_isis.py +++ b/src/conf_mode/protocols_isis.py @@ -1,252 +1,253 @@ #!/usr/bin/env python3 # # Copyright (C) 2020-2024 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/>. from sys import exit from sys import argv from vyos.config import Config from vyos.configdict import get_frrender_dict from vyos.configverify import has_frr_protocol_in_dict from vyos.configverify import verify_common_route_maps from vyos.configverify import verify_interface_exists from vyos.frrender import FRRender from vyos.ifconfig import Interface from vyos.utils.dict import dict_search from vyos.utils.network import get_interface_config +from vyos.utils.process import is_systemd_service_running from vyos import ConfigError from vyos import airbag airbag.enable() def get_config(config=None): if config: conf = config else: conf = Config() return get_frrender_dict(conf, argv) def verify(config_dict): if not has_frr_protocol_in_dict(config_dict, 'isis'): return None vrf = None if 'vrf_context' in config_dict: vrf = config_dict['vrf_context'] # eqivalent of the C foo ? 'a' : 'b' statement isis = vrf and config_dict['vrf']['name'][vrf]['protocols']['isis'] or config_dict['isis'] isis['policy'] = config_dict['policy'] if 'deleted' in isis: return None if 'net' not in isis: raise ConfigError('Network entity is mandatory!') # last byte in IS-IS area address must be 0 tmp = isis['net'].split('.') if int(tmp[-1]) != 0: raise ConfigError('Last byte of IS-IS network entity title must always be 0!') verify_common_route_maps(isis) # If interface not set if 'interface' not in isis: raise ConfigError('Interface used for routing updates is mandatory!') for interface in isis['interface']: verify_interface_exists(isis, interface) # Interface MTU must be >= configured lsp-mtu mtu = Interface(interface).get_mtu() area_mtu = isis['lsp_mtu'] # Recommended maximum PDU size = interface MTU - 3 bytes recom_area_mtu = mtu - 3 if mtu < int(area_mtu) or int(area_mtu) > recom_area_mtu: raise ConfigError(f'Interface {interface} has MTU {mtu}, ' \ f'current area MTU is {area_mtu}! \n' \ f'Recommended area lsp-mtu {recom_area_mtu} or less ' \ '(calculated on MTU size).') if vrf: # If interface specific options are set, we must ensure that the # interface is bound to our requesting VRF. Due to the VyOS # priorities the interface is bound to the VRF after creation of # the VRF itself, and before any routing protocol is configured. tmp = get_interface_config(interface) if 'master' not in tmp or tmp['master'] != vrf: raise ConfigError(f'Interface "{interface}" is not a member of VRF "{vrf}"!') # If md5 and plaintext-password set at the same time for password in ['area_password', 'domain_password']: if password in isis: if {'md5', 'plaintext_password'} <= set(isis[password]): tmp = password.replace('_', '-') raise ConfigError(f'Can use either md5 or plaintext-password for {tmp}!') # If one param from delay set, but not set others if 'spf_delay_ietf' in isis: required_timers = ['holddown', 'init_delay', 'long_delay', 'short_delay', 'time_to_learn'] exist_timers = [] for elm_timer in required_timers: if elm_timer in isis['spf_delay_ietf']: exist_timers.append(elm_timer) exist_timers = set(required_timers).difference(set(exist_timers)) if len(exist_timers) > 0: raise ConfigError('All types of spf-delay must be configured. Missing: ' + ', '.join(exist_timers).replace('_', '-')) # If Redistribute set, but level don't set if 'redistribute' in isis: proc_level = isis.get('level','').replace('-','_') for afi in ['ipv4', 'ipv6']: if afi not in isis['redistribute']: continue for proto, proto_config in isis['redistribute'][afi].items(): if 'level_1' not in proto_config and 'level_2' not in proto_config: raise ConfigError(f'Redistribute level-1 or level-2 should be specified in ' \ f'"protocols isis redistribute {afi} {proto}"!') for redistr_level, redistr_config in proto_config.items(): if proc_level and proc_level != 'level_1_2' and proc_level != redistr_level: raise ConfigError(f'"protocols isis redistribute {afi} {proto} {redistr_level}" ' \ f'can not be used with \"protocols isis level {proc_level}\"!') # Segment routing checks if dict_search('segment_routing.global_block', isis): g_high_label_value = dict_search('segment_routing.global_block.high_label_value', isis) g_low_label_value = dict_search('segment_routing.global_block.low_label_value', isis) # If segment routing global block high or low value is blank, throw error if not (g_low_label_value or g_high_label_value): raise ConfigError('Segment routing global-block requires both low and high value!') # If segment routing global block low value is higher than the high value, throw error if int(g_low_label_value) > int(g_high_label_value): raise ConfigError('Segment routing global-block low value must be lower than high value') if dict_search('segment_routing.local_block', isis): if dict_search('segment_routing.global_block', isis) == None: raise ConfigError('Segment routing local-block requires global-block to be configured!') l_high_label_value = dict_search('segment_routing.local_block.high_label_value', isis) l_low_label_value = dict_search('segment_routing.local_block.low_label_value', isis) # If segment routing local-block high or low value is blank, throw error if not (l_low_label_value or l_high_label_value): raise ConfigError('Segment routing local-block requires both high and low value!') # If segment routing local-block low value is higher than the high value, throw error if int(l_low_label_value) > int(l_high_label_value): raise ConfigError('Segment routing local-block low value must be lower than high value') # local-block most live outside global block global_range = range(int(g_low_label_value), int(g_high_label_value) +1) local_range = range(int(l_low_label_value), int(l_high_label_value) +1) # Check for overlapping ranges if list(set(global_range) & set(local_range)): raise ConfigError(f'Segment-Routing Global Block ({g_low_label_value}/{g_high_label_value}) '\ f'conflicts with Local Block ({l_low_label_value}/{l_high_label_value})!') # Check for a blank or invalid value per prefix if dict_search('segment_routing.prefix', isis): for prefix, prefix_config in isis['segment_routing']['prefix'].items(): if 'absolute' in prefix_config: if prefix_config['absolute'].get('value') is None: raise ConfigError(f'Segment routing prefix {prefix} absolute value cannot be blank.') elif 'index' in prefix_config: if prefix_config['index'].get('value') is None: raise ConfigError(f'Segment routing prefix {prefix} index value cannot be blank.') # Check for explicit-null and no-php-flag configured at the same time per prefix if dict_search('segment_routing.prefix', isis): for prefix, prefix_config in isis['segment_routing']['prefix'].items(): if 'absolute' in prefix_config: if ("explicit_null" in prefix_config['absolute']) and ("no_php_flag" in prefix_config['absolute']): raise ConfigError(f'Segment routing prefix {prefix} cannot have both explicit-null '\ f'and no-php-flag configured at the same time.') elif 'index' in prefix_config: if ("explicit_null" in prefix_config['index']) and ("no_php_flag" in prefix_config['index']): raise ConfigError(f'Segment routing prefix {prefix} cannot have both explicit-null '\ f'and no-php-flag configured at the same time.') # Check for index ranges being larger than the segment routing global block if dict_search('segment_routing.global_block', isis): g_high_label_value = dict_search('segment_routing.global_block.high_label_value', isis) g_low_label_value = dict_search('segment_routing.global_block.low_label_value', isis) g_label_difference = int(g_high_label_value) - int(g_low_label_value) if dict_search('segment_routing.prefix', isis): for prefix, prefix_config in isis['segment_routing']['prefix'].items(): if 'index' in prefix_config: index_size = isis['segment_routing']['prefix'][prefix]['index']['value'] if int(index_size) > int(g_label_difference): raise ConfigError(f'Segment routing prefix {prefix} cannot have an '\ f'index base size larger than the SRGB label base.') # Check for LFA tiebreaker index duplication if dict_search('fast_reroute.lfa.local.tiebreaker', isis): comparison_dictionary = {} for item, item_options in isis['fast_reroute']['lfa']['local']['tiebreaker'].items(): for index, index_options in item_options.items(): for index_value, index_value_options in index_options.items(): if index_value not in comparison_dictionary.keys(): comparison_dictionary[index_value] = [item] else: comparison_dictionary[index_value].append(item) for index, index_length in comparison_dictionary.items(): if int(len(index_length)) > 1: raise ConfigError(f'LFA index {index} cannot have more than one tiebreaker configured.') # Check for LFA priority-limit configured multiple times per level if dict_search('fast_reroute.lfa.local.priority_limit', isis): comparison_dictionary = {} for priority, priority_options in isis['fast_reroute']['lfa']['local']['priority_limit'].items(): for level, level_options in priority_options.items(): if level not in comparison_dictionary.keys(): comparison_dictionary[level] = [priority] else: comparison_dictionary[level].append(priority) for level, level_length in comparison_dictionary.items(): if int(len(level_length)) > 1: raise ConfigError(f'LFA priority-limit on {level.replace("_", "-")} cannot have more than one priority configured.') # Check for LFA remote prefix list configured with more than one list if dict_search('fast_reroute.lfa.remote.prefix_list', isis): if int(len(isis['fast_reroute']['lfa']['remote']['prefix_list'].items())) > 1: raise ConfigError(f'LFA remote prefix-list has more than one configured. Cannot have more than one configured.') return None def generate(config_dict): - if config_dict and 'frrender_cls' not in config_dict: + if config_dict and not is_systemd_service_running('vyos-configd.service'): FRRender().generate(config_dict) return None def apply(config_dict): - if config_dict and 'frrender_cls' not in config_dict: + if config_dict and not is_systemd_service_running('vyos-configd.service'): FRRender().apply() 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_mpls.py b/src/conf_mode/protocols_mpls.py index bab9648c4..e8097b7ff 100755 --- a/src/conf_mode/protocols_mpls.py +++ b/src/conf_mode/protocols_mpls.py @@ -1,139 +1,140 @@ #!/usr/bin/env python3 # # Copyright (C) 2020-2024 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 glob import glob from vyos.config import Config from vyos.configdict import get_frrender_dict from vyos.configverify import has_frr_protocol_in_dict from vyos.frrender import FRRender from vyos.utils.dict import dict_search from vyos.utils.file import read_file +from vyos.utils.process import is_systemd_service_running from vyos.utils.system import sysctl_write from vyos.configverify import verify_interface_exists from vyos import ConfigError from vyos import airbag airbag.enable() def get_config(config=None): if config: conf = config else: conf = Config() return get_frrender_dict(conf) def verify(config_dict): if not has_frr_protocol_in_dict(config_dict, 'mpls'): return None mpls = config_dict['mpls'] if 'interface' in mpls: for interface in mpls['interface']: verify_interface_exists(mpls, interface) # Checks to see if LDP is properly configured if 'ldp' in mpls: # If router ID not defined if 'router_id' not in mpls['ldp']: raise ConfigError('Router ID missing. An LDP router id is mandatory!') # If interface not set if 'interface' not in mpls['ldp']: raise ConfigError('LDP interfaces are missing. An LDP interface is mandatory!') # If transport addresses are not set if not dict_search('ldp.discovery.transport_ipv4_address', mpls) and \ not dict_search('ldp.discovery.transport_ipv6_address', mpls): raise ConfigError('LDP transport address missing!') return None def generate(config_dict): - if config_dict and 'frrender_cls' not in config_dict: + if config_dict and not is_systemd_service_running('vyos-configd.service'): FRRender().generate(config_dict) return None def apply(config_dict): - if config_dict and 'frrender_cls' not in config_dict: + if config_dict and not is_systemd_service_running('vyos-configd.service'): FRRender().apply() if not has_frr_protocol_in_dict(config_dict, 'mpls'): return None mpls = config_dict['mpls'] # Set number of entries in the platform label tables labels = '0' if 'interface' in mpls: labels = '1048575' sysctl_write('net.mpls.platform_labels', labels) # Check for changes in global MPLS options if 'parameters' in mpls: # Choose whether to copy IP TTL to MPLS header TTL if 'no_propagate_ttl' in mpls['parameters']: sysctl_write('net.mpls.ip_ttl_propagate', 0) # Choose whether to limit maximum MPLS header TTL if 'maximum_ttl' in mpls['parameters']: ttl = mpls['parameters']['maximum_ttl'] sysctl_write('net.mpls.default_ttl', ttl) else: # Set default global MPLS options if not defined. sysctl_write('net.mpls.ip_ttl_propagate', 1) sysctl_write('net.mpls.default_ttl', 255) # Enable and disable MPLS processing on interfaces per configuration if 'interface' in mpls: system_interfaces = [] # Populate system interfaces list with local MPLS capable interfaces for interface in glob('/proc/sys/net/mpls/conf/*'): system_interfaces.append(os.path.basename(interface)) # This is where the comparison is done on if an interface needs to be enabled/disabled. for system_interface in system_interfaces: interface_state = read_file(f'/proc/sys/net/mpls/conf/{system_interface}/input') if '1' in interface_state: if system_interface not in mpls['interface']: system_interface = system_interface.replace('.', '/') sysctl_write(f'net.mpls.conf.{system_interface}.input', 0) elif '0' in interface_state: if system_interface in mpls['interface']: system_interface = system_interface.replace('.', '/') sysctl_write(f'net.mpls.conf.{system_interface}.input', 1) else: system_interfaces = [] # If MPLS interfaces are not configured, set MPLS processing disabled for interface in glob('/proc/sys/net/mpls/conf/*'): system_interfaces.append(os.path.basename(interface)) for system_interface in system_interfaces: system_interface = system_interface.replace('.', '/') sysctl_write(f'net.mpls.conf.{system_interface}.input', 0) 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_openfabric.py b/src/conf_mode/protocols_openfabric.py index 48c89d742..41c5d9544 100644 --- a/src/conf_mode/protocols_openfabric.py +++ b/src/conf_mode/protocols_openfabric.py @@ -1,109 +1,110 @@ #!/usr/bin/env python3 # # Copyright (C) 2024 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/>. from sys import exit from vyos.base import Warning from vyos.config import Config from vyos.configdict import get_frrender_dict from vyos.configverify import verify_interface_exists from vyos.configverify import has_frr_protocol_in_dict +from vyos.utils.process import is_systemd_service_running from vyos.frrender import FRRender from vyos import ConfigError from vyos import airbag airbag.enable() def get_config(config=None): if config: conf = config else: conf = Config() return get_frrender_dict(conf) def verify(config_dict): if not has_frr_protocol_in_dict(config_dict, 'openfabric'): return None openfabric = config_dict['openfabric'] if 'deleted' in openfabric: return None if 'net' not in openfabric: raise ConfigError('Network entity is mandatory!') # last byte in OpenFabric area address must be 0 tmp = openfabric['net'].split('.') if int(tmp[-1]) != 0: raise ConfigError('Last byte of OpenFabric network entity title must always be 0!') if 'domain' not in openfabric: raise ConfigError('OpenFabric domain name is mandatory!') interfaces_used = [] for domain, domain_config in openfabric['domain'].items(): # If interface not set if 'interface' not in domain_config: raise ConfigError(f'Interface used for routing updates in OpenFabric "{domain}" is mandatory!') for iface, iface_config in domain_config['interface'].items(): verify_interface_exists(openfabric, iface) # interface can be activated only on one OpenFabric instance if iface in interfaces_used: raise ConfigError(f'Interface {iface} is already used in different OpenFabric instance!') if 'address_family' not in iface_config or len(iface_config['address_family']) < 1: raise ConfigError(f'Need to specify address family for the interface "{iface}"!') # If md5 and plaintext-password set at the same time if 'password' in iface_config: if {'md5', 'plaintext_password'} <= set(iface_config['password']): raise ConfigError(f'Can use either md5 or plaintext-password for password for the interface!') if iface == 'lo' and 'passive' not in iface_config: Warning('For loopback interface passive mode is implied!') interfaces_used.append(iface) # If md5 and plaintext-password set at the same time password = 'domain_password' if password in domain_config: if {'md5', 'plaintext_password'} <= set(domain_config[password]): raise ConfigError(f'Can use either md5 or plaintext-password for domain-password!') return None def generate(config_dict): - if config_dict and 'frrender_cls' not in config_dict: + if config_dict and not is_systemd_service_running('vyos-configd.service'): FRRender().generate(config_dict) return None def apply(config_dict): - if config_dict and 'frrender_cls' not in config_dict: + if config_dict and not is_systemd_service_running('vyos-configd.service'): FRRender().apply() 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_ospf.py b/src/conf_mode/protocols_ospf.py index 9d35aa007..f2c95a63c 100755 --- a/src/conf_mode/protocols_ospf.py +++ b/src/conf_mode/protocols_ospf.py @@ -1,196 +1,197 @@ #!/usr/bin/env python3 # # Copyright (C) 2021-2024 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/>. from sys import exit from sys import argv from vyos.config import Config from vyos.configdict import get_frrender_dict from vyos.configverify import verify_common_route_maps from vyos.configverify import verify_route_map from vyos.configverify import verify_interface_exists from vyos.configverify import verify_access_list from vyos.configverify import has_frr_protocol_in_dict from vyos.frrender import FRRender from vyos.utils.dict import dict_search from vyos.utils.network import get_interface_config +from vyos.utils.process import is_systemd_service_running from vyos import ConfigError from vyos import airbag airbag.enable() def get_config(config=None): if config: conf = config else: conf = Config() return get_frrender_dict(conf, argv) def verify(config_dict): if not has_frr_protocol_in_dict(config_dict, 'ospf'): return None vrf = None if 'vrf_context' in config_dict: vrf = config_dict['vrf_context'] # eqivalent of the C foo ? 'a' : 'b' statement ospf = vrf and config_dict['vrf']['name'][vrf]['protocols']['ospf'] or config_dict['ospf'] ospf['policy'] = config_dict['policy'] verify_common_route_maps(ospf) # As we can have a default-information route-map, we need to validate it! route_map_name = dict_search('default_information.originate.route_map', ospf) if route_map_name: verify_route_map(route_map_name, ospf) # Validate if configured Access-list exists if 'area' in ospf: networks = [] for area, area_config in ospf['area'].items(): if 'import_list' in area_config: acl_import = area_config['import_list'] if acl_import: verify_access_list(acl_import, ospf) if 'export_list' in area_config: acl_export = area_config['export_list'] if acl_export: verify_access_list(acl_export, ospf) if 'network' in area_config: for network in area_config['network']: if network in networks: raise ConfigError(f'Network "{network}" already defined in different area!') networks.append(network) if 'interface' in ospf: for interface, interface_config in ospf['interface'].items(): verify_interface_exists(ospf, interface) # One can not use dead-interval and hello-multiplier at the same # time. FRR will only activate the last option set via CLI. if {'hello_multiplier', 'dead_interval'} <= set(interface_config): raise ConfigError(f'Can not use hello-multiplier and dead-interval ' \ f'concurrently for {interface}!') # One can not use the "network <prefix> area <id>" command and an # per interface area assignment at the same time. FRR will error # out using: "Please remove all network commands first." if 'area' in ospf and 'area' in interface_config: for area, area_config in ospf['area'].items(): if 'network' in area_config: raise ConfigError('Can not use OSPF interface area and area ' \ 'network configuration at the same time!') # If interface specific options are set, we must ensure that the # interface is bound to our requesting VRF. Due to the VyOS # priorities the interface is bound to the VRF after creation of # the VRF itself, and before any routing protocol is configured. if vrf: tmp = get_interface_config(interface) if 'master' not in tmp or tmp['master'] != vrf: raise ConfigError(f'Interface "{interface}" is not a member of VRF "{vrf}"!') # Segment routing checks if dict_search('segment_routing.global_block', ospf): g_high_label_value = dict_search('segment_routing.global_block.high_label_value', ospf) g_low_label_value = dict_search('segment_routing.global_block.low_label_value', ospf) # If segment routing global block high or low value is blank, throw error if not (g_low_label_value or g_high_label_value): raise ConfigError('Segment routing global-block requires both low and high value!') # If segment routing global block low value is higher than the high value, throw error if int(g_low_label_value) > int(g_high_label_value): raise ConfigError('Segment routing global-block low value must be lower than high value') if dict_search('segment_routing.local_block', ospf): if dict_search('segment_routing.global_block', ospf) == None: raise ConfigError('Segment routing local-block requires global-block to be configured!') l_high_label_value = dict_search('segment_routing.local_block.high_label_value', ospf) l_low_label_value = dict_search('segment_routing.local_block.low_label_value', ospf) # If segment routing local-block high or low value is blank, throw error if not (l_low_label_value or l_high_label_value): raise ConfigError('Segment routing local-block requires both high and low value!') # If segment routing local-block low value is higher than the high value, throw error if int(l_low_label_value) > int(l_high_label_value): raise ConfigError('Segment routing local-block low value must be lower than high value') # local-block most live outside global block global_range = range(int(g_low_label_value), int(g_high_label_value) +1) local_range = range(int(l_low_label_value), int(l_high_label_value) +1) # Check for overlapping ranges if list(set(global_range) & set(local_range)): raise ConfigError(f'Segment-Routing Global Block ({g_low_label_value}/{g_high_label_value}) '\ f'conflicts with Local Block ({l_low_label_value}/{l_high_label_value})!') # Check for a blank or invalid value per prefix if dict_search('segment_routing.prefix', ospf): for prefix, prefix_config in ospf['segment_routing']['prefix'].items(): if 'index' in prefix_config: if prefix_config['index'].get('value') is None: raise ConfigError(f'Segment routing prefix {prefix} index value cannot be blank.') # Check for explicit-null and no-php-flag configured at the same time per prefix if dict_search('segment_routing.prefix', ospf): for prefix, prefix_config in ospf['segment_routing']['prefix'].items(): if 'index' in prefix_config: if ("explicit_null" in prefix_config['index']) and ("no_php_flag" in prefix_config['index']): raise ConfigError(f'Segment routing prefix {prefix} cannot have both explicit-null '\ f'and no-php-flag configured at the same time.') # Check for index ranges being larger than the segment routing global block if dict_search('segment_routing.global_block', ospf): g_high_label_value = dict_search('segment_routing.global_block.high_label_value', ospf) g_low_label_value = dict_search('segment_routing.global_block.low_label_value', ospf) g_label_difference = int(g_high_label_value) - int(g_low_label_value) if dict_search('segment_routing.prefix', ospf): for prefix, prefix_config in ospf['segment_routing']['prefix'].items(): if 'index' in prefix_config: index_size = ospf['segment_routing']['prefix'][prefix]['index']['value'] if int(index_size) > int(g_label_difference): raise ConfigError(f'Segment routing prefix {prefix} cannot have an '\ f'index base size larger than the SRGB label base.') # Check route summarisation if 'summary_address' in ospf: for prefix, prefix_options in ospf['summary_address'].items(): if {'tag', 'no_advertise'} <= set(prefix_options): raise ConfigError(f'Can not set both "tag" and "no-advertise" for Type-5 '\ f'and Type-7 route summarisation of "{prefix}"!') return None def generate(config_dict): - if config_dict and 'frrender_cls' not in config_dict: + if config_dict and not is_systemd_service_running('vyos-configd.service'): FRRender().generate(config_dict) return None def apply(config_dict): - if config_dict and 'frrender_cls' not in config_dict: + if config_dict and not is_systemd_service_running('vyos-configd.service'): FRRender().apply() 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_ospfv3.py b/src/conf_mode/protocols_ospfv3.py index c6b042b54..ac189c378 100755 --- a/src/conf_mode/protocols_ospfv3.py +++ b/src/conf_mode/protocols_ospfv3.py @@ -1,107 +1,108 @@ #!/usr/bin/env python3 # # Copyright (C) 2021-2024 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/>. from sys import exit from sys import argv from vyos.config import Config from vyos.configdict import get_frrender_dict from vyos.configverify import verify_common_route_maps from vyos.configverify import verify_route_map from vyos.configverify import verify_interface_exists from vyos.configverify import has_frr_protocol_in_dict from vyos.frrender import FRRender from vyos.ifconfig import Interface from vyos.utils.dict import dict_search from vyos.utils.network import get_interface_config +from vyos.utils.process import is_systemd_service_running from vyos import ConfigError from vyos import airbag airbag.enable() def get_config(config=None): if config: conf = config else: conf = Config() return get_frrender_dict(conf, argv) def verify(config_dict): if not has_frr_protocol_in_dict(config_dict, 'ospfv3'): return None vrf = None if 'vrf_context' in config_dict: vrf = config_dict['vrf_context'] # eqivalent of the C foo ? 'a' : 'b' statement ospfv3 = vrf and config_dict['vrf']['name'][vrf]['protocols']['ospfv3'] or config_dict['ospfv3'] ospfv3['policy'] = config_dict['policy'] verify_common_route_maps(ospfv3) # As we can have a default-information route-map, we need to validate it! route_map_name = dict_search('default_information.originate.route_map', ospfv3) if route_map_name: verify_route_map(route_map_name, ospfv3) if 'area' in ospfv3: for area, area_config in ospfv3['area'].items(): if 'area_type' in area_config: if len(area_config['area_type']) > 1: raise ConfigError(f'Can only configure one area-type for OSPFv3 area "{area}"!') if 'range' in area_config: for range, range_config in area_config['range'].items(): if {'not_advertise', 'advertise'} <= range_config.keys(): raise ConfigError(f'"not-advertise" and "advertise" for "range {range}" cannot be both configured at the same time!') if 'interface' in ospfv3: for interface, interface_config in ospfv3['interface'].items(): verify_interface_exists(ospfv3, interface) if 'ifmtu' in interface_config: mtu = Interface(interface).get_mtu() if int(interface_config['ifmtu']) > int(mtu): raise ConfigError(f'OSPFv3 ifmtu can not exceed physical MTU of "{mtu}"') # If interface specific options are set, we must ensure that the # interface is bound to our requesting VRF. Due to the VyOS # priorities the interface is bound to the VRF after creation of # the VRF itself, and before any routing protocol is configured. if vrf: tmp = get_interface_config(interface) if 'master' not in tmp or tmp['master'] != vrf: raise ConfigError(f'Interface "{interface}" is not a member of VRF "{vrf}"!') return None def generate(config_dict): - if config_dict and 'frrender_cls' not in config_dict: + if config_dict and not is_systemd_service_running('vyos-configd.service'): FRRender().generate(config_dict) return None def apply(config_dict): - if config_dict and 'frrender_cls' not in config_dict: + if config_dict and not is_systemd_service_running('vyos-configd.service'): FRRender().apply() 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_pim.py b/src/conf_mode/protocols_pim.py index c40b9d86a..477895b0b 100755 --- a/src/conf_mode/protocols_pim.py +++ b/src/conf_mode/protocols_pim.py @@ -1,118 +1,119 @@ #!/usr/bin/env python3 # # Copyright (C) 2020-2024 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 ipaddress import IPv4Address from ipaddress import IPv4Network from signal import SIGTERM from sys import exit from vyos.config import Config from vyos.configdict import get_frrender_dict from vyos.configverify import verify_interface_exists from vyos.configverify import has_frr_protocol_in_dict from vyos.frrender import FRRender from vyos.frrender import pim_daemon +from vyos.utils.process import is_systemd_service_running from vyos.utils.process import process_named_running from vyos.utils.process import call from vyos import ConfigError from vyos import airbag airbag.enable() def get_config(config=None): if config: conf = config else: conf = Config() return get_frrender_dict(conf) def verify(config_dict): if not has_frr_protocol_in_dict(config_dict, 'pim'): return None pim = config_dict['pim'] if 'deleted' in pim: return None if 'igmp_proxy_enabled' in pim: raise ConfigError('IGMP proxy and PIM cannot be configured at the same time!') if 'interface' not in pim: raise ConfigError('PIM require defined interfaces!') RESERVED_MC_NET = '224.0.0.0/24' for interface, interface_config in pim['interface'].items(): verify_interface_exists(pim, interface) # Check join group in reserved net if 'igmp' in interface_config and 'join' in interface_config['igmp']: for join_addr in interface_config['igmp']['join']: if IPv4Address(join_addr) in IPv4Network(RESERVED_MC_NET): raise ConfigError(f'Groups within {RESERVED_MC_NET} are reserved and cannot be joined!') if 'rp' in pim: if 'address' not in pim['rp']: raise ConfigError('PIM rendezvous point needs to be defined!') # Check unique multicast groups unique = [] pim_base_error = 'PIM rendezvous point group' for address, address_config in pim['rp']['address'].items(): if 'group' not in address_config: raise ConfigError(f'{pim_base_error} should be defined for "{address}"!') # Check if it is a multicast group for gr_addr in address_config['group']: if not IPv4Network(gr_addr).is_multicast: raise ConfigError(f'{pim_base_error} "{gr_addr}" is not a multicast group!') if gr_addr in unique: raise ConfigError(f'{pim_base_error} must be unique!') unique.append(gr_addr) def generate(config_dict): - if config_dict and 'frrender_cls' not in config_dict: + if config_dict and not is_systemd_service_running('vyos-configd.service'): FRRender().generate(config_dict) return None def apply(config_dict): if not has_frr_protocol_in_dict(config_dict, 'pim'): return None pim_pid = process_named_running(pim_daemon) pim = config_dict['pim'] if 'deleted' in pim: os.kill(int(pim_pid), SIGTERM) return None if not pim_pid: call('/usr/lib/frr/pimd -d -F traditional --daemon -A 127.0.0.1') - if config_dict and 'frrender_cls' not in config_dict: + if config_dict and not is_systemd_service_running('vyos-configd.service'): FRRender().apply() 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_pim6.py b/src/conf_mode/protocols_pim6.py index ff8fa38c3..3a9b876cc 100755 --- a/src/conf_mode/protocols_pim6.py +++ b/src/conf_mode/protocols_pim6.py @@ -1,95 +1,96 @@ #!/usr/bin/env python3 # # Copyright (C) 2023 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/>. from ipaddress import IPv6Address from ipaddress import IPv6Network from sys import exit from vyos.config import Config from vyos.configdict import get_frrender_dict from vyos.configverify import has_frr_protocol_in_dict from vyos.configverify import verify_interface_exists +from vyos.utils.process import is_systemd_service_running from vyos.frrender import FRRender from vyos import ConfigError from vyos import airbag airbag.enable() def get_config(config=None): if config: conf = config else: conf = Config() return get_frrender_dict(conf) def verify(config_dict): if not has_frr_protocol_in_dict(config_dict, 'pim6'): return None pim6 = config_dict['pim6'] if 'deleted' in pim6: return None for interface, interface_config in pim6.get('interface', {}).items(): verify_interface_exists(pim6, interface) if 'mld' in interface_config: mld = interface_config['mld'] for group in mld.get('join', {}).keys(): # Validate multicast group address if not IPv6Address(group).is_multicast: raise ConfigError(f"{group} is not a multicast group") if 'rp' in pim6: if 'address' not in pim6['rp']: raise ConfigError('PIM6 rendezvous point needs to be defined!') # Check unique multicast groups unique = [] pim_base_error = 'PIM6 rendezvous point group' if {'address', 'prefix-list6'} <= set(pim6['rp']): raise ConfigError(f'{pim_base_error} supports either address or a prefix-list!') for address, address_config in pim6['rp']['address'].items(): if 'group' not in address_config: raise ConfigError(f'{pim_base_error} should be defined for "{address}"!') # Check if it is a multicast group for gr_addr in address_config['group']: if not IPv6Network(gr_addr).is_multicast: raise ConfigError(f'{pim_base_error} "{gr_addr}" is not a multicast group!') if gr_addr in unique: raise ConfigError(f'{pim_base_error} must be unique!') unique.append(gr_addr) def generate(config_dict): - if config_dict and 'frrender_cls' not in config_dict: + if config_dict and not is_systemd_service_running('vyos-configd.service'): FRRender().generate(config_dict) return None def apply(config_dict): - if config_dict and 'frrender_cls' not in config_dict: + if config_dict and not is_systemd_service_running('vyos-configd.service'): FRRender().apply() 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_rip.py b/src/conf_mode/protocols_rip.py index 38108c9f5..39743f965 100755 --- a/src/conf_mode/protocols_rip.py +++ b/src/conf_mode/protocols_rip.py @@ -1,88 +1,89 @@ #!/usr/bin/env python3 # # Copyright (C) 2021-2024 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/>. from sys import exit from vyos.config import Config from vyos.configdict import get_frrender_dict from vyos.configverify import has_frr_protocol_in_dict from vyos.configverify import verify_common_route_maps from vyos.configverify import verify_access_list from vyos.configverify import verify_prefix_list from vyos.frrender import FRRender from vyos.utils.dict import dict_search +from vyos.utils.process import is_systemd_service_running from vyos import ConfigError from vyos import airbag airbag.enable() def get_config(config=None): if config: conf = config else: conf = Config() return get_frrender_dict(conf) def verify(config_dict): if not has_frr_protocol_in_dict(config_dict, 'rip'): return None rip = config_dict['rip'] rip['policy'] = config_dict['policy'] verify_common_route_maps(rip) acl_in = dict_search('distribute_list.access_list.in', rip) if acl_in: verify_access_list(acl_in, rip) acl_out = dict_search('distribute_list.access_list.out', rip) if acl_out: verify_access_list(acl_out, rip) prefix_list_in = dict_search('distribute_list.prefix-list.in', rip) if prefix_list_in: verify_prefix_list(prefix_list_in, rip) prefix_list_out = dict_search('distribute_list.prefix_list.out', rip) if prefix_list_out: verify_prefix_list(prefix_list_out, rip) if 'interface' in rip: for interface, interface_options in rip['interface'].items(): if 'authentication' in interface_options: if {'md5', 'plaintext_password'} <= set(interface_options['authentication']): raise ConfigError('Can not use both md5 and plaintext-password at the same time!') if 'split_horizon' in interface_options: if {'disable', 'poison_reverse'} <= set(interface_options['split_horizon']): raise ConfigError(f'You can not have "split-horizon poison-reverse" enabled ' \ f'with "split-horizon disable" for "{interface}"!') def generate(config_dict): - if config_dict and 'frrender_cls' not in config_dict: + if config_dict and not is_systemd_service_running('vyos-configd.service'): FRRender().generate(config_dict) return None def apply(config_dict): - if config_dict and 'frrender_cls' not in config_dict: + if config_dict and not is_systemd_service_running('vyos-configd.service'): FRRender().apply() 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_ripng.py b/src/conf_mode/protocols_ripng.py index 46f3ce014..14f038444 100755 --- a/src/conf_mode/protocols_ripng.py +++ b/src/conf_mode/protocols_ripng.py @@ -1,88 +1,89 @@ #!/usr/bin/env python3 # # Copyright (C) 2021-2024 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/>. from sys import exit from vyos.config import Config from vyos.configdict import get_frrender_dict from vyos.configverify import has_frr_protocol_in_dict from vyos.configverify import verify_common_route_maps from vyos.configverify import verify_access_list from vyos.configverify import verify_prefix_list from vyos.frrender import FRRender from vyos.utils.dict import dict_search +from vyos.utils.process import is_systemd_service_running from vyos import ConfigError from vyos import airbag airbag.enable() def get_config(config=None): if config: conf = config else: conf = Config() return get_frrender_dict(conf) def verify(config_dict): if not has_frr_protocol_in_dict(config_dict, 'ripng'): return None ripng = config_dict['ripng'] ripng['policy'] = config_dict['policy'] verify_common_route_maps(ripng) acl_in = dict_search('distribute_list.access_list.in', ripng) if acl_in: verify_access_list(acl_in, ripng, version='6') acl_out = dict_search('distribute_list.access_list.out', ripng) if acl_out: verify_access_list(acl_out, ripng, version='6') prefix_list_in = dict_search('distribute_list.prefix_list.in', ripng) if prefix_list_in: verify_prefix_list(prefix_list_in, ripng, version='6') prefix_list_out = dict_search('distribute_list.prefix_list.out', ripng) if prefix_list_out: verify_prefix_list(prefix_list_out, ripng, version='6') if 'interface' in ripng: for interface, interface_options in ripng['interface'].items(): if 'authentication' in interface_options: if {'md5', 'plaintext_password'} <= set(interface_options['authentication']): raise ConfigError('Can not use both md5 and plaintext-password at the same time!') if 'split_horizon' in interface_options: if {'disable', 'poison_reverse'} <= set(interface_options['split_horizon']): raise ConfigError(f'You can not have "split-horizon poison-reverse" enabled ' \ f'with "split-horizon disable" for "{interface}"!') def generate(config_dict): - if config_dict and 'frrender_cls' not in config_dict: + if config_dict and not is_systemd_service_running('vyos-configd.service'): FRRender().generate(config_dict) return None def apply(config_dict): - if config_dict and 'frrender_cls' not in config_dict: + if config_dict and not is_systemd_service_running('vyos-configd.service'): FRRender().apply() 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_rpki.py b/src/conf_mode/protocols_rpki.py index 4aefbe36c..5ad656586 100755 --- a/src/conf_mode/protocols_rpki.py +++ b/src/conf_mode/protocols_rpki.py @@ -1,115 +1,115 @@ #!/usr/bin/env python3 # # Copyright (C) 2021-2024 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 glob import glob from sys import exit from vyos.config import Config from vyos.configdict import get_frrender_dict from vyos.configverify import has_frr_protocol_in_dict from vyos.frrender import FRRender from vyos.pki import wrap_openssh_public_key from vyos.pki import wrap_openssh_private_key from vyos.utils.dict import dict_search_args -from vyos.utils.process import is_systemd_service_running from vyos.utils.file import write_file +from vyos.utils.process import is_systemd_service_running from vyos import ConfigError from vyos import airbag airbag.enable() rpki_ssh_key_base = '/run/frr/id_rpki' def get_config(config=None): if config: conf = config else: conf = Config() return get_frrender_dict(conf) def verify(config_dict): if not has_frr_protocol_in_dict(config_dict, 'rpki'): return None rpki = config_dict['rpki'] if 'cache' in rpki: preferences = [] for peer, peer_config in rpki['cache'].items(): for mandatory in ['port', 'preference']: if mandatory not in peer_config: raise ConfigError(f'RPKI cache "{peer}" {mandatory} must be defined!') if 'preference' in peer_config: preference = peer_config['preference'] if preference in preferences: raise ConfigError(f'RPKI cache with preference {preference} already configured!') preferences.append(preference) if 'ssh' in peer_config: if 'username' not in peer_config['ssh']: raise ConfigError('RPKI+SSH requires username to be defined!') if 'key' not in peer_config['ssh'] or 'openssh' not in rpki['pki']: raise ConfigError('RPKI+SSH requires key to be defined!') if peer_config['ssh']['key'] not in rpki['pki']['openssh']: raise ConfigError('RPKI+SSH key not found on PKI subsystem!') return None def generate(config_dict): for key in glob(f'{rpki_ssh_key_base}*'): os.unlink(key) if not has_frr_protocol_in_dict(config_dict, 'rpki'): return None rpki = config_dict['rpki'] if 'cache' in rpki: for cache, cache_config in rpki['cache'].items(): if 'ssh' in cache_config: key_name = cache_config['ssh']['key'] public_key_data = dict_search_args(rpki['pki'], 'openssh', key_name, 'public', 'key') public_key_type = dict_search_args(rpki['pki'], 'openssh', key_name, 'public', 'type') private_key_data = dict_search_args(rpki['pki'], 'openssh', key_name, 'private', 'key') cache_config['ssh']['public_key_file'] = f'{rpki_ssh_key_base}_{cache}.pub' cache_config['ssh']['private_key_file'] = f'{rpki_ssh_key_base}_{cache}' write_file(cache_config['ssh']['public_key_file'], wrap_openssh_public_key(public_key_data, public_key_type)) write_file(cache_config['ssh']['private_key_file'], wrap_openssh_private_key(private_key_data)) if config_dict and not is_systemd_service_running('vyos-configd.service'): FRRender().generate(config_dict) return None def apply(config_dict): if config_dict and not is_systemd_service_running('vyos-configd.service'): FRRender().apply() 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_segment-routing.py b/src/conf_mode/protocols_segment-routing.py index cc42e5ac7..99cf87556 100755 --- a/src/conf_mode/protocols_segment-routing.py +++ b/src/conf_mode/protocols_segment-routing.py @@ -1,103 +1,104 @@ #!/usr/bin/env python3 # # Copyright (C) 2023-2024 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/>. from sys import exit from vyos.config import Config from vyos.configdict import get_frrender_dict from vyos.configdict import list_diff from vyos.configverify import has_frr_protocol_in_dict from vyos.frrender import FRRender from vyos.ifconfig import Section from vyos.utils.dict import dict_search +from vyos.utils.process import is_systemd_service_running from vyos.utils.system import sysctl_write from vyos import ConfigError from vyos import airbag airbag.enable() def get_config(config=None): if config: conf = config else: conf = Config() return get_frrender_dict(conf) def verify(config_dict): if not has_frr_protocol_in_dict(config_dict, 'segment_routing'): return None sr = config_dict['segment_routing'] if 'srv6' in sr: srv6_enable = False if 'interface' in sr: for interface, interface_config in sr['interface'].items(): if 'srv6' in interface_config: srv6_enable = True break if not srv6_enable: raise ConfigError('SRv6 should be enabled on at least one interface!') return None def generate(config_dict): - if config_dict and 'frrender_cls' not in config_dict: + if config_dict and not is_systemd_service_running('vyos-configd.service'): FRRender().generate(config_dict) return None def apply(config_dict): if not has_frr_protocol_in_dict(config_dict, 'segment_routing'): return None sr = config_dict['segment_routing'] current_interfaces = Section.interfaces() sr_interfaces = list(sr.get('interface', {}).keys()) for interface in list_diff(current_interfaces, sr_interfaces): # Disable processing of IPv6-SR packets sysctl_write(f'net.ipv6.conf.{interface}.seg6_enabled', '0') for interface, interface_config in sr.get('interface', {}).items(): # Accept or drop SR-enabled IPv6 packets on this interface if 'srv6' in interface_config: sysctl_write(f'net.ipv6.conf.{interface}.seg6_enabled', '1') # Define HMAC policy for ingress SR-enabled packets on this interface # It's a redundant check as HMAC has a default value - but better safe # then sorry tmp = dict_search('srv6.hmac', interface_config) if tmp == 'accept': sysctl_write(f'net.ipv6.conf.{interface}.seg6_require_hmac', '0') elif tmp == 'drop': sysctl_write(f'net.ipv6.conf.{interface}.seg6_require_hmac', '1') elif tmp == 'ignore': sysctl_write(f'net.ipv6.conf.{interface}.seg6_require_hmac', '-1') else: sysctl_write(f'net.ipv6.conf.{interface}.seg6_enabled', '0') - if config_dict and 'frrender_cls' not in config_dict: + if config_dict and not is_systemd_service_running('vyos-configd.service'): FRRender().apply() 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 29fa530f4..9d02db6dd 100755 --- a/src/conf_mode/protocols_static.py +++ b/src/conf_mode/protocols_static.py @@ -1,114 +1,115 @@ #!/usr/bin/env python3 # # Copyright (C) 2021-2024 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/>. from ipaddress import IPv4Network from sys import exit from sys import argv from vyos.config import Config from vyos.configdict import get_frrender_dict from vyos.configverify import has_frr_protocol_in_dict from vyos.configverify import verify_common_route_maps from vyos.configverify import verify_vrf from vyos.frrender import FRRender +from vyos.utils.process import is_systemd_service_running from vyos.template import render from vyos import ConfigError from vyos import airbag airbag.enable() config_file = '/etc/iproute2/rt_tables.d/vyos-static.conf' def get_config(config=None): if config: conf = config else: conf = Config() return get_frrender_dict(conf, argv) def verify(config_dict): if not has_frr_protocol_in_dict(config_dict, 'static'): return None vrf = None if 'vrf_context' in config_dict: vrf = config_dict['vrf_context'] # eqivalent of the C foo ? 'a' : 'b' statement static = vrf and config_dict['vrf']['name'][vrf]['protocols']['static'] or config_dict['static'] static['policy'] = config_dict['policy'] 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 '\ f'prefix "{prefix}"!') if 'multicast' in static and 'route' in static['multicast']: for prefix, prefix_options in static['multicast']['route'].items(): if not IPv4Network(prefix).is_multicast: raise ConfigError(f'{prefix} is not a multicast network!') return None def generate(config_dict): if not has_frr_protocol_in_dict(config_dict, 'static'): return None vrf = None if 'vrf_context' in config_dict: vrf = config_dict['vrf_context'] # eqivalent of the C foo ? 'a' : 'b' statement static = vrf and config_dict['vrf']['name'][vrf]['protocols']['static'] or config_dict['static'] # Put routing table names in /etc/iproute2/rt_tables render(config_file, 'iproute2/static.conf.j2', static) - if config_dict and 'frrender_cls' not in config_dict: + if config_dict and not is_systemd_service_running('vyos-configd.service'): FRRender().generate(config_dict) return None def apply(config_dict): - if config_dict and 'frrender_cls' not in config_dict: + if config_dict and not is_systemd_service_running('vyos-configd.service'): FRRender().apply() 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/system_ip.py b/src/conf_mode/system_ip.py index 374e6e611..86843eb78 100755 --- a/src/conf_mode/system_ip.py +++ b/src/conf_mode/system_ip.py @@ -1,126 +1,127 @@ #!/usr/bin/env python3 # # Copyright (C) 2019-2024 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/>. from sys import exit from vyos.config import Config from vyos.configdep import set_dependents from vyos.configdep import call_dependents from vyos.configdict import get_frrender_dict from vyos.configverify import has_frr_protocol_in_dict from vyos.configverify import verify_route_map from vyos.frrender import FRRender from vyos.utils.dict import dict_search from vyos.utils.process import is_systemd_service_active +from vyos.utils.process import is_systemd_service_running from vyos.utils.system import sysctl_write from vyos import ConfigError from vyos import airbag airbag.enable() def get_config(config=None): if config: conf = config else: conf = Config() # If IPv4 ARP table size is set here and also manually in sysctl, the more # fine grained value from sysctl must win set_dependents('sysctl', conf) return get_frrender_dict(conf) def verify(config_dict): if not has_frr_protocol_in_dict(config_dict, 'ip'): return None opt = config_dict['ip'] opt['policy'] = config_dict['policy'] if 'protocol' in opt: for protocol, protocol_options in opt['protocol'].items(): if 'route_map' in protocol_options: verify_route_map(protocol_options['route_map'], opt) return def generate(config_dict): - if config_dict and 'frrender_cls' not in config_dict: + if config_dict and not is_systemd_service_running('vyos-configd.service'): FRRender().generate(config_dict) return None def apply(config_dict): if not has_frr_protocol_in_dict(config_dict, 'ip'): return None opt = config_dict['ip'] # Apply ARP threshold values # table_size has a default value - thus the key always exists size = int(dict_search('arp.table_size', opt)) # Amount upon reaching which the records begin to be cleared immediately sysctl_write('net.ipv4.neigh.default.gc_thresh3', size) # Amount after which the records begin to be cleaned after 5 seconds sysctl_write('net.ipv4.neigh.default.gc_thresh2', size // 2) # Minimum number of stored records is indicated which is not cleared sysctl_write('net.ipv4.neigh.default.gc_thresh1', size // 8) # configure multipath tmp = dict_search('multipath.ignore_unreachable_nexthops', opt) value = '1' if (tmp != None) else '0' sysctl_write('net.ipv4.fib_multipath_use_neigh', value) tmp = dict_search('multipath.layer4_hashing', opt) value = '1' if (tmp != None) else '0' sysctl_write('net.ipv4.fib_multipath_hash_policy', value) # configure TCP options (defaults as of Linux 6.4) tmp = dict_search('tcp.mss.probing', opt) if tmp is None: value = 0 elif tmp == 'on-icmp-black-hole': value = 1 elif tmp == 'force': value = 2 else: # Shouldn't happen raise ValueError("TCP MSS probing is neither 'on-icmp-black-hole' nor 'force'!") sysctl_write('net.ipv4.tcp_mtu_probing', value) tmp = dict_search('tcp.mss.base', opt) value = '1024' if (tmp is None) else tmp sysctl_write('net.ipv4.tcp_base_mss', value) tmp = dict_search('tcp.mss.floor', opt) value = '48' if (tmp is None) else tmp sysctl_write('net.ipv4.tcp_mtu_probe_floor', value) # During startup of vyos-router that brings up FRR, the service is not yet # running when this script is called first. Skip this part and wait for initial # commit of the configuration to trigger this statement if is_systemd_service_active('frr.service'): - if config_dict and 'frrender_cls' not in config_dict: + if config_dict and not is_systemd_service_running('vyos-configd.service'): FRRender().apply() call_dependents() 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/system_ipv6.py b/src/conf_mode/system_ipv6.py index 02c9a8201..593b8f7f3 100755 --- a/src/conf_mode/system_ipv6.py +++ b/src/conf_mode/system_ipv6.py @@ -1,110 +1,111 @@ #!/usr/bin/env python3 # # Copyright (C) 2019-2024 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 vyos.config import Config from vyos.configdep import set_dependents from vyos.configdep import call_dependents from vyos.configdict import get_frrender_dict from vyos.configverify import has_frr_protocol_in_dict from vyos.configverify import verify_route_map from vyos.frrender import FRRender from vyos.utils.dict import dict_search from vyos.utils.file import write_file from vyos.utils.process import is_systemd_service_active +from vyos.utils.process import is_systemd_service_running from vyos.utils.system import sysctl_write from vyos import ConfigError from vyos import airbag airbag.enable() def get_config(config=None): if config: conf = config else: conf = Config() # If IPv6 neighbor table size is set here and also manually in sysctl, the more # fine grained value from sysctl must win set_dependents('sysctl', conf) return get_frrender_dict(conf) def verify(config_dict): if not has_frr_protocol_in_dict(config_dict, 'ipv6'): return None opt = config_dict['ipv6'] opt['policy'] = config_dict['policy'] if 'protocol' in opt: for protocol, protocol_options in opt['protocol'].items(): if 'route_map' in protocol_options: verify_route_map(protocol_options['route_map'], opt) return def generate(config_dict): - if config_dict and 'frrender_cls' not in config_dict: + if config_dict and not is_systemd_service_running('vyos-configd.service'): FRRender().generate(config_dict) return None def apply(config_dict): if not has_frr_protocol_in_dict(config_dict, 'ipv6'): return None opt = config_dict['ipv6'] # configure multipath tmp = dict_search('multipath.layer4_hashing', opt) value = '1' if (tmp != None) else '0' sysctl_write('net.ipv6.fib_multipath_hash_policy', value) # Apply ND threshold values # table_size has a default value - thus the key always exists size = int(dict_search('neighbor.table_size', opt)) # Amount upon reaching which the records begin to be cleared immediately sysctl_write('net.ipv6.neigh.default.gc_thresh3', size) # Amount after which the records begin to be cleaned after 5 seconds sysctl_write('net.ipv6.neigh.default.gc_thresh2', size // 2) # Minimum number of stored records is indicated which is not cleared sysctl_write('net.ipv6.neigh.default.gc_thresh1', size // 8) # configure IPv6 strict-dad tmp = dict_search('strict_dad', opt) value = '2' if (tmp != None) else '1' for root, dirs, files in os.walk('/proc/sys/net/ipv6/conf'): for name in files: if name == 'accept_dad': write_file(os.path.join(root, name), value) # During startup of vyos-router that brings up FRR, the service is not yet # running when this script is called first. Skip this part and wait for initial # commit of the configuration to trigger this statement if is_systemd_service_active('frr.service'): - if config_dict and 'frrender_cls' not in config_dict: + if config_dict and not is_systemd_service_running('vyos-configd.service'): FRRender().apply() call_dependents() 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/vrf.py b/src/conf_mode/vrf.py index 1b19c55d2..6533f493f 100755 --- a/src/conf_mode/vrf.py +++ b/src/conf_mode/vrf.py @@ -1,357 +1,358 @@ #!/usr/bin/env python3 # # Copyright (C) 2020-2024 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/>. from sys import exit from jmespath import search from json import loads from vyos.config import Config from vyos.configdict import get_frrender_dict from vyos.configdict import node_changed from vyos.configverify import verify_route_map from vyos.firewall import conntrack_required from vyos.frrender import FRRender from vyos.ifconfig import Interface from vyos.template import render from vyos.utils.dict import dict_search from vyos.utils.network import get_vrf_tableid from vyos.utils.network import get_vrf_members from vyos.utils.network import interface_exists from vyos.utils.process import call from vyos.utils.process import cmd +from vyos.utils.process import is_systemd_service_running from vyos.utils.process import popen from vyos.utils.system import sysctl_write from vyos import ConfigError from vyos import airbag airbag.enable() config_file = '/etc/iproute2/rt_tables.d/vyos-vrf.conf' k_mod = ['vrf'] nftables_table = 'inet vrf_zones' nftables_rules = { 'vrf_zones_ct_in': 'counter ct original zone set iifname map @ct_iface_map', 'vrf_zones_ct_out': 'counter ct original zone set oifname map @ct_iface_map' } def has_rule(af : str, priority : int, table : str=None): """ Check if a given ip rule exists $ ip --json -4 rule show [{'l3mdev': None, 'priority': 1000, 'src': 'all'}, {'action': 'unreachable', 'l3mdev': None, 'priority': 2000, 'src': 'all'}, {'priority': 32765, 'src': 'all', 'table': 'local'}, {'priority': 32766, 'src': 'all', 'table': 'main'}, {'priority': 32767, 'src': 'all', 'table': 'default'}] """ if af not in ['-4', '-6']: raise ValueError() command = f'ip --detail --json {af} rule show' for tmp in loads(cmd(command)): if 'priority' in tmp and 'table' in tmp: if tmp['priority'] == priority and tmp['table'] == table: return True elif 'priority' in tmp and table in tmp: # l3mdev table has a different layout if tmp['priority'] == priority: return True return False def is_nft_vrf_zone_rule_setup() -> bool: """ Check if an nftables connection tracking rule already exists """ tmp = loads(cmd('sudo nft -j list table inet vrf_zones')) num_rules = len(search("nftables[].rule[].chain", tmp)) return bool(num_rules) def vrf_interfaces(c, match): matched = [] old_level = c.get_level() c.set_level(['interfaces']) section = c.get_config_dict([], get_first_key=True) for type in section: interfaces = section[type] for name in interfaces: interface = interfaces[name] if 'vrf' in interface: v = interface.get('vrf', '') if v == match: matched.append(name) c.set_level(old_level) return matched def vrf_routing(c, match): matched = [] old_level = c.get_level() c.set_level(['protocols', 'vrf']) if match in c.list_nodes([]): matched.append(match) c.set_level(old_level) return matched def get_config(config=None): if config: conf = config else: conf = Config() base = ['vrf'] vrf = conf.get_config_dict(base, key_mangling=('-', '_'), no_tag_node_value_mangle=True, get_first_key=True) # determine which VRF has been removed for name in node_changed(conf, base + ['name']): if 'vrf_remove' not in vrf: vrf.update({'vrf_remove' : {}}) vrf['vrf_remove'][name] = {} # get VRF bound interfaces interfaces = vrf_interfaces(conf, name) if interfaces: vrf['vrf_remove'][name]['interface'] = interfaces # get VRF bound routing instances routes = vrf_routing(conf, name) if routes: vrf['vrf_remove'][name]['route'] = routes if 'name' in vrf: vrf['conntrack'] = conntrack_required(conf) # We need to merge the FRR rendering dict into the VRF dict # this is required to get the route-map information to FRR vrf.update({'frr_dict' : get_frrender_dict(conf)}) return vrf def verify(vrf): # ensure VRF is not assigned to any interface if 'vrf_remove' in vrf: for name, config in vrf['vrf_remove'].items(): if 'interface' in config: raise ConfigError(f'Can not remove VRF "{name}", it still has '\ f'member interfaces!') if 'route' in config: raise ConfigError(f'Can not remove VRF "{name}", it still has '\ f'static routes installed!') if 'name' in vrf: reserved_names = ["add", "all", "broadcast", "default", "delete", "dev", "get", "inet", "mtu", "link", "type", "vrf"] table_ids = [] vnis = [] for name, vrf_config in vrf['name'].items(): # Reserved VRF names if name in reserved_names: raise ConfigError(f'VRF name "{name}" is reserved and connot be used!') # table id is mandatory if 'table' not in vrf_config: raise ConfigError(f'VRF "{name}" table id is mandatory!') # routing table id can't be changed - OS restriction if interface_exists(name): tmp = get_vrf_tableid(name) if tmp and tmp != int(vrf_config['table']): raise ConfigError(f'VRF "{name}" table id modification not possible!') # VRF routing table ID must be unique on the system if 'table' in vrf_config and vrf_config['table'] in table_ids: raise ConfigError(f'VRF "{name}" table id is not unique!') table_ids.append(vrf_config['table']) # VRF VNIs must be unique on the system if 'vni' in vrf_config: vni = vrf_config['vni'] if vni in vnis: raise ConfigError(f'VRF "{name}" VNI "{vni}" is not unique!') vnis.append(vni) tmp = dict_search('ip.protocol', vrf_config) if tmp != None: for protocol, protocol_options in tmp.items(): if 'route_map' in protocol_options: verify_route_map(protocol_options['route_map'], vrf['frr_dict']) tmp = dict_search('ipv6.protocol', vrf_config) if tmp != None: for protocol, protocol_options in tmp.items(): if 'route_map' in protocol_options: verify_route_map(protocol_options['route_map'], vrf['frr_dict']) return None def generate(vrf): # Render iproute2 VR helper names render(config_file, 'iproute2/vrf.conf.j2', vrf) - if 'frr_dict' in vrf and 'frrender_cls' not in vrf['frr_dict']: + if 'frr_dict' in vrf and not is_systemd_service_running('vyos-configd.service'): FRRender().generate(vrf['frr_dict']) return None def apply(vrf): # Documentation # # - https://github.com/torvalds/linux/blob/master/Documentation/networking/vrf.txt # - https://github.com/Mellanox/mlxsw/wiki/Virtual-Routing-and-Forwarding-(VRF) # - https://github.com/Mellanox/mlxsw/wiki/L3-Tunneling # - https://netdevconf.info/1.1/proceedings/slides/ahern-vrf-tutorial.pdf # - https://netdevconf.info/1.2/slides/oct6/02_ahern_what_is_l3mdev_slides.pdf # set the default VRF global behaviour bind_all = '0' if 'bind_to_all' in vrf: bind_all = '1' sysctl_write('net.ipv4.tcp_l3mdev_accept', bind_all) sysctl_write('net.ipv4.udp_l3mdev_accept', bind_all) for tmp in (dict_search('vrf_remove', vrf) or []): if interface_exists(tmp): # T5492: deleting a VRF instance may leafe processes running # (e.g. dhclient) as there is a depedency ordering issue in the CLI. # We need to ensure that we stop the dhclient processes first so # a proper DHCLP RELEASE message is sent for interface in get_vrf_members(tmp): vrf_iface = Interface(interface) vrf_iface.set_dhcp(False) vrf_iface.set_dhcpv6(False) # Remove nftables conntrack zone map item nft_del_element = f'delete element inet vrf_zones ct_iface_map {{ "{tmp}" }}' # Check if deleting is possible first to avoid raising errors _, err = popen(f'nft --check {nft_del_element}') if not err: # Remove map element cmd(f'nft {nft_del_element}') # Delete the VRF Kernel interface call(f'ip link delete dev {tmp}') if 'name' in vrf: # Linux routing uses rules to find tables - routing targets are then # looked up in those tables. If the lookup got a matching route, the # process ends. # # TL;DR; first table with a matching entry wins! # # You can see your routing table lookup rules using "ip rule", sadly the # local lookup is hit before any VRF lookup. Pinging an addresses from the # VRF will usually find a hit in the local table, and never reach the VRF # routing table - this is usually not what you want. Thus we will # re-arrange the tables and move the local lookup further down once VRFs # are enabled. # # Thanks to https://stbuehler.de/blog/article/2020/02/29/using_vrf__virtual_routing_and_forwarding__on_linux.html for afi in ['-4', '-6']: # move lookup local to pref 32765 (from 0) if not has_rule(afi, 32765, 'local'): call(f'ip {afi} rule add pref 32765 table local') if has_rule(afi, 0, 'local'): call(f'ip {afi} rule del pref 0') # make sure that in VRFs after failed lookup in the VRF specific table # nothing else is reached if not has_rule(afi, 1000, 'l3mdev'): # this should be added by the kernel when a VRF is created # add it here for completeness call(f'ip {afi} rule add pref 1000 l3mdev protocol kernel') # add another rule with an unreachable target which only triggers in VRF context # if a route could not be reached if not has_rule(afi, 2000, 'l3mdev'): call(f'ip {afi} rule add pref 2000 l3mdev unreachable') nft_vrf_zone_rule_setup = False for name, config in vrf['name'].items(): table = config['table'] if not interface_exists(name): # For each VRF apart from your default context create a VRF # interface with a separate routing table call(f'ip link add {name} type vrf table {table}') # set VRF description for e.g. SNMP monitoring vrf_if = Interface(name) # We also should add proper loopback IP addresses to the newly added # VRF for services bound to the loopback address (SNMP, NTP) vrf_if.add_addr('127.0.0.1/8') vrf_if.add_addr('::1/128') # add VRF description if available vrf_if.set_alias(config.get('description', '')) # Enable/Disable IPv4 forwarding tmp = dict_search('ip.disable_forwarding', config) value = '0' if (tmp != None) else '1' vrf_if.set_ipv4_forwarding(value) # Enable/Disable IPv6 forwarding tmp = dict_search('ipv6.disable_forwarding', config) value = '0' if (tmp != None) else '1' vrf_if.set_ipv6_forwarding(value) # Enable/Disable of an interface must always be done at the end of the # derived class to make use of the ref-counting set_admin_state() # function. We will only enable the interface if 'up' was called as # often as 'down'. This is required by some interface implementations # as certain parameters can only be changed when the interface is # in admin-down state. This ensures the link does not flap during # reconfiguration. state = 'down' if 'disable' in config else 'up' vrf_if.set_admin_state(state) # Add nftables conntrack zone map item nft_add_element = f'add element inet vrf_zones ct_iface_map {{ "{name}" : {table} }}' cmd(f'nft {nft_add_element}') # Only call into nftables as long as there is nothing setup to avoid wasting # CPU time and thus lenghten the commit process if not nft_vrf_zone_rule_setup: nft_vrf_zone_rule_setup = is_nft_vrf_zone_rule_setup() # Install nftables conntrack rules only once if vrf['conntrack'] and not nft_vrf_zone_rule_setup: for chain, rule in nftables_rules.items(): cmd(f'nft add rule inet vrf_zones {chain} {rule}') if 'name' not in vrf or not vrf['conntrack']: for chain, rule in nftables_rules.items(): cmd(f'nft flush chain inet vrf_zones {chain}') # Return default ip rule values if 'name' not in vrf: for afi in ['-4', '-6']: # move lookup local to pref 0 (from 32765) if not has_rule(afi, 0, 'local'): call(f'ip {afi} rule add pref 0 from all lookup local') if has_rule(afi, 32765, 'local'): call(f'ip {afi} rule del pref 32765 table local') if has_rule(afi, 1000, 'l3mdev'): call(f'ip {afi} rule del pref 1000 l3mdev protocol kernel') if has_rule(afi, 2000, 'l3mdev'): call(f'ip {afi} rule del pref 2000 l3mdev unreachable') - if 'frr_dict' in vrf and 'frrender_cls' not in vrf['frr_dict']: + if 'frr_dict' in vrf and not is_systemd_service_running('vyos-configd.service'): FRRender().apply() return None if __name__ == '__main__': try: c = get_config() verify(c) generate(c) apply(c) except ConfigError as e: print(e) exit(1)