diff options
Diffstat (limited to 'src')
| -rwxr-xr-x | src/conf_mode/interfaces-wwan.py | 2 | ||||
| -rwxr-xr-x | src/conf_mode/policy.py | 132 | ||||
| -rwxr-xr-x | src/migration-scripts/policy/3-to-4 | 162 | ||||
| -rwxr-xr-x | src/validators/bgp-extended-community | 55 | ||||
| -rwxr-xr-x | src/validators/bgp-large-community | 53 | ||||
| -rwxr-xr-x | src/validators/bgp-regular-community | 50 | 
6 files changed, 430 insertions, 24 deletions
| diff --git a/src/conf_mode/interfaces-wwan.py b/src/conf_mode/interfaces-wwan.py index 97b3a6396..a14a992ae 100755 --- a/src/conf_mode/interfaces-wwan.py +++ b/src/conf_mode/interfaces-wwan.py @@ -116,7 +116,7 @@ def generate(wwan):      # disconnect - e.g. happens during RF signal loss. The script watches every      # WWAN interface - so there is only one instance.      if not os.path.exists(cron_script): -        write_file(cron_script, '*/5 * * * * root /usr/libexec/vyos/vyos-check-wwan.py') +        write_file(cron_script, '*/5 * * * * root /usr/libexec/vyos/vyos-check-wwan.py\n')      return None diff --git a/src/conf_mode/policy.py b/src/conf_mode/policy.py index 3008a20e0..a0d288e91 100755 --- a/src/conf_mode/policy.py +++ b/src/conf_mode/policy.py @@ -23,8 +23,42 @@ from vyos.util import dict_search  from vyos import ConfigError  from vyos import frr  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, @@ -46,6 +80,7 @@ def routing_policy_find(key, dictionary):                      for result in routing_policy_find(key, d):                          yield result +  def get_config(config=None):      if config:          conf = config @@ -53,7 +88,8 @@ def get_config(config=None):          conf = Config()      base = ['policy'] -    policy = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True, +    policy = conf.get_config_dict(base, key_mangling=('-', '_'), +                                  get_first_key=True,                                    no_tag_node_value_mangle=True)      # We also need some additional information from the config, prefix-lists @@ -67,12 +103,14 @@ def get_config(config=None):      policy = dict_merge(tmp, policy)      return policy +  def verify(policy):      if not policy:          return None      for policy_type in ['access_list', 'access_list6', 'as_path_list', -                        'community_list', 'extcommunity_list', 'large_community_list', +                        'community_list', 'extcommunity_list', +                        'large_community_list',                          'prefix_list', 'prefix_list6', 'route_map']:          # Bail out early and continue with next policy type          if policy_type not in policy: @@ -97,15 +135,18 @@ def verify(policy):                      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 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}') +                            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', +                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}') @@ -115,10 +156,10 @@ def verify(policy):                          raise ConfigError(f'A prefix {mandatory_error}')                      if rule_config in entries: -                        raise ConfigError(f'Rule "{rule}" contains a duplicate prefix definition!') +                        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(): @@ -127,19 +168,23 @@ def verify(policy):              for rule, rule_config in route_map_config['rule'].items():                  # Specified community-list must exist -                tmp = dict_search('match.community.community_list', rule_config) +                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!') +                    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) +                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!') +                    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) @@ -147,49 +192,87 @@ def verify(policy):                      raise ConfigError(f'prefix-list {tmp} does not exist!')                  # Specified prefix-list must exist -                tmp = dict_search('match.ipv6.address.prefix_list', rule_config) +                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) +                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) +                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      if 'protocols' in policy: -        for policy_type in ['access_list', 'access_list6', 'as_path_list', 'community_list', -                            'extcommunity_list', 'large_community_list', 'prefix_list', 'route_map']: +        for policy_type in ['access_list', 'access_list6', 'as_path_list', +                            'community_list', +                            'extcommunity_list', 'large_community_list', +                            'prefix_list', 'route_map']:              if policy_type in policy: -                for policy_name in list(set(routing_policy_find(policy_type, policy['protocols']))): +                for policy_name in list(set(routing_policy_find(policy_type, +                                                                policy[ +                                                                    'protocols']))):                      found = False                      if 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']: +                    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!') +                        tmp = policy_type.replace('_', '-') +                        raise ConfigError( +                            f'Can not delete {tmp} "{policy_name}", still in use!')      return None +  def generate(policy):      if not policy:          return None      policy['new_frr_config'] = render_to_string('frr/policy.frr.j2', policy)      return None +  def apply(policy):      bgp_daemon = 'bgpd'      zebra_daemon = 'zebra' @@ -203,7 +286,8 @@ def apply(policy):      frr_cfg.modify_section(r'^bgp community-list .*')      frr_cfg.modify_section(r'^bgp extcommunity-list .*')      frr_cfg.modify_section(r'^bgp large-community-list .*') -    frr_cfg.modify_section(r'^route-map .*', stop_pattern='^exit', remove_stop_mark=True) +    frr_cfg.modify_section(r'^route-map .*', stop_pattern='^exit', +                           remove_stop_mark=True)      if 'new_frr_config' in policy:          frr_cfg.add_before(frr.default_add_before, policy['new_frr_config'])      frr_cfg.commit_configuration(bgp_daemon) @@ -214,13 +298,15 @@ def apply(policy):      frr_cfg.modify_section(r'^ipv6 access-list .*')      frr_cfg.modify_section(r'^ip prefix-list .*')      frr_cfg.modify_section(r'^ipv6 prefix-list .*') -    frr_cfg.modify_section(r'^route-map .*', stop_pattern='^exit', remove_stop_mark=True) +    frr_cfg.modify_section(r'^route-map .*', stop_pattern='^exit', +                           remove_stop_mark=True)      if 'new_frr_config' in policy:          frr_cfg.add_before(frr.default_add_before, policy['new_frr_config'])      frr_cfg.commit_configuration(zebra_daemon)      return None +  if __name__ == '__main__':      try:          c = get_config() diff --git a/src/migration-scripts/policy/3-to-4 b/src/migration-scripts/policy/3-to-4 new file mode 100755 index 000000000..bae30cffc --- /dev/null +++ b/src/migration-scripts/policy/3-to-4 @@ -0,0 +1,162 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2022 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program.  If not, see <http://www.gnu.org/licenses/>. + +# T4660: change cli +#     from: set policy route-map FOO rule 10 set community 'TEXT' +#     Multiple value +#     to: set policy route-map FOO rule 10 set community replace <community> +#     Multiple value +#     to: set policy route-map FOO rule 10 set community add <community> +#     to: set policy route-map FOO rule 10 set community none +# +#     from: set policy route-map FOO rule 10 set large-community 'TEXT' +#     Multiple value +#     to: set policy route-map FOO rule 10 set large-community replace <community> +#     Multiple value +#     to: set policy route-map FOO rule 10 set large-community add <community> +#     to: set policy route-map FOO rule 10 set large-community none +# +#     from: set policy route-map FOO rule 10 set extecommunity [rt|soo] 'TEXT' +#     Multiple value +#     to: set policy route-map FOO rule 10 set extcommunity [rt|soo] <community> + +from sys import argv +from sys import exit + +from vyos.configtree import ConfigTree + + +# Migration function for large and regular communities +def community_migrate(config: ConfigTree, rule: list[str]) -> bool: +    """ + +    :param config: configuration object +    :type config: ConfigTree +    :param rule: Path to variable +    :type rule: list[str] +    :return: True if additive presents in community string +    :rtype: bool +    """ +    community_list = list((config.return_value(rule)).split(" ")) +    config.delete(rule) +    if 'none' in community_list: +        config.set(rule + ['none']) +        return False +    else: +        community_action: str = 'replace' +        if 'additive' in community_list: +            community_action = 'add' +            community_list.remove('additive') +        for community in community_list: +            config.set(rule + [community_action], value=community, +                       replace=False) +        if community_action == 'replace': +            return False +        else: +            return True + + +# Migration function for extcommunities +def extcommunity_migrate(config: ConfigTree, rule: list[str]) -> None: +    """ + +    :param config: configuration object +    :type config: ConfigTree +    :param rule: Path to variable +    :type rule: list[str] +    """ +    # if config.exists(rule + ['bandwidth']): +    #     bandwidth: str = config.return_value(rule + ['bandwidth']) +    #     config.delete(rule + ['bandwidth']) +    #     config.set(rule + ['bandwidth'], value=bandwidth) + +    if config.exists(rule + ['rt']): +        community_list = list((config.return_value(rule + ['rt'])).split(" ")) +        config.delete(rule + ['rt']) +        for community in community_list: +            config.set(rule + ['rt'], value=community, replace=False) + +    if config.exists(rule + ['soo']): +        community_list = list((config.return_value(rule + ['soo'])).split(" ")) +        config.delete(rule + ['soo']) +        for community in community_list: +            config.set(rule + ['soo'], value=community, replace=False) + + +if (len(argv) < 1): +    print("Must specify file name!") +    exit(1) + +file_name: str = argv[1] + +with open(file_name, 'r') as f: +    config_file = f.read() + +base: list[str] = ['policy', 'route-map'] +config = ConfigTree(config_file) + +if not config.exists(base): +    # Nothing to do +    exit(0) + +for route_map in config.list_nodes(base): +    if not config.exists(base + [route_map, 'rule']): +        continue +    for rule in config.list_nodes(base + [route_map, 'rule']): +        base_rule: list[str] = base + [route_map, 'rule', rule, 'set'] + +        # IF additive presents in coummunity then comm-list is redundant +        isAdditive: bool = True +        #### Change Set community ######## +        if config.exists(base_rule + ['community']): +            isAdditive = community_migrate(config, +                                           base_rule + ['community']) + +        #### Change Set community-list delete migrate ######## +        if config.exists(base_rule + ['comm-list', 'comm-list']): +            if isAdditive: +                tmp = config.return_value( +                    base_rule + ['comm-list', 'comm-list']) +                config.delete(base_rule + ['comm-list']) +                config.set(base_rule + ['community', 'delete'], value=tmp) +            else: +                config.delete(base_rule + ['comm-list']) + +        isAdditive = False +        #### Change Set large-community ######## +        if config.exists(base_rule + ['large-community']): +            isAdditive = community_migrate(config, +                                           base_rule + ['large-community']) + +        #### Change Set large-community delete by List ######## +        if config.exists(base_rule + ['large-comm-list-delete']): +            if isAdditive: +                tmp = config.return_value( +                    base_rule + ['large-comm-list-delete']) +                config.delete(base_rule + ['large-comm-list-delete']) +                config.set(base_rule + ['large-community', 'delete'], +                           value=tmp) +            else: +                config.delete(base_rule + ['large-comm-list-delete']) + +        #### Change Set extcommunity ######## +        extcommunity_migrate(config, base_rule + ['extcommunity']) +try: +    with open(file_name, 'w') as f: +        f.write(config.to_string()) +except OSError as e: +    print(f'Failed to save the modified config: {e}') +    exit(1) diff --git a/src/validators/bgp-extended-community b/src/validators/bgp-extended-community new file mode 100755 index 000000000..b69ae3449 --- /dev/null +++ b/src/validators/bgp-extended-community @@ -0,0 +1,55 @@ +#!/usr/bin/env python3 + +# Copyright 2019-2022 VyOS maintainers and contributors <maintainers@vyos.io> +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library.  If not, see <http://www.gnu.org/licenses/>. + +from argparse import ArgumentParser +from sys import exit + +from vyos.template import is_ipv4 + +COMM_MAX_2_OCTET: int = 65535 +COMM_MAX_4_OCTET: int = 4294967295 + +if __name__ == '__main__': +    # add an argument with community +    parser: ArgumentParser = ArgumentParser() +    parser.add_argument('community', type=str) +    args = parser.parse_args() +    community: str = args.community +    if community.count(':') != 1: +        print("Invalid community format") +        exit(1) +    try: +        # try to extract community parts from an argument +        comm_left: str = community.split(':')[0] +        comm_right: int = int(community.split(':')[1]) + +        # check if left part is an IPv4 address +        if is_ipv4(comm_left) and 0 <= comm_right <= COMM_MAX_2_OCTET: +            exit() +        # check if a left part is a number +        if 0 <= int(comm_left) <= COMM_MAX_2_OCTET \ +                and 0 <= comm_right <= COMM_MAX_4_OCTET: +            exit() + +    except Exception: +        # fail if something was wrong +        print("Invalid community format") +        exit(1) + +    # fail if none of validators catched the value +    print("Invalid community format") +    exit(1)
\ No newline at end of file diff --git a/src/validators/bgp-large-community b/src/validators/bgp-large-community new file mode 100755 index 000000000..386398308 --- /dev/null +++ b/src/validators/bgp-large-community @@ -0,0 +1,53 @@ +#!/usr/bin/env python3 + +# Copyright 2019-2022 VyOS maintainers and contributors <maintainers@vyos.io> +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library.  If not, see <http://www.gnu.org/licenses/>. + +from argparse import ArgumentParser +from sys import exit + +from vyos.template import is_ipv4 + +COMM_MAX_4_OCTET: int = 4294967295 + +if __name__ == '__main__': +    # add an argument with community +    parser: ArgumentParser = ArgumentParser() +    parser.add_argument('community', type=str) +    args = parser.parse_args() +    community: str = args.community +    if community.count(':') != 2: +        print("Invalid community format") +        exit(1) +    try: +        # try to extract community parts from an argument +        comm_part1: int = int(community.split(':')[0]) +        comm_part2: int = int(community.split(':')[1]) +        comm_part3: int = int(community.split(':')[2]) + +        # check compatibilities of left and right parts +        if 0 <= comm_part1 <= COMM_MAX_4_OCTET \ +                and 0 <= comm_part2 <= COMM_MAX_4_OCTET \ +                and 0 <= comm_part3 <= COMM_MAX_4_OCTET: +            exit(0) + +    except Exception: +        # fail if something was wrong +        print("Invalid community format") +        exit(1) + +    # fail if none of validators catched the value +    print("Invalid community format") +    exit(1)
\ No newline at end of file diff --git a/src/validators/bgp-regular-community b/src/validators/bgp-regular-community new file mode 100755 index 000000000..d43a71eae --- /dev/null +++ b/src/validators/bgp-regular-community @@ -0,0 +1,50 @@ +#!/usr/bin/env python3 + +# Copyright 2019-2022 VyOS maintainers and contributors <maintainers@vyos.io> +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library.  If not, see <http://www.gnu.org/licenses/>. + +from argparse import ArgumentParser +from sys import exit + +from vyos.template import is_ipv4 + +COMM_MAX_2_OCTET: int = 65535 + +if __name__ == '__main__': +    # add an argument with community +    parser: ArgumentParser = ArgumentParser() +    parser.add_argument('community', type=str) +    args = parser.parse_args() +    community: str = args.community +    if community.count(':') != 1: +        print("Invalid community format") +        exit(1) +    try: +        # try to extract community parts from an argument +        comm_left: int = int(community.split(':')[0]) +        comm_right: int = int(community.split(':')[1]) + +        # check compatibilities of left and right parts +        if 0 <= comm_left <= COMM_MAX_2_OCTET \ +                and 0 <= comm_right <= COMM_MAX_2_OCTET: +            exit(0) +    except Exception: +        # fail if something was wrong +        print("Invalid community format") +        exit(1) + +    # fail if none of validators catched the value +    print("Invalid community format") +    exit(1)
\ No newline at end of file | 
