From ad801bfb81633812b4aa25f45bdd555a27121845 Mon Sep 17 00:00:00 2001 From: Dmytro Date: Thu, 13 May 2021 04:57:27 +0300 Subject: [PATCH] [config]Static routes to config_db (#1534) * Write static routes to config_db * Update configuration with "ifname" as "null" Signed-off-by: d-dashkov --- config/main.py | 286 +++++++++++++++++++--------- doc/Command-Reference.md | 78 ++++++++ tests/static_routes_test.py | 365 ++++++++++++++++++++++++++++++++++++ 3 files changed, 641 insertions(+), 88 deletions(-) mode change 100644 => 100755 config/main.py create mode 100644 tests/static_routes_test.py diff --git a/config/main.py b/config/main.py old mode 100644 new mode 100755 index cdf8b14b6e3b..3daf93c73d61 --- a/config/main.py +++ b/config/main.py @@ -11,6 +11,7 @@ import subprocess import sys import time +import itertools from generic_config_updater.generic_updater import GenericUpdater, ConfigFormat from socket import AF_INET, AF_INET6 @@ -761,6 +762,66 @@ def validate_mirror_session_config(config_db, session_name, dst_port, src_port, return True +def cli_sroute_to_config(ctx, command_str, strict_nh = True): + if len(command_str) < 2 or len(command_str) > 9: + ctx.fail("argument is not in pattern prefix [vrf ] nexthop <[vrf ] >|>!") + if "prefix" not in command_str: + ctx.fail("argument is incomplete, prefix not found!") + if "nexthop" not in command_str and strict_nh: + ctx.fail("argument is incomplete, nexthop not found!") + + nexthop_str = None + config_entry = {} + vrf_name = "" + + if "nexthop" in command_str: + idx = command_str.index("nexthop") + prefix_str = command_str[:idx] + nexthop_str = command_str[idx:] + else: + prefix_str = command_str[:] + + if prefix_str: + if 'prefix' in prefix_str and 'vrf' in prefix_str: + # prefix_str: ['prefix', 'vrf', Vrf-name, ip] + vrf_name = prefix_str[2] + ip_prefix = prefix_str[3] + elif 'prefix' in prefix_str: + # prefix_str: ['prefix', ip] + ip_prefix = prefix_str[1] + else: + ctx.fail("prefix is not in pattern!") + + if nexthop_str: + if 'nexthop' in nexthop_str and 'vrf' in nexthop_str: + # nexthop_str: ['nexthop', 'vrf', Vrf-name, ip] + config_entry["nexthop"] = nexthop_str[3] + config_entry["nexthop-vrf"] = nexthop_str[2] + elif 'nexthop' in nexthop_str and 'dev' in nexthop_str: + # nexthop_str: ['nexthop', 'dev', ifname] + config_entry["ifname"] = nexthop_str[2] + elif 'nexthop' in nexthop_str: + # nexthop_str: ['nexthop', ip] + config_entry["nexthop"] = nexthop_str[1] + else: + ctx.fail("nexthop is not in pattern!") + + try: + ipaddress.ip_network(ip_prefix) + if 'nexthop' in config_entry: + nh = config_entry['nexthop'].split(',') + for ip in nh: + ipaddress.ip_address(ip) + except ValueError: + ctx.fail("ip address is not valid.") + + if not vrf_name == "": + key = vrf_name + "|" + ip_prefix + else: + key = ip_prefix + + return key, config_entry + def update_sonic_environment(): """Prepare sonic environment variable using SONiC environment template file. """ @@ -3932,111 +3993,160 @@ def del_vrf_vni_map(ctx, vrfname): @click.pass_context def route(ctx): """route-related configuration tasks""" - pass + config_db = ConfigDBConnector() + config_db.connect() + ctx.obj = {} + ctx.obj['config_db'] = config_db @route.command('add', context_settings={"ignore_unknown_options":True}) @click.argument('command_str', metavar='prefix [vrf ] nexthop <[vrf ] >|>', nargs=-1, type=click.Path()) @click.pass_context def add_route(ctx, command_str): """Add route command""" - if len(command_str) < 4 or len(command_str) > 9: - ctx.fail("argument is not in pattern prefix [vrf ] nexthop <[vrf ] >|>!") - if "prefix" not in command_str: - ctx.fail("argument is incomplete, prefix not found!") - if "nexthop" not in command_str: - ctx.fail("argument is incomplete, nexthop not found!") - for i in range(0, len(command_str)): - if "nexthop" == command_str[i]: - prefix_str = command_str[:i] - nexthop_str = command_str[i:] - vrf_name = "" - cmd = 'sudo vtysh -c "configure terminal" -c "ip route' - if prefix_str: - if len(prefix_str) == 2: - prefix_mask = prefix_str[1] - cmd += ' {}'.format(prefix_mask) - elif len(prefix_str) == 4: - vrf_name = prefix_str[2] - prefix_mask = prefix_str[3] - cmd += ' {}'.format(prefix_mask) + config_db = ctx.obj['config_db'] + key, route = cli_sroute_to_config(ctx, command_str) + + # If defined intf name, check if it belongs to interface + if 'ifname' in route: + if (not route['ifname'] in config_db.get_keys('VLAN_INTERFACE') and + not route['ifname'] in config_db.get_keys('INTERFACE') and + not route['ifname'] in config_db.get_keys('PORTCHANNEL_INTERFACE') and + not route['ifname'] == 'null'): + ctx.fail('interface {} doesn`t exist'.format(route['ifname'])) + + entry_counter = 1 + if 'nexthop' in route: + entry_counter = len(route['nexthop'].split(',')) + + # Alignment in case the command contains several nexthop ip + for i in range(entry_counter): + if 'nexthop-vrf' in route: + if i > 0: + vrf = route['nexthop-vrf'].split(',')[0] + route['nexthop-vrf'] += ',' + vrf else: - ctx.fail("prefix is not in pattern!") - if nexthop_str: - if len(nexthop_str) == 2: - ip = nexthop_str[1] - if vrf_name == "": - cmd += ' {}'.format(ip) - else: - cmd += ' {} vrf {}'.format(ip, vrf_name) - elif len(nexthop_str) == 3: - dev_name = nexthop_str[2] - if vrf_name == "": - cmd += ' {}'.format(dev_name) + route['nexthop-vrf'] = '' + + if not 'nexthop' in route: + route['nexthop'] = '' + + if 'ifname' in route: + if i > 0: + route['ifname'] += ',' + else: + route['ifname'] = '' + + # Set default values for distance and blackhole because the command doesn't have such an option + if 'distance' in route: + route['distance'] += ',0' + else: + route['distance'] = '0' + + if 'blackhole' in route: + route['blackhole'] += ',false' + else: + # If the user configure with "ifname" as "null", set 'blackhole' attribute as true. + if 'ifname' in route and route['ifname'] == 'null': + route['blackhole'] = 'true' else: - cmd += ' {} vrf {}'.format(dev_name, vrf_name) - elif len(nexthop_str) == 4: - vrf_name_dst = nexthop_str[2] - ip = nexthop_str[3] - if vrf_name == "": - cmd += ' {} nexthop-vrf {}'.format(ip, vrf_name_dst) + route['blackhole'] = 'false' + + # Check if exist entry with key + keys = config_db.get_keys('STATIC_ROUTE') + if key in keys: + # If exist update current entry + current_entry = config_db.get_entry('STATIC_ROUTE', key) + + for entry in ['nexthop', 'nexthop-vrf', 'ifname', 'distance', 'blackhole']: + if not entry in current_entry: + current_entry[entry] = '' + if entry in route: + current_entry[entry] += ',' + route[entry] else: - cmd += ' {} vrf {} nexthop-vrf {}'.format(ip, vrf_name, vrf_name_dst) - else: - ctx.fail("nexthop is not in pattern!") - cmd += '"' - clicommon.run_command(cmd) + current_entry[entry] += ',' + + config_db.set_entry("STATIC_ROUTE", key, current_entry) + else: + config_db.set_entry("STATIC_ROUTE", key, route) @route.command('del', context_settings={"ignore_unknown_options":True}) @click.argument('command_str', metavar='prefix [vrf ] nexthop <[vrf ] >|>', nargs=-1, type=click.Path()) @click.pass_context def del_route(ctx, command_str): """Del route command""" - if len(command_str) < 4 or len(command_str) > 9: - ctx.fail("argument is not in pattern prefix [vrf ] nexthop <[vrf ] >|>!") - if "prefix" not in command_str: - ctx.fail("argument is incomplete, prefix not found!") - if "nexthop" not in command_str: - ctx.fail("argument is incomplete, nexthop not found!") - for i in range(0, len(command_str)): - if "nexthop" == command_str[i]: - prefix_str = command_str[:i] - nexthop_str = command_str[i:] - vrf_name = "" - cmd = 'sudo vtysh -c "configure terminal" -c "no ip route' - if prefix_str: - if len(prefix_str) == 2: - prefix_mask = prefix_str[1] - cmd += ' {}'.format(prefix_mask) - elif len(prefix_str) == 4: - vrf_name = prefix_str[2] - prefix_mask = prefix_str[3] - cmd += ' {}'.format(prefix_mask) - else: - ctx.fail("prefix is not in pattern!") - if nexthop_str: - if len(nexthop_str) == 2: - ip = nexthop_str[1] - if vrf_name == "": - cmd += ' {}'.format(ip) - else: - cmd += ' {} vrf {}'.format(ip, vrf_name) - elif len(nexthop_str) == 3: - dev_name = nexthop_str[2] - if vrf_name == "": - cmd += ' {}'.format(dev_name) - else: - cmd += ' {} vrf {}'.format(dev_name, vrf_name) - elif len(nexthop_str) == 4: - vrf_name_dst = nexthop_str[2] - ip = nexthop_str[3] - if vrf_name == "": - cmd += ' {} nexthop-vrf {}'.format(ip, vrf_name_dst) + config_db = ctx.obj['config_db'] + key, route = cli_sroute_to_config(ctx, command_str, strict_nh=False) + keys = config_db.get_keys('STATIC_ROUTE') + prefix_tuple = tuple(key.split('|')) + if not key in keys and not prefix_tuple in keys: + ctx.fail('Route {} doesnt exist'.format(key)) + else: + # If not defined nexthop or intf name remove entire route + if not 'nexthop' in route and not 'ifname' in route: + config_db.set_entry("STATIC_ROUTE", key, None) + return + + current_entry = config_db.get_entry('STATIC_ROUTE', key) + + nh = [''] + nh_vrf = [''] + ifname = [''] + distance = [''] + blackhole = [''] + if 'nexthop' in current_entry: + nh = current_entry['nexthop'].split(',') + if 'nexthop-vrf' in current_entry: + nh_vrf = current_entry['nexthop-vrf'].split(',') + if 'ifname' in current_entry: + ifname = current_entry['ifname'].split(',') + if 'distance' in current_entry: + distance = current_entry['distance'].split(',') + if 'blackhole' in current_entry: + blackhole = current_entry['blackhole'].split(',') + + # Zip data from config_db into tuples + # {'nexthop': '10.0.0.2,20.0.0.2', 'vrf-nexthop': ',Vrf-RED', 'ifname': ','} + # [('10.0.0.2', '', ''), ('20.0.0.2', 'Vrf-RED', '')] + nh_zip = list(itertools.zip_longest(nh, nh_vrf, ifname, fillvalue='')) + cli_tuple = () + + # Create tuple from CLI argument + # config route add prefix 1.4.3.4/32 nexthop vrf Vrf-RED 20.0.0.2 + # ('20.0.0.2', 'Vrf-RED', '') + for entry in ['nexthop', 'nexthop-vrf', 'ifname']: + if entry in route: + cli_tuple += (route[entry],) else: - cmd += ' {} vrf {} nexthop-vrf {}'.format(ip, vrf_name, vrf_name_dst) + cli_tuple += ('',) + + if cli_tuple in nh_zip: + # If cli tuple is in config_db find its index and delete from lists + idx = nh_zip.index(cli_tuple) + if len(nh) - 1 >= idx: + del nh[idx] + if len(nh_vrf) - 1 >= idx: + del nh_vrf[idx] + if len(ifname) - 1 >= idx: + del ifname[idx] + if len(distance) - 1 >= idx: + del distance[idx] + if len(blackhole) - 1 >= idx: + del blackhole[idx] else: - ctx.fail("nexthop is not in pattern!") - cmd += '"' - clicommon.run_command(cmd) + ctx.fail('Not found {} in {}'.format(cli_tuple, key)) + + if (len(nh) == 0 or (len(nh) == 1 and nh[0] == '')) and \ + (len(ifname) == 0 or (len(ifname) == 1 and ifname[0] == '')): + # If there are no nexthop and ifname fields in the current record, delete it + config_db.set_entry("STATIC_ROUTE", key, None) + else: + # Otherwise it still has ECMP nexthop or ifname fields, so compose it from the lists into db + current_entry['nexthop'] = ','.join((str(e)) for e in nh) + current_entry['nexthop-vrf'] = ','.join((str(e)) for e in nh_vrf) + current_entry['ifname'] = ','.join((str(e)) for e in ifname) + current_entry['distance'] = ','.join((str(e)) for e in distance) + current_entry['blackhole'] = ','.join((str(e)) for e in blackhole) + config_db.set_entry("STATIC_ROUTE", key, current_entry) # # 'acl' group ('config acl ...') diff --git a/doc/Command-Reference.md b/doc/Command-Reference.md index c72cc10f350f..8a247d40e38e 100644 --- a/doc/Command-Reference.md +++ b/doc/Command-Reference.md @@ -121,6 +121,7 @@ * [Startup & Running Configuration](#startup--running-configuration) * [Startup Configuration](#startup-configuration) * [Running Configuration](#running-configuration) +* [Static routing](#static-routing) * [Syslog](#syslog) * [Syslog config commands](#syslog-config-commands) * [System State](#system-state) @@ -7184,6 +7185,83 @@ This command displays the running configuration of the snmp module. Go Back To [Beginning of the document](#) or [Beginning of this section](#Startup--Running-Configuration) +## Static routing + +### Static routing Config Commands + +This sub-section explains of commands is used to add or remove the static route. + +**config route add** + +This command is used to add a static route. Note that prefix /nexthop vrf`s and interface name are optional. + +- Usage: + + ``` + config route add prefix [vrf ] nexthop [vrf ] dev + ``` + +- Example: + + ``` + admin@sonic:~$ config route add prefix 2.2.3.4/32 nexthop 30.0.0.9 + ``` + +It also supports ECMP, and adding a new nexthop to the existing prefix will complement it and not overwrite them. + +- Example: + + ``` + admin@sonic:~$ sudo config route add prefix 2.2.3.4/32 nexthop vrf Vrf-RED 30.0.0.9 + admin@sonic:~$ sudo config route add prefix 2.2.3.4/32 nexthop vrf Vrf-BLUE 30.0.0.10 + ``` + +**config route del** + +This command is used to remove a static route. Note that prefix /nexthop vrf`s and interface name are optional. + +- Usage: + + ``` + config route del prefix [vrf ] nexthop [vrf ] dev + ``` + +- Example: + + ``` + admin@sonic:~$ sudo config route del prefix 2.2.3.4/32 nexthop vrf Vrf-RED 30.0.0.9 + admin@sonic:~$ sudo config route del prefix 2.2.3.4/32 nexthop vrf Vrf-BLUE 30.0.0.10 + ``` + +This sub-section explains of command is used to show current routes. + +**show ip route** + +- Usage: + + ``` + show ip route + ``` + +- Example: + + ``` + admin@sonic:~$ show ip route + Codes: K - kernel route, C - connected, S - static, R - RIP, + O - OSPF, I - IS-IS, B - BGP, E - EIGRP, N - NHRP, + T - Table, v - VNC, V - VNC-Direct, A - Babel, D - SHARP, + F - PBR, f - OpenFabric, + > - selected route, * - FIB route, q - queued, r - rejected, b - backup + + S>* 0.0.0.0/0 [200/0] via 192.168.111.3, eth0, weight 1, 3d03h58m + S> 1.2.3.4/32 [1/0] via 30.0.0.7, weight 1, 00:00:06 + C>* 10.0.0.18/31 is directly connected, Ethernet36, 3d03h57m + C>* 10.0.0.20/31 is directly connected, Ethernet40, 3d03h57m + ``` + +Go Back To [Beginning of the document](#) or [Beginning of this section](#static-routing) + + ## Syslog ### Syslog Config Commands diff --git a/tests/static_routes_test.py b/tests/static_routes_test.py new file mode 100644 index 000000000000..c354cb97c449 --- /dev/null +++ b/tests/static_routes_test.py @@ -0,0 +1,365 @@ +import os +import traceback + +from click.testing import CliRunner + +import config.main as config +import show.main as show +from utilities_common.db import Db + +ERROR_STR = ''' +Error: argument is not in pattern prefix [vrf ] nexthop <[vrf ] >|>! +''' +ERROR_STR_MISS_PREFIX = ''' +Error: argument is incomplete, prefix not found! +''' +ERROR_STR_MISS_NEXTHOP = ''' +Error: argument is incomplete, nexthop not found! +''' +ERROR_DEL_NONEXIST_KEY_STR = ''' +Error: Route {} doesnt exist +''' +ERROR_DEL_NONEXIST_ENTRY_STR = ''' +Error: Not found {} in {} +''' +ERROR_INVALID_IP = ''' +Error: ip address is not valid. +''' + + +class TestStaticRoutes(object): + @classmethod + def setup_class(cls): + os.environ['UTILITIES_UNIT_TESTING'] = "1" + print("SETUP") + + def test_simple_static_route(self): + db = Db() + runner = CliRunner() + obj = {'config_db':db.cfgdb} + + # config route add prefix 1.2.3.4/32 nexthop 30.0.0.5 + result = runner.invoke(config.config.commands["route"].commands["add"], \ + ["prefix", "1.2.3.4/32", "nexthop", "30.0.0.5"], obj=obj) + print(result.exit_code, result.output) + assert ('1.2.3.4/32') in db.cfgdb.get_table('STATIC_ROUTE') + assert db.cfgdb.get_entry('STATIC_ROUTE', '1.2.3.4/32') == {'nexthop': '30.0.0.5', 'blackhole': 'false', 'distance': '0', 'ifname': '', 'nexthop-vrf': ''} + + # config route del prefix 1.2.3.4/32 nexthop 30.0.0.5 + result = runner.invoke(config.config.commands["route"].commands["del"], \ + ["prefix", "1.2.3.4/32", "nexthop", "30.0.0.5"], obj=obj) + print(result.exit_code, result.output) + assert not '1.2.3.4/32' in db.cfgdb.get_table('STATIC_ROUTE') + + def test_static_route_invalid_prefix_ip(self): + db = Db() + runner = CliRunner() + obj = {'config_db':db.cfgdb} + + # config route add prefix 1.2.3/32 nexthop 30.0.0.5 + result = runner.invoke(config.config.commands["route"].commands["add"], \ + ["prefix", "1.2.3/32", "nexthop", "30.0.0.5"], obj=obj) + print(result.exit_code, result.output) + assert ERROR_INVALID_IP in result.output + + def test_static_route_invalid_nexthop_ip(self): + db = Db() + runner = CliRunner() + obj = {'config_db':db.cfgdb} + + # config route add prefix 1.2.3.4/32 nexthop 30.0.5 + result = runner.invoke(config.config.commands["route"].commands["add"], \ + ["prefix", "1.2.3.4/32", "nexthop", "30.0.5"], obj=obj) + print(result.exit_code, result.output) + assert ERROR_INVALID_IP in result.output + + def test_vrf_static_route(self): + db = Db() + runner = CliRunner() + obj = {'config_db':db.cfgdb} + + # config route add prefix vrf Vrf-BLUE 2.2.3.4/32 nexthop 30.0.0.6 + result = runner.invoke(config.config.commands["route"].commands["add"], \ + ["prefix", "vrf", "Vrf-BLUE", "2.2.3.4/32", "nexthop", "30.0.0.6"], obj=obj) + print(result.exit_code, result.output) + assert ('Vrf-BLUE', '2.2.3.4/32') in db.cfgdb.get_table('STATIC_ROUTE') + assert db.cfgdb.get_entry('STATIC_ROUTE', 'Vrf-BLUE|2.2.3.4/32') == {'nexthop': '30.0.0.6', 'blackhole': 'false', 'distance': '0', 'ifname': '', 'nexthop-vrf': ''} + + # config route del prefix vrf Vrf-BLUE 2.2.3.4/32 nexthop 30.0.0.6 + result = runner.invoke(config.config.commands["route"].commands["del"], \ + ["prefix", "vrf", "Vrf-BLUE", "2.2.3.4/32", "nexthop", "30.0.0.6"], obj=obj) + print(result.exit_code, result.output) + assert not ('Vrf-BLUE', '2.2.3.4/32') in db.cfgdb.get_table('STATIC_ROUTE') + + def test_dest_vrf_static_route(self): + db = Db() + runner = CliRunner() + obj = {'config_db':db.cfgdb} + + # config route add prefix 3.2.3.4/32 nexthop vrf Vrf-RED 30.0.0.6 + result = runner.invoke(config.config.commands["route"].commands["add"], \ + ["prefix", "3.2.3.4/32", "nexthop", "vrf", "Vrf-RED", "30.0.0.6"], obj=obj) + print(result.exit_code, result.output) + assert ('3.2.3.4/32') in db.cfgdb.get_table('STATIC_ROUTE') + assert db.cfgdb.get_entry('STATIC_ROUTE', '3.2.3.4/32') == {'nexthop': '30.0.0.6', 'nexthop-vrf': 'Vrf-RED', 'blackhole': 'false', 'distance': '0', 'ifname': ''} + + # config route del prefix 3.2.3.4/32 nexthop vrf Vrf-RED 30.0.0.6 + result = runner.invoke(config.config.commands["route"].commands["del"], \ + ["prefix", "3.2.3.4/32", "nexthop", "vrf", "Vrf-RED", "30.0.0.6"], obj=obj) + print(result.exit_code, result.output) + assert not ('3.2.3.4/32') in db.cfgdb.get_table('STATIC_ROUTE') + + def test_multiple_nexthops_with_vrf_static_route(self): + db = Db() + runner = CliRunner() + obj = {'config_db':db.cfgdb} + + ''' Add ''' + # config route add prefix 6.2.3.4/32 nexthop vrf Vrf-RED "30.0.0.6,30.0.0.7" + result = runner.invoke(config.config.commands["route"].commands["add"], \ + ["prefix", "6.2.3.4/32", "nexthop", "vrf", "Vrf-RED", "30.0.0.6,30.0.0.7"], obj=obj) + print(result.exit_code, result.output) + assert ('6.2.3.4/32') in db.cfgdb.get_table('STATIC_ROUTE') + assert db.cfgdb.get_entry('STATIC_ROUTE', '6.2.3.4/32') == {'nexthop': '30.0.0.6,30.0.0.7', 'blackhole': 'false,false', 'distance': '0,0', 'ifname': ',', 'nexthop-vrf': 'Vrf-RED,Vrf-RED'} + + ''' Del ''' + # config route del prefix 6.2.3.4/32 nexthop vrf Vrf-RED 30.0.0.7 + result = runner.invoke(config.config.commands["route"].commands["del"], \ + ["prefix", "6.2.3.4/32", "nexthop", "vrf", "Vrf-RED", "30.0.0.7"], obj=obj) + print(result.exit_code, result.output) + assert ('6.2.3.4/32') in db.cfgdb.get_table('STATIC_ROUTE') + assert db.cfgdb.get_entry('STATIC_ROUTE', '6.2.3.4/32') == {'nexthop': '30.0.0.6', 'blackhole': 'false', 'distance': '0', 'ifname': '', 'nexthop-vrf': 'Vrf-RED'} + + # config route del prefix 6.2.3.4/32 nexthop vrf Vrf-RED 30.0.0.6 + result = runner.invoke(config.config.commands["route"].commands["del"], \ + ["prefix", "6.2.3.4/32", "nexthop", "vrf", "Vrf-RED", "30.0.0.6"], obj=obj) + print(result.exit_code, result.output) + assert not ('6.2.3.4/32') in db.cfgdb.get_table('STATIC_ROUTE') + + def test_multiple_nexthops_static_route(self): + db = Db() + runner = CliRunner() + obj = {'config_db':db.cfgdb} + + ''' Add ''' + # config route add prefix 6.2.3.4/32 nexthop "30.0.0.6,30.0.0.7" + result = runner.invoke(config.config.commands["route"].commands["add"], \ + ["prefix", "6.2.3.4/32", "nexthop", "30.0.0.6,30.0.0.7"], obj=obj) + print(result.exit_code, result.output) + assert ('6.2.3.4/32') in db.cfgdb.get_table('STATIC_ROUTE') + assert db.cfgdb.get_entry('STATIC_ROUTE', '6.2.3.4/32') == {'nexthop': '30.0.0.6,30.0.0.7', 'blackhole': 'false,false', 'distance': '0,0', 'ifname': ',', 'nexthop-vrf': ','} + + # config route add prefix 6.2.3.4/32 nexthop 30.0.0.8 + result = runner.invoke(config.config.commands["route"].commands["add"], \ + ["prefix", "6.2.3.4/32", "nexthop", "30.0.0.8"], obj=obj) + print(result.exit_code, result.output) + assert ('6.2.3.4/32') in db.cfgdb.get_table('STATIC_ROUTE') + assert db.cfgdb.get_entry('STATIC_ROUTE', '6.2.3.4/32') == {'nexthop': '30.0.0.6,30.0.0.7,30.0.0.8', 'blackhole': 'false,false,false', 'distance': '0,0,0', 'ifname': ',,', 'nexthop-vrf': ',,'} + + ''' Del ''' + # config route del prefix 6.2.3.4/32 nexthop 30.0.0.8 + result = runner.invoke(config.config.commands["route"].commands["del"], \ + ["prefix", "6.2.3.4/32", "nexthop", "30.0.0.8"], obj=obj) + print(result.exit_code, result.output) + assert ('6.2.3.4/32') in db.cfgdb.get_table('STATIC_ROUTE') + assert db.cfgdb.get_entry('STATIC_ROUTE', '6.2.3.4/32') == {"nexthop": '30.0.0.6,30.0.0.7', 'blackhole': 'false,false', 'distance': '0,0', 'ifname': ',', 'nexthop-vrf': ','} + + # config route del prefix 6.2.3.4/32 nexthop 30.0.0.7 + result = runner.invoke(config.config.commands["route"].commands["del"], \ + ["prefix", "6.2.3.4/32", "nexthop", "30.0.0.7"], obj=obj) + print(result.exit_code, result.output) + assert ('6.2.3.4/32') in db.cfgdb.get_table('STATIC_ROUTE') + assert db.cfgdb.get_entry('STATIC_ROUTE', '6.2.3.4/32') == {'nexthop': '30.0.0.6', 'blackhole': 'false', 'distance': '0', 'ifname': '', 'nexthop-vrf': ''} + + # config route del prefix 6.2.3.4/32 nexthop 30.0.0.6 + result = runner.invoke(config.config.commands["route"].commands["del"], \ + ["prefix", "6.2.3.4/32", "nexthop", "30.0.0.6"], obj=obj) + print(result.exit_code, result.output) + assert not ('6.2.3.4/32') in db.cfgdb.get_table('STATIC_ROUTE') + + def test_static_route_miss_prefix(self): + db = Db() + runner = CliRunner() + obj = {'config_db':db.cfgdb} + + # config route add nexthop 30.0.0.6 + result = runner.invoke(config.config.commands["route"].commands["add"], ["nexthop", "30.0.0.6"], obj=obj) + print(result.exit_code, result.output) + assert ERROR_STR_MISS_PREFIX in result.output + + def test_static_route_miss_nexthop(self): + db = Db() + runner = CliRunner() + obj = {'config_db':db.cfgdb} + + # config route add prefix 7.2.3.4/32 + result = runner.invoke(config.config.commands["route"].commands["add"], ["prefix", "7.2.3.4/32"], obj=obj) + print(result.exit_code, result.output) + assert ERROR_STR_MISS_NEXTHOP in result.output + + def test_static_route_ECMP_nexthop(self): + db = Db() + runner = CliRunner() + obj = {'config_db':db.cfgdb} + + ''' Add ''' + # config route add prefix 10.2.3.4/32 nexthop 30.0.0.5 + result = runner.invoke(config.config.commands["route"].commands["add"], \ + ["prefix", "10.2.3.4/32", "nexthop", "30.0.0.5"], obj=obj) + print(result.exit_code, result.output) + assert ('10.2.3.4/32') in db.cfgdb.get_table('STATIC_ROUTE') + assert db.cfgdb.get_entry('STATIC_ROUTE', '10.2.3.4/32') == {'nexthop': '30.0.0.5', 'blackhole': 'false', 'distance': '0', 'ifname': '', 'nexthop-vrf': ''} + + # config route add prefix 10.2.3.4/32 nexthop 30.0.0.6 + result = runner.invoke(config.config.commands["route"].commands["add"], \ + ["prefix", "10.2.3.4/32", "nexthop", "30.0.0.6"], obj=obj) + print(result.exit_code, result.output) + assert ('10.2.3.4/32') in db.cfgdb.get_table('STATIC_ROUTE') + assert db.cfgdb.get_entry('STATIC_ROUTE', '10.2.3.4/32') == {'nexthop': '30.0.0.5,30.0.0.6', 'blackhole': 'false,false', 'distance': '0,0', 'ifname': ',', 'nexthop-vrf': ','} + + ''' Del ''' + # config route del prefix 10.2.3.4/32 nexthop 30.0.0.5 + result = runner.invoke(config.config.commands["route"].commands["del"], \ + ["prefix", "10.2.3.4/32", "nexthop", "30.0.0.5"], obj=obj) + print(result.exit_code, result.output) + assert ('10.2.3.4/32') in db.cfgdb.get_table('STATIC_ROUTE') + assert db.cfgdb.get_entry('STATIC_ROUTE', '10.2.3.4/32') == {'nexthop': '30.0.0.6', 'blackhole': 'false', 'distance': '0', 'ifname': '', 'nexthop-vrf': ''} + + # config route del prefix 1.2.3.4/32 nexthop 30.0.0.6 + result = runner.invoke(config.config.commands["route"].commands["del"], \ + ["prefix", "10.2.3.4/32", "nexthop", "30.0.0.6"], obj=obj) + print(result.exit_code, result.output) + assert not ('10.2.3.4/32') in db.cfgdb.get_table('STATIC_ROUTE') + + def test_static_route_ECMP_nexthop_with_vrf(self): + db = Db() + runner = CliRunner() + obj = {'config_db':db.cfgdb} + + ''' Add ''' + # config route add prefix 11.2.3.4/32 nexthop vrf Vrf-RED 30.0.0.5 + result = runner.invoke(config.config.commands["route"].commands["add"], \ + ["prefix", "11.2.3.4/32", "nexthop", "vrf", "Vrf-RED", "30.0.0.5"], obj=obj) + print(result.exit_code, result.output) + assert ('11.2.3.4/32') in db.cfgdb.get_table('STATIC_ROUTE') + assert db.cfgdb.get_entry('STATIC_ROUTE', '11.2.3.4/32') == {'nexthop': '30.0.0.5', 'nexthop-vrf': 'Vrf-RED', 'blackhole': 'false', 'distance': '0', 'ifname': ''} + + # config route add prefix 11.2.3.4/32 nexthop vrf Vrf-BLUE 30.0.0.6 + result = runner.invoke(config.config.commands["route"].commands["add"], \ + ["prefix", "11.2.3.4/32", "nexthop", "vrf", "Vrf-BLUE", "30.0.0.6"], obj=obj) + print(result.exit_code, result.output) + assert ('11.2.3.4/32') in db.cfgdb.get_table('STATIC_ROUTE') + assert db.cfgdb.get_entry('STATIC_ROUTE', '11.2.3.4/32') == {"nexthop": "30.0.0.5,30.0.0.6", "nexthop-vrf": "Vrf-RED,Vrf-BLUE", 'blackhole': 'false,false', 'distance': '0,0', 'ifname': ','} + + ''' Del ''' + # config route del prefix 11.2.3.4/32 nexthop vrf Vrf-RED 30.0.0.5 + result = runner.invoke(config.config.commands["route"].commands["del"], \ + ["prefix", "11.2.3.4/32", "nexthop", "vrf", "Vrf-RED", "30.0.0.5"], obj=obj) + print(result.exit_code, result.output) + assert ('11.2.3.4/32') in db.cfgdb.get_table('STATIC_ROUTE') + assert db.cfgdb.get_entry('STATIC_ROUTE', '11.2.3.4/32') == {"nexthop": "30.0.0.6", "nexthop-vrf": "Vrf-BLUE", 'blackhole': 'false', 'distance': '0', 'ifname': ''} + + # config route del prefix 11.2.3.4/32 nexthop vrf Vrf-BLUE 30.0.0.6 + result = runner.invoke(config.config.commands["route"].commands["del"], \ + ["prefix", "11.2.3.4/32", "nexthop", "vrf", "Vrf-BLUE", "30.0.0.6"], obj=obj) + print(result.exit_code, result.output) + assert not ('11.2.3.4/32') in db.cfgdb.get_table('STATIC_ROUTE') + + def test_static_route_ECMP_mixed_nextfop(self): + db = Db() + runner = CliRunner() + obj = {'config_db':db.cfgdb} + + ''' Add ''' + # config route add prefix 12.2.3.4/32 nexthop 30.0.0.6 + result = runner.invoke(config.config.commands["route"].commands["add"], \ + ["prefix", "12.2.3.4/32", "nexthop", "30.0.0.6"], obj=obj) + print(result.exit_code, result.output) + assert ('12.2.3.4/32') in db.cfgdb.get_table('STATIC_ROUTE') + assert db.cfgdb.get_entry('STATIC_ROUTE', '12.2.3.4/32') == {'nexthop': '30.0.0.6', 'blackhole': 'false', 'distance': '0', 'ifname': '', 'nexthop-vrf': ''} + + # config route add prefix 12.2.3.4/32 nexthop vrf Vrf-RED 30.0.0.7 + result = runner.invoke(config.config.commands["route"].commands["add"], \ + ["prefix", "12.2.3.4/32", "nexthop", "vrf", "Vrf-RED", "30.0.0.7"], obj=obj) + print(result.exit_code, result.output) + assert ('12.2.3.4/32') in db.cfgdb.get_table('STATIC_ROUTE') + assert db.cfgdb.get_entry('STATIC_ROUTE', '12.2.3.4/32') == {'nexthop': '30.0.0.6,30.0.0.7', 'nexthop-vrf': ',Vrf-RED', 'blackhole': 'false,false', 'distance': '0,0', 'ifname': ','} + + ''' Del ''' + # config route del prefix 12.2.3.4/32 nexthop vrf Vrf-Red 30.0.0.7 + result = runner.invoke(config.config.commands["route"].commands["del"], \ + ["prefix", "12.2.3.4/32", "nexthop", "vrf", "Vrf-RED", "30.0.0.7"], obj=obj) + print(result.exit_code, result.output) + assert ('12.2.3.4/32') in db.cfgdb.get_table('STATIC_ROUTE') + assert db.cfgdb.get_entry('STATIC_ROUTE', '12.2.3.4/32') == {'nexthop': '30.0.0.6', 'nexthop-vrf': '', 'ifname': '', 'blackhole': 'false', 'distance': '0'} + + # config route del prefix 12.2.3.4/32 nexthop 30.0.0.6 + result = runner.invoke(config.config.commands["route"].commands["del"], \ + ["prefix", "12.2.3.4/32", "nexthop", "30.0.0.6"], obj=obj) + print(result.exit_code, result.output) + assert not ('12.2.3.4/32') in db.cfgdb.get_table('STATIC_ROUTE') + + def test_del_nonexist_key_static_route(self): + db = Db() + runner = CliRunner() + obj = {'config_db':db.cfgdb} + + # config route del prefix 10.2.3.4/32 nexthop 30.0.0.6 + result = runner.invoke(config.config.commands["route"].commands["del"], \ + ["prefix", "17.2.3.4/32", "nexthop", "30.0.0.6"], obj=obj) + print(result.exit_code, result.output) + assert ERROR_DEL_NONEXIST_KEY_STR.format("17.2.3.4/32") in result.output + + def test_del_nonexist_entry_static_route(self): + db = Db() + runner = CliRunner() + obj = {'config_db':db.cfgdb} + + # config route add prefix 13.2.3.4/32 nexthop 30.0.0.5 + result = runner.invoke(config.config.commands["route"].commands["add"], \ + ["prefix", "13.2.3.4/32", "nexthop", "30.0.0.5"], obj=obj) + print(result.exit_code, result.output) + assert ('13.2.3.4/32') in db.cfgdb.get_table('STATIC_ROUTE') + assert db.cfgdb.get_entry('STATIC_ROUTE', '13.2.3.4/32') == {'nexthop': '30.0.0.5', 'blackhole': 'false', 'distance': '0', 'ifname': '', 'nexthop-vrf': ''} + + # config route del prefix 13.2.3.4/32 nexthop 30.0.0.6 <- nh ip that doesnt exist + result = runner.invoke(config.config.commands["route"].commands["del"], \ + ["prefix", "13.2.3.4/32", "nexthop", "30.0.0.6"], obj=obj) + print(result.exit_code, result.output) + assert ERROR_DEL_NONEXIST_ENTRY_STR.format(('30.0.0.6', '', ''), "13.2.3.4/32") in result.output + + # config route del prefix 13.2.3.4/32 nexthop 30.0.0.5 + result = runner.invoke(config.config.commands["route"].commands["del"], \ + ["prefix", "13.2.3.4/32", "nexthop", "30.0.0.5"], obj=obj) + print(result.exit_code, result.output) + assert not '13.2.3.4/32' in db.cfgdb.get_table('STATIC_ROUTE') + + def test_del_entire_ECMP_static_route(self): + db = Db() + runner = CliRunner() + obj = {'config_db':db.cfgdb} + + # config route add prefix 14.2.3.4/32 nexthop 30.0.0.5 + result = runner.invoke(config.config.commands["route"].commands["add"], \ + ["prefix", "14.2.3.4/32", "nexthop", "30.0.0.5"], obj=obj) + print(result.exit_code, result.output) + assert ('14.2.3.4/32') in db.cfgdb.get_table('STATIC_ROUTE') + assert db.cfgdb.get_entry('STATIC_ROUTE', '14.2.3.4/32') == {'nexthop': '30.0.0.5', 'blackhole': 'false', 'distance': '0', 'ifname': '', 'nexthop-vrf': ''} + + # config route add prefix 14.2.3.4/32 nexthop 30.0.0.6 + result = runner.invoke(config.config.commands["route"].commands["add"], \ + ["prefix", "14.2.3.4/32", "nexthop", "30.0.0.6"], obj=obj) + print(result.exit_code, result.output) + assert ('14.2.3.4/32') in db.cfgdb.get_table('STATIC_ROUTE') + assert db.cfgdb.get_entry('STATIC_ROUTE', '14.2.3.4/32') == {'nexthop': '30.0.0.5,30.0.0.6', 'nexthop-vrf': ',', 'ifname': ',', 'blackhole': 'false,false', 'distance': '0,0'} + + # config route del prefix 14.2.3.4/32 + result = runner.invoke(config.config.commands["route"].commands["del"], ["prefix", "14.2.3.4/32"], obj=obj) + print(result.exit_code, result.output) + assert not '14.2.3.4/32' in db.cfgdb.get_table('STATIC_ROUTE') + + @classmethod + def teardown_class(cls): + os.environ['UTILITIES_UNIT_TESTING'] = "0" + print("TEARDOWN") +