From 481a878dadd80711a85f5f2c28e46ed2a4a9b0c9 Mon Sep 17 00:00:00 2001 From: Baorong Liu Date: Fri, 9 Jun 2023 16:27:42 -0700 Subject: [PATCH] double commit #13789 and #15269 from sonic-net/sonic-buildimage --- .../frr/supervisord/supervisord.conf.j2 | 14 + .../bgpcfgd/managers_static_rt.py | 59 +- src/sonic-bgpcfgd/setup.py | 1 + src/sonic-bgpcfgd/staticroutebfd/__init__.py | 0 src/sonic-bgpcfgd/staticroutebfd/main.py | 779 ++++++++++++++++++ src/sonic-bgpcfgd/staticroutebfd/vars.py | 5 + src/sonic-bgpcfgd/tests/test_static_rt.py | 110 ++- src/sonic-bgpcfgd/tests/test_static_rt_bfd.py | 605 ++++++++++++++ 8 files changed, 1567 insertions(+), 6 deletions(-) create mode 100644 src/sonic-bgpcfgd/staticroutebfd/__init__.py create mode 100644 src/sonic-bgpcfgd/staticroutebfd/main.py create mode 100644 src/sonic-bgpcfgd/staticroutebfd/vars.py create mode 100644 src/sonic-bgpcfgd/tests/test_static_rt_bfd.py diff --git a/dockers/docker-fpm-frr/frr/supervisord/supervisord.conf.j2 b/dockers/docker-fpm-frr/frr/supervisord/supervisord.conf.j2 index 105c03345b48..2c99eb66f7c5 100644 --- a/dockers/docker-fpm-frr/frr/supervisord/supervisord.conf.j2 +++ b/dockers/docker-fpm-frr/frr/supervisord/supervisord.conf.j2 @@ -141,6 +141,20 @@ stderr_logfile=syslog dependent_startup=true dependent_startup_wait_for=bgpd:running +{% if DEVICE_METADATA.localhost.frr_mgmt_framework_config is defined and DEVICE_METADATA.localhost.frr_mgmt_framework_config == "true" %} +{% else %} +[program:staticroutebfd] +command=/usr/local/bin/staticroutebfd +priority=6 +autostart=false +autorestart=true +startsecs=0 +stdout_logfile=syslog +stderr_logfile=syslog +dependent_startup=true +dependent_startup_wait_for=bgpd:running +{% endif %} + [program:bgpmon] command=/usr/local/bin/bgpmon priority=6 diff --git a/src/sonic-bgpcfgd/bgpcfgd/managers_static_rt.py b/src/sonic-bgpcfgd/bgpcfgd/managers_static_rt.py index fae7a5365aba..951ee4a43863 100644 --- a/src/sonic-bgpcfgd/bgpcfgd/managers_static_rt.py +++ b/src/sonic-bgpcfgd/bgpcfgd/managers_static_rt.py @@ -25,6 +25,7 @@ def __init__(self, common_objs, db, table): self.directory.subscribe([("CONFIG_DB", swsscommon.CFG_DEVICE_METADATA_TABLE_NAME, "localhost/bgp_asn"),], self.on_bgp_asn_change) self.static_routes = {} self.vrf_pending_redistribution = set() + self.config_db = None OP_DELETE = 'DELETE' OP_ADD = 'ADD' @@ -41,8 +42,18 @@ def set_handler(self, key, data): intf_list = arg_list(data['ifname']) if 'ifname' in data else None dist_list = arg_list(data['distance']) if 'distance' in data else None nh_vrf_list = arg_list(data['nexthop-vrf']) if 'nexthop-vrf' in data else None + bfd_enable = arg_list(data['bfd']) if 'bfd' in data else None route_tag = self.ROUTE_ADVERTISE_DISABLE_TAG if 'advertise' in data and data['advertise'] == "false" else self.ROUTE_ADVERTISE_ENABLE_TAG + # bfd enabled route would be handled in staticroutebfd, skip here + if bfd_enable and bfd_enable[0].lower() == "true": + log_debug("{} static route {} bfd flag is true".format(self.db_name, key)) + tmp_nh_set, tmp_route_tag = self.static_routes.get(vrf, {}).get(ip_prefix, (IpNextHopSet(is_ipv6), route_tag)) + if tmp_nh_set: #clear nexthop set if it is not empty + log_debug("{} static route {} bfd flag is true, cur_nh is not empty, clear it".format(self.db_name, key)) + self.static_routes.setdefault(vrf, {}).pop(ip_prefix, None) + return True + try: ip_nh_set = IpNextHopSet(is_ipv6, bkh_list, nh_list, intf_list, dist_list, nh_vrf_list) cur_nh_set, cur_route_tag = self.static_routes.get(vrf, {}).get(ip_prefix, (IpNextHopSet(is_ipv6), route_tag)) @@ -60,19 +71,59 @@ def set_handler(self, key, data): if cmd_list: self.cfg_mgr.push_list(cmd_list) - log_debug("Static route {} is scheduled for updates".format(key)) + log_debug("{} Static route {} is scheduled for updates. {}".format(self.db_name, key, str(cmd_list))) else: - log_debug("Nothing to update for static route {}".format(key)) + log_debug("{} Nothing to update for static route {}".format(self.db_name, key)) self.static_routes.setdefault(vrf, {})[ip_prefix] = (ip_nh_set, route_tag) return True + def skip_appl_del(self, vrf, ip_prefix): + """ + If a static route is bfd enabled, the processed static route is written into application DB by staticroutebfd. + When we disable bfd for that route at runtime, that static route entry will be removed from APPL_DB STATIC_ROUTE_TABLE. + In the case, the StaticRouteMgr(appl_db) cannot uninstall the static route from FRR if the static route is still in CONFIG_DB, + so need this checking (skip appl_db deletion) to avoid race condition between StaticRouteMgr(appl_db) and StaticRouteMgr(config_db) + For more detailed information: + https://github.com/sonic-net/SONiC/blob/master/doc/static-route/SONiC_static_route_bfd_hld.md#bfd-field-changes-from-true-to-false + + :param vrf: vrf from the split_key(key) return + :param ip_prefix: ip_prefix from the split_key(key) return + :return: True if the deletion comes from APPL_DB and the vrf|ip_prefix exists in CONFIG_DB, otherwise return False + """ + if self.db_name == "CONFIG_DB": + return False + + if self.config_db is None: + self.config_db = swsscommon.SonicV2Connector() + self.config_db.connect(self.config_db.CONFIG_DB) + + #just pop local cache if the route exist in config_db + cfg_key = "STATIC_ROUTE|" + vrf + "|" + ip_prefix + nexthop = self.config_db.get(self.config_db.CONFIG_DB, cfg_key, "nexthop") + if nexthop and len(nexthop)>0: + self.static_routes.setdefault(vrf, {}).pop(ip_prefix, None) + return True + + if vrf == "default": + cfg_key = "STATIC_ROUTE|" + ip_prefix + nexthop = self.config_db.get(self.config_db.CONFIG_DB, cfg_key, "nexthop") + if nexthop and len(nexthop)>0: + self.static_routes.setdefault(vrf, {}).pop(ip_prefix, None) + return True + + return False + def del_handler(self, key): vrf, ip_prefix = self.split_key(key) is_ipv6 = TemplateFabric.is_ipv6(ip_prefix) + if self.skip_appl_del(vrf, ip_prefix): + log_debug("{} ignore appl_db static route deletion because of key {} exist in config_db".format(self.db_name, key)) + return + ip_nh_set = IpNextHopSet(is_ipv6) cur_nh_set, route_tag = self.static_routes.get(vrf, {}).get(ip_prefix, (IpNextHopSet(is_ipv6), self.ROUTE_ADVERTISE_DISABLE_TAG)) cmd_list = self.static_route_commands(ip_nh_set, cur_nh_set, ip_prefix, vrf, route_tag, route_tag) @@ -85,9 +136,9 @@ def del_handler(self, key): if cmd_list: self.cfg_mgr.push_list(cmd_list) - log_debug("Static route {} is scheduled for updates".format(key)) + log_debug("{} Static route {} is scheduled for updates. {}".format(self.db_name, key, str(cmd_list))) else: - log_debug("Nothing to update for static route {}".format(key)) + log_debug("{} Nothing to update for static route {}".format(self.db_name, key)) self.static_routes.setdefault(vrf, {}).pop(ip_prefix, None) diff --git a/src/sonic-bgpcfgd/setup.py b/src/sonic-bgpcfgd/setup.py index b451accb9792..30298fea989a 100755 --- a/src/sonic-bgpcfgd/setup.py +++ b/src/sonic-bgpcfgd/setup.py @@ -11,6 +11,7 @@ entry_points = { 'console_scripts': [ 'bgpcfgd = bgpcfgd.main:main', + 'staticroutebfd = staticroutebfd.main:main', 'bgpmon = bgpmon.bgpmon:main', ] }, diff --git a/src/sonic-bgpcfgd/staticroutebfd/__init__.py b/src/sonic-bgpcfgd/staticroutebfd/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/sonic-bgpcfgd/staticroutebfd/main.py b/src/sonic-bgpcfgd/staticroutebfd/main.py new file mode 100644 index 000000000000..e3b2ed10be30 --- /dev/null +++ b/src/sonic-bgpcfgd/staticroutebfd/main.py @@ -0,0 +1,779 @@ +import os +import signal +import sys +import syslog +import threading +import traceback +from collections import defaultdict +from ipaddress import IPv4Address, IPv6Address +from copy import deepcopy + +from swsscommon import swsscommon + +from .vars import g_debug, bfd_multihop, bfd_rx_interval, bfd_tx_interval, bfd_multiplier + +g_run = True + +CONFIG_DB_NAME = "CONFIG_DB" +APPL_DB_NAME = "APPL_DB" +STATE_DB_NAME = "STATE_DB" + +INTERFACE_TABLE_NAME = "INTERFACE" +PORTCHANNEL_INTERFACE_TABLE_NAME = "PORTCHANNEL_INTERFACE" +STATIC_ROUTE_TABLE_NAME = "STATIC_ROUTE" +BFD_SESSION_TABLE_NAME = "BFD_SESSION_TABLE" + +LOCAL_CONFIG_TABLE = "config" +LOCAL_NEXTHOP_TABLE = "nexthop" +LOCAL_SRT_TABLE = "srt" +LOCAL_BFD_TABLE = "bfd" +LOCAL_BFD_PENDING_TABLE = "bfd_pending" +LOCAL_INTERFACE_TABLE = "interface" + +def log_debug(msg): + """ Send a message msg to the syslog as DEBUG """ + if g_debug: + syslog.syslog(syslog.LOG_DEBUG, msg) + +def log_notice(msg): + """ Send a message msg to the syslog as NOTICE """ + syslog.syslog(syslog.LOG_NOTICE, msg) + +def log_info(msg): + """ Send a message msg to the syslog as INFO """ + syslog.syslog(syslog.LOG_INFO, msg) + +def log_warn(msg): + """ Send a message msg to the syslog as WARNING """ + syslog.syslog(syslog.LOG_WARNING, msg) + +def log_err(msg): + """ Send a message msg to the syslog as ERR """ + syslog.syslog(syslog.LOG_ERR, msg) + +def log_crit(msg): + """ Send a message msg to the syslog as CRIT """ + syslog.syslog(syslog.LOG_CRIT, msg) + +def signal_handler(_, __): # signal_handler(signum, frame) + """ signal handler """ + global g_run + g_run = False + +def static_route_split_key(key): + """ + Split key into vrf name and prefix. + :param key: key to split + :return: valid, vrf name extracted from the key, ip prefix extracted from the key + """ + l = tuple(key.split('|')) + + if len(l) == 1: + return True, 'default', l[0] + + return True, l[0], l[1] + +def check_ip(ip): + if len(ip) == 0: + return False, False, "" + + value = ip.split('/',1) + v = value[0] + try: + IPv4Address(v) + valid = True + is_ipv4 = True + except: + is_ipv4 = False + try: + IPv6Address(v) + valid = True + except: + valid = False + return valid, is_ipv4, v + +class StaticRouteBfd(object): + + SELECT_TIMEOUT = 1000 + BFD_DEFAULT_CFG = {"multihop": "false", "rx_interval": "50", "tx_interval": "50"} + + def __init__(self): + self.local_db = defaultdict(dict) + self.local_db[LOCAL_CONFIG_TABLE] = defaultdict(dict) + self.local_db[LOCAL_NEXTHOP_TABLE] = defaultdict(set) + self.local_db[LOCAL_SRT_TABLE] = defaultdict(set) + self.local_db[LOCAL_BFD_TABLE] = defaultdict(dict) + self.local_db[LOCAL_BFD_PENDING_TABLE] = defaultdict(dict) + #interface, portchannel_interface and loopback_interface share same table, assume name is unique + #assume only one ipv4 and/or one ipv6 for each interface + self.local_db[LOCAL_INTERFACE_TABLE] = defaultdict(dict) + + self.config_db = swsscommon.DBConnector(CONFIG_DB_NAME, 0, True) + self.appl_db = swsscommon.DBConnector(APPL_DB_NAME, 0, True) + self.state_db = swsscommon.DBConnector(STATE_DB_NAME, 0, True) + + self.bfd_appl_tbl = swsscommon.ProducerStateTable(self.appl_db, BFD_SESSION_TABLE_NAME) + + self.static_route_appl_tbl = swsscommon.Table(self.appl_db, STATIC_ROUTE_TABLE_NAME) + + self.selector = swsscommon.Select() + self.callbacks = defaultdict(lambda: defaultdict(list)) # db -> table -> handlers[] + self.subscribers = set() + self.first_time = True + + def get_ip_from_key(self, key): + """ + Get ip address from key for LOOPBACK/INTERFACE/PORTCHANNEL_INTERFACE table + :param key: key in the tables + :return: valid, is_ipv4, ip address + """ + if '|' not in key: + return False, False, "", "" + else: + if_ip = key.split('|') + if len(if_ip) < 2: + return False, False, "", "" + if_name = if_ip[0] + value = if_ip[1] + valid, is_ipv4, ip = check_ip(value) + log_debug("get ip from intf key: valid %s is_ipv4 %s, if_name %s ip %s"%(str(valid), str(is_ipv4), if_name, ip)) + return valid, is_ipv4, if_name, ip + + def set_local_db(self, table, key, data): + try: + self.local_db[table][key] = data + return + except: + log_err("set_local_db error, table %s key %s"%(table, key)) + pass + + def get_local_db(self, table, key): + try: + v = self.local_db[table][key] + return v + except: + return {} + + def remove_from_local_db(self, table, key): + if table in self.local_db: + if key in self.local_db[table]: + del self.local_db[table][key] + + def append_to_nh_table_entry(self, nh_key, ip_prefix): + entry = self.get_local_db(LOCAL_NEXTHOP_TABLE, nh_key) + entry.add(ip_prefix) + + def remove_from_nh_table_entry(self, nh_key, ip_prefix): + entry = self.get_local_db(LOCAL_NEXTHOP_TABLE, nh_key) + if ip_prefix in entry: + entry.remove(ip_prefix) + if len(entry) == 0: + self.remove_from_local_db(LOCAL_NEXTHOP_TABLE, nh_key) + + def set_bfd_session_into_appl_db(self, key, data): + fvs = swsscommon.FieldValuePairs(list(data.items())) + self.bfd_appl_tbl.set(key, fvs) + log_debug("set bfd session to appl_db, key %s, data %s"%(key, str(data))) + + def del_bfd_session_from_appl_db(self, key): + self.bfd_appl_tbl.delete(key) + + def interface_set_handler(self, key, data): + valid, is_ipv4, if_name, ip = self.get_ip_from_key(key) + if not valid: + return True + value = self.get_local_db(LOCAL_INTERFACE_TABLE, if_name) + if len(value) == 0: + value = {is_ipv4: ip} + else: + value[is_ipv4] = ip + self.set_local_db(LOCAL_INTERFACE_TABLE, if_name, value) + self.update_bfd_pending(if_name) + return True + + def interface_del_handler(self, key): + valid, is_ipv4, if_name, ip = self.get_ip_from_key(key) + if not valid: + return True + value = self.get_local_db(LOCAL_INTERFACE_TABLE, if_name) + if len(value) == 0: + return + else: + value[is_ipv4] = "" #remove the IP address for the interface + self.set_local_db(LOCAL_INTERFACE_TABLE, if_name, value) + + def find_interface_ip(self, if_name, ip_example): + valid, is_ipv4, nh = check_ip(ip_example) + if not valid: + return False, "" + + value = self.get_local_db(LOCAL_INTERFACE_TABLE, if_name) + ip = value.get(is_ipv4, "") + if len(ip)>0: #ip should be verified when add to local_db + return True, ip + + return False, "" + + def update_bfd_pending(self, if_name): + del_list=[] + for k, v in self.local_db[LOCAL_BFD_PENDING_TABLE].items(): + if len(v) == 3 and v[0] == if_name: + intf, nh_ip, bfd_key = v[0], v[1], v[2] + valid, local_addr = self.find_interface_ip(intf, nh_ip) + if not valid: #IP address might not be available for this type of nh_ip (IPv4 or IPv6) yet + continue + log_notice("bfd_pending: get ip for interface: %s, create bfd session for %s"%(intf, bfd_key)) + bfd_entry_cfg = self.BFD_DEFAULT_CFG.copy() + if all([bfd_rx_interval, bfd_tx_interval, bfd_multiplier, bfd_multihop]): + bfd_entry_cfg["multihop"] = bfd_multihop + bfd_entry_cfg["rx_interval"] = bfd_rx_interval + bfd_entry_cfg["tx_interval"] = bfd_tx_interval + bfd_entry_cfg["multiplier"] = bfd_multiplier + + bfd_entry_cfg["local_addr"] = local_addr + self.set_bfd_session_into_appl_db(bfd_key, bfd_entry_cfg) + bfd_entry_cfg["static_route"] = "true" + self.set_local_db(LOCAL_BFD_TABLE, bfd_key, bfd_entry_cfg) + del_list.append(k) + + for k in del_list: + self.local_db[LOCAL_BFD_PENDING_TABLE].pop(k) + + def strip_table_name(self, key, splitter): + return key.split(splitter, 1)[1] + + def reconciliation(self): + #to use SonicV2Connector get_all method, DBConnector doesn't have get_all + db = swsscommon.SonicV2Connector() + db.connect(db.CONFIG_DB) + db.connect(db.APPL_DB) + db.connect(db.STATE_DB) + + #MUST keep the restore sequene + #restore interface(loopback/interface/portchannel_interface) tables + + #restore interface tables + log_info("restore interface table -->") + keys = db.keys(db.CONFIG_DB, "LOOPBACK_INTERFACE|*") + for key in keys: + key_new = self.strip_table_name(key, "|") + self.interface_set_handler(key_new, "") + keys = db.keys(db.CONFIG_DB, "INTERFACE|*") + for key in keys: + key_new = self.strip_table_name(key, "|") + self.interface_set_handler(key_new, "") + keys = db.keys(db.CONFIG_DB, "PORTCHANNEL_INTERFACE|*") + for key in keys: + key_new = self.strip_table_name(key, "|") + self.interface_set_handler(key_new, "") + + #restore bfd session table, static route won't create bfd session if it is already in appl_db + log_info("restore bfd session table -->") + keys = db.keys(db.APPL_DB, "BFD_SESSION_TABLE:*") + for key in keys: + data = db.get_all(db.APPL_DB, key) + key_new = self.strip_table_name(key, ":") + self.set_local_db(LOCAL_BFD_TABLE, key_new, data) + + #restore static route table + log_info("restore static route table -->") + keys = db.keys(db.CONFIG_DB, "STATIC_ROUTE|*") + for key in keys: + data = db.get_all(db.CONFIG_DB, key) + key_new = self.strip_table_name(key, "|") + log_debug("SRT_BFD: restore static route from config_db, key %s, data %s"%(key, str(data))) + self.static_route_set_handler(key_new, data) + + #clean up local bfd table, remove non static route bfd session + log_info("cleanup bfd session table -->") + self.cleanup_local_bfd_table() + + #restore bfd state table + log_info("restore bfd state table -->") + keys = db.keys(db.STATE_DB, "BFD_SESSION_TABLE|*") + for key in keys: + data = db.get_all(db.STATE_DB, key) + key_new = self.strip_table_name(key, "|") + self.bfd_state_set_handler(key_new, data) + + def cleanup_local_bfd_table(self): + kl=[] + for key in self.local_db[LOCAL_BFD_TABLE]: + kl.append(key) + for key in kl: + bfd_session = self.local_db[LOCAL_BFD_TABLE][key] + if "static_route" not in bfd_session or bfd_session["static_route"] != "true": + self.local_db[LOCAL_BFD_TABLE].pop(key) + + def isFieldTrue(self, bfd_field): + if isinstance(bfd_field, list): + if len(bfd_field) == 1: + if isinstance(bfd_field[0], str): + if bfd_field[0].lower() == "true": + return True + return False + + def refresh_active_nh(self, route_cfg_key): + data = self.get_local_db(LOCAL_CONFIG_TABLE, route_cfg_key) + + arg_list = lambda v: [x.strip() for x in v.split(',')] if len(v.strip()) != 0 else None + nh_list = arg_list(data['nexthop']) if 'nexthop' in data else None + nh_vrf_list = arg_list(data['nexthop-vrf']) if 'nexthop-vrf' in data else None + nh_cnt = 0 + + for index in range(len(nh_list)): + nh_ip = nh_list[index] + nh_vrf = nh_vrf_list[index] + nh_key = nh_vrf + "|" + nh_ip + bfd_key = nh_vrf + ":default:" + nh_ip + + bfd_session = self.get_local_db(LOCAL_BFD_TABLE, bfd_key) + if len(bfd_session) == 0: + continue + if "state" in bfd_session and bfd_session["state"].upper() == "UP": + self.append_to_srt_table_entry(route_cfg_key, (nh_vrf, nh_ip)) + nh_cnt += 1 + + #do not write to appl_db is no nexthop reachable + if nh_cnt == 0: + return + + #if there is any bfd session state UP, we don't need to hold the static route update. + data['bfd_nh_hold'] = "false" + new_config = self.reconstruct_static_route_config(data, self.get_local_db(LOCAL_SRT_TABLE, route_cfg_key)) + self.set_static_route_into_appl_db(route_cfg_key.replace("|", ":"), new_config) + + def handle_bfd_change(self, cfg_key, data, to_bfd_enable): + valid, vrf, ip_prefix = static_route_split_key(cfg_key) + key = vrf + ":" + ip_prefix + log_debug("SRT_BFD: handle_bfd_change. key %s, data %s, to_bfd_enable %s"%(key, str(data), str(to_bfd_enable))) + if to_bfd_enable: + #write route with full_nh_list to appl_db, let StaticRouteMgr(appl_db) install this route to update its cache + data['bfd'] = "false" + data['expiry'] = "false" + self.set_static_route_into_appl_db(key, data) + log_debug("SRT_BFD: bfd toggle to true. write the route to appl_db, update StaticRouteMgr(appl_db), key %s"%(key)) + else: + self.del_static_route_from_appl_db(key) + log_debug("SRT_BFD: bfd toggle to false. delete static route from appl_db, key %s"%(key)) + + #treat it as static route deletion, but do not delete it from LOCAL_CONFIG_TABLE + self.static_route_del_handler(cfg_key, False) + + def static_route_set_handler(self, key, data): + + #sanity checking + if len(data) == 0: + return True + + valid, vrf, ip_prefix = static_route_split_key(key) + route_cfg_key = vrf + "|" + ip_prefix + if not valid: + return True + + valid, is_ipv4, ip = check_ip(ip_prefix) + if not valid: + log_err("invalid ip prefix for static route: ", key) + return True + + arg_list = lambda v: [x.strip() for x in v.split(',')] if len(v.strip()) != 0 else None + bfd_field = arg_list(data['bfd']) if 'bfd' in data else ["false"] + + cur_data = self.get_local_db(LOCAL_CONFIG_TABLE, route_cfg_key) + cur_bfd_enabled = False + if cur_data: + cur_bfd_field = arg_list(cur_data['bfd']) if 'bfd' in cur_data else ["false"] + cur_bfd_enabled = self.isFieldTrue(cur_bfd_field) + + # this process, staticroutebfd, only handle the bfd enabled case, other cases would be handled in bgpcfgd/StaticRouteMgr + bfd_enabled = self.isFieldTrue(bfd_field) + + #when bfd changed from "false" to "true", before bfd session created and state becomes up, + #the installed static route need to be kept in the system system, so put this route in "hold" state until at least one + #bfd session becomes UP. + data_copy = data.copy() + data['bfd_nh_hold'] = "false" + if cur_data: + if cur_bfd_enabled and not bfd_enabled: # dynamic bfd flag change from TRUE to FALSE + self.handle_bfd_change(key, data_copy, False) + if not cur_bfd_enabled and bfd_enabled: # dynamic bfd flag change from FALSE to TRUE + self.handle_bfd_change(key, data_copy, True) + data['bfd_nh_hold'] = "true" + + # preprocess empty nexthop-vrf list before save to LOCAL_CONFIG_TABLE, bfd session need this information + nh_list = arg_list(data['nexthop']) if 'nexthop' in data else None + nh_vrf_list = arg_list(data['nexthop-vrf']) if 'nexthop-vrf' in data else None + if nh_vrf_list is None: + nh_vrf_list = [vrf] * len(nh_list) if len(nh_list) > 0 else None + data['nexthop-vrf'] = ','.join(nh_vrf_list) if nh_vrf_list else '' + else: # preprocess empty nexthop-vrf member + for index in range(len(nh_vrf_list)): + if len(nh_vrf_list[index]) == 0: + nh_vrf_list[index] = vrf + data['nexthop-vrf'] = ','.join(nh_vrf_list) + + if not bfd_enabled: + #skip if bfd is not enabled, but store it to local_db to detect bfd field dynamic change + data['bfd'] = "false" + self.set_local_db(LOCAL_CONFIG_TABLE, route_cfg_key, data) + return True + + bkh_list = arg_list(data['blackhole']) if 'blackhole' in data else None + intf_list = arg_list(data['ifname']) if 'ifname' in data else None + dist_list = arg_list(data['distance']) if 'distance' in data else None + if intf_list is None or nh_list is None or nh_vrf_list is None or \ + len(intf_list) != len(nh_list) or len(intf_list) != len(nh_vrf_list): + log_err("Static route bfd set Failed, nexthop, interface and vrf lists do not match.") + return True + + + if cur_data and cur_bfd_enabled: + # route with the prefix already exist, remove the deleted nexthops + nh_list_exist = arg_list(cur_data['nexthop']) if 'nexthop' in cur_data else None + nh_vrf_list_exist = arg_list(cur_data['nexthop-vrf']) if 'nexthop-vrf' in cur_data else None + if nh_vrf_list_exist is None: + nh_vrf_list_exist = [] + for nh in nh_list: + nh_vrf_list_exist.append(vrf) + + intf_list_exist = arg_list(cur_data['ifname']) if 'ifname' in cur_data else None + nh_key_list_exist = list(zip(nh_vrf_list_exist, intf_list_exist, nh_list_exist)) + nh_key_list_new = list(zip(nh_vrf_list, intf_list, nh_list)) + for nh in nh_key_list_exist: + if nh not in nh_key_list_new: + nh_vrf = nh[0] + nh_ip = nh[2] + nh_key = nh_vrf + "|" + nh_ip + self.remove_from_srt_table_entry(route_cfg_key, (nh_vrf, nh_ip)) + self.remove_from_nh_table_entry(nh_key, route_cfg_key) + if len(self.get_local_db(LOCAL_NEXTHOP_TABLE, nh_key)) == 0: + bfd_key = nh_vrf + ":default:" + nh_ip + self.remove_from_local_db(LOCAL_BFD_TABLE, bfd_key) + self.del_bfd_session_from_appl_db(bfd_key) + + self.set_local_db(LOCAL_CONFIG_TABLE, route_cfg_key, data) + for index in range(len(nh_list)): + nh_ip = nh_list[index] + intf = intf_list[index] + nh_vrf = nh_vrf_list[index] + nh_key = nh_vrf + "|" + nh_ip + + #check if the bfd session is already created + bfd_key = nh_vrf + ":default:" + nh_ip + bfd_session = self.get_local_db(LOCAL_BFD_TABLE, bfd_key) + if len(bfd_session)>0: + self.local_db[LOCAL_BFD_TABLE][bfd_key]["static_route"] = "true" + + if len(self.get_local_db(LOCAL_NEXTHOP_TABLE, nh_key)) == 0 and len(bfd_session) == 0: + valid, local_addr = self.find_interface_ip(intf, nh_ip) + if not valid: + #interface IP is not available yet, put this request to cache + self.set_local_db(LOCAL_BFD_PENDING_TABLE, intf+"_"+bfd_key, [intf, nh_ip, bfd_key]) + self.append_to_nh_table_entry(nh_key, vrf + "|" + ip_prefix) + log_warn("bfd_pending: cannot find ip for interface: %s, postpone bfd session creation" %intf) + continue + + bfd_entry_cfg = self.BFD_DEFAULT_CFG.copy() + if all([bfd_rx_interval, bfd_tx_interval, bfd_multiplier, bfd_multihop]): + bfd_entry_cfg["multihop"] = bfd_multihop + bfd_entry_cfg["rx_interval"] = bfd_rx_interval + bfd_entry_cfg["tx_interval"] = bfd_tx_interval + bfd_entry_cfg["multiplier"] = bfd_multiplier + + bfd_entry_cfg["local_addr"] = local_addr + self.set_bfd_session_into_appl_db(bfd_key, bfd_entry_cfg) + bfd_entry_cfg["static_route"] = "true" + self.set_local_db(LOCAL_BFD_TABLE, bfd_key, bfd_entry_cfg) + + self.append_to_nh_table_entry(nh_key, vrf + "|" + ip_prefix) + + self.refresh_active_nh(route_cfg_key) + + return True + + def static_route_del_handler(self, key, redis_del): + valid, vrf, ip_prefix = static_route_split_key(key) + if not valid: + return True + route_cfg_key = vrf + "|" + ip_prefix + + valid, is_ipv4, ip = check_ip(ip_prefix) + if not valid: + return True + + data = self.get_local_db(LOCAL_CONFIG_TABLE, route_cfg_key) + if len(data) == 0: + # this route is not handled by StaticRouteBfd, skip + return True + + arg_list = lambda v: [x.strip() for x in v.split(',')] if len(v.strip()) != 0 else None + nh_list = arg_list(data['nexthop']) if 'nexthop' in data else None + nh_vrf_list = arg_list(data['nexthop-vrf']) if 'nexthop-vrf' in data else None + bfd_field = arg_list(data['bfd']) if 'bfd' in data else ["false"] + bfd_enabled = self.isFieldTrue(bfd_field) + + # for a bfd_enabled static route, the nh_vrf_list was processed, has same length with nh_list + if bfd_enabled and nh_list and nh_vrf_list and len(nh_list) == len(nh_vrf_list): + for index in range(len(nh_list)): + nh_ip = nh_list[index] + nh_vrf = nh_vrf_list[index] + nh_key = nh_vrf + "|" + nh_ip + self.remove_from_nh_table_entry(nh_key, route_cfg_key) + + if len(self.get_local_db(LOCAL_NEXTHOP_TABLE, nh_key)) == 0: + bfd_key = nh_vrf + ":default:" + nh_ip + self.remove_from_local_db(LOCAL_BFD_TABLE, bfd_key) + self.del_bfd_session_from_appl_db(bfd_key) + + # do not delete it from appl_db if the route is not bfd enabled + if bfd_enabled: + self.del_static_route_from_appl_db(route_cfg_key.replace("|", ":")) + + self.remove_from_local_db(LOCAL_SRT_TABLE, route_cfg_key) + + if redis_del: + self.remove_from_local_db(LOCAL_CONFIG_TABLE, route_cfg_key) + + return True + + def interface_callback(self, key, op, data): + if op == swsscommon.SET_COMMAND: + self.interface_set_handler(key, data) + elif op == swsscommon.DEL_COMMAND: + self.interface_del_handler(key) + else: + log_err("Invalid operation '%s' for key '%s'" % (op, key)) + + def static_route_callback(self, key, op, data): + if op == swsscommon.SET_COMMAND: + self.static_route_set_handler(key, data) + elif op == swsscommon.DEL_COMMAND: + self.static_route_del_handler(key, True) + else: + log_err("Invalid operation '%s' for key '%s'" % (op, key)) + + def bfd_state_split_key(self, key): + """ + Split key into table name, vrf name, interface name and peer ip. + :param key: key to split + :return: table name, vrf name, interface name and peer ip extracted from the key + """ + if key.count("|") < 2: + return 'default', 'default', key + else: + return tuple(key.split('|')) + + def append_to_srt_table_entry(self, srt_key, nh_info): + entry = self.get_local_db(LOCAL_SRT_TABLE, srt_key) + entry.add(nh_info) + + def remove_from_srt_table_entry(self, srt_key, nh_info): + entry = self.get_local_db(LOCAL_SRT_TABLE, srt_key) + if nh_info in entry: + entry.remove(nh_info) + if len(entry) == 0: + self.remove_from_local_db(LOCAL_SRT_TABLE, srt_key) + + def set_static_route_into_appl_db(self, key, data): + fvs = swsscommon.FieldValuePairs(list(data.items())) + self.static_route_appl_tbl.set(key, fvs) + log_debug("SRT_BFD: set static route to appl_db, key %s, data %s"%(key, str(data))) + + def del_static_route_from_appl_db(self, key): + self.static_route_appl_tbl.delete(key) + + def reconstruct_static_route_config(self, original_config, reachable_nexthops): + arg_list = lambda v: [x.strip() for x in v.split(',')] if len(v.strip()) != 0 else None + bkh_list = arg_list(original_config['blackhole']) if 'blackhole' in original_config else None + nh_list = arg_list(original_config['nexthop']) if 'nexthop' in original_config else None + intf_list = arg_list(original_config['ifname']) if 'ifname' in original_config else None + dist_list = arg_list(original_config['distance']) if 'distance' in original_config else None + nh_vrf_list = arg_list(original_config['nexthop-vrf']) if 'nexthop-vrf' in original_config else None + + bkh_candidate = "" + nh_candidate = "" + intf_candidate = "" + dist_candidate = "" + nh_vrf_candidate = "" + + + for i in range(len(nh_list)): + if (nh_vrf_list[i], nh_list[i]) in reachable_nexthops: + bkh_candidate += "," + (bkh_list[i] if bkh_list else "") + nh_candidate += "," + (nh_list[i] if nh_list else "") + intf_candidate += "," + (intf_list[i] if intf_list else "") + dist_candidate += "," + (dist_list[i] if dist_list else "") + nh_vrf_candidate += "," + (nh_vrf_list[i] if nh_vrf_list else "") + + new_config = dict() + for key in original_config: + if key == "bfd": + continue + if key == "bfd_nh_hold": + continue + if key == "blackhole": + new_config[key] = bkh_candidate[1:] + elif key == "nexthop": + new_config[key] = nh_candidate[1:] + elif key == "ifname": + new_config[key] = intf_candidate[1:] + elif key == "distance": + new_config[key] = dist_candidate[1:] + elif key == "nexthop-vrf": + new_config[key] = nh_vrf_candidate[1:] + else: + new_config[key] = original_config[key] + new_config["expiry"] = "false" + + return new_config + + + def bfd_state_set_handler(self, key, data): + #key are diff in state db and appl_db, + #intf is always default for multihop bfd + vrf, intf, peer_ip = self.bfd_state_split_key(key) + bfd_key = vrf + ":" + intf + ":" + peer_ip + + #check if the BFD session is in local table + bfd_session = self.get_local_db(LOCAL_BFD_TABLE, bfd_key) + if len(bfd_session) == 0: + return True + + nh_key = vrf + "|" + peer_ip + state = data['state'] if 'state' in data else "DOWN" + log_info("bfd seesion %s state %s" %(bfd_key, state)) + + self.local_db[LOCAL_BFD_TABLE][bfd_key]["state"] = state + + if state.upper() == "UP": + for prefix in self.get_local_db(LOCAL_NEXTHOP_TABLE, nh_key): + srt_key = prefix + config_key = prefix + #skip if the (vrf, peer_ip) is already in the nexthop list + if (vrf, peer_ip) in self.get_local_db(LOCAL_SRT_TABLE, srt_key): + continue + self.append_to_srt_table_entry(srt_key, (vrf, peer_ip)) + config_data = self.get_local_db(LOCAL_CONFIG_TABLE, config_key) + #exit "hold" state when any BFD session becomes UP + config_data['bfd_nh_hold'] = "false" + new_config = self.reconstruct_static_route_config(config_data, self.get_local_db(LOCAL_SRT_TABLE, srt_key)) + self.set_static_route_into_appl_db(srt_key.replace("|", ":"), new_config) + + elif state.upper() == "DOWN": + for prefix in self.get_local_db(LOCAL_NEXTHOP_TABLE, nh_key): + srt_key = prefix + config_key = prefix + config_data = self.get_local_db(LOCAL_CONFIG_TABLE, config_key) + #skip if the static route is in "hold" state + if config_data['bfd_nh_hold'] == "true": + continue + self.remove_from_srt_table_entry(srt_key, (vrf, peer_ip)) + if len(self.get_local_db(LOCAL_SRT_TABLE, srt_key)) == 0: + log_debug("SRT_BFD: bfd_state DOWN. nh_list is empty, delete static route from appl_db, key %s"%(srt_key.replace("|", ":"))) + self.del_static_route_from_appl_db(srt_key.replace("|", ":")) + else: + config_data = self.get_local_db(LOCAL_CONFIG_TABLE, config_key) + new_config = self.reconstruct_static_route_config(config_data, self.get_local_db(LOCAL_SRT_TABLE, srt_key)) + self.set_static_route_into_appl_db(srt_key.replace("|", ":"), new_config) + + + def bfd_state_del_handler(self, key): + vrf, intf, peer_ip = self.bfd_state_split_key(key) + bfd_key = vrf + ":" + intf + ":" + peer_ip + + nh_key = vrf + "|" + peer_ip + + for prefix in self.get_local_db(LOCAL_NEXTHOP_TABLE, nh_key): + srt_key = prefix + config_key = prefix + self.remove_from_srt_table_entry(srt_key, (vrf, peer_ip)) + if len(self.get_local_db(LOCAL_SRT_TABLE, srt_key)) == 0: + log_debug("SRT_BFD: bfd_state deletion. nh_list is empty, delete static route from appl_db, key %s"%(srt_key.replace("|", ":"))) + self.del_static_route_from_appl_db(srt_key.replace("|", ":")) + else: + config_data = self.get_local_db(LOCAL_CONFIG_TABLE, config_key) + new_config = self.reconstruct_static_route_config(config_data, self.get_local_db(LOCAL_SRT_TABLE, srt_key)) + self.set_static_route_into_appl_db(srt_key.replace("|", ":"), new_config) + + def bfd_state_callback(self, key, op, data): + if op == swsscommon.SET_COMMAND: + self.bfd_state_set_handler(key, data) + elif op == swsscommon.DEL_COMMAND: + self.bfd_state_del_handler(key) + else: + log_err("Invalid operation '%s' for key '%s'" % (op, key)) + + + def prepare_selector(self): + interface_subscriber = swsscommon.SubscriberStateTable(self.config_db, INTERFACE_TABLE_NAME) + portchannel_interface_subscriber = swsscommon.SubscriberStateTable(self.config_db, PORTCHANNEL_INTERFACE_TABLE_NAME) + static_route_subscriber = swsscommon.SubscriberStateTable(self.config_db, STATIC_ROUTE_TABLE_NAME) + bfd_state_subscriber = swsscommon.SubscriberStateTable(self.state_db, swsscommon.STATE_BFD_SESSION_TABLE_NAME) + + self.selector.addSelectable(interface_subscriber) + self.selector.addSelectable(portchannel_interface_subscriber) + self.selector.addSelectable(static_route_subscriber) + self.selector.addSelectable(bfd_state_subscriber) + + self.subscribers.add(interface_subscriber) + self.subscribers.add(portchannel_interface_subscriber) + self.subscribers.add(static_route_subscriber) + self.subscribers.add(bfd_state_subscriber) + + self.callbacks[self.config_db.getDbId()][INTERFACE_TABLE_NAME].append(self.interface_callback) + self.callbacks[self.config_db.getDbId()][PORTCHANNEL_INTERFACE_TABLE_NAME].append(self.interface_callback) + self.callbacks[self.config_db.getDbId()][STATIC_ROUTE_TABLE_NAME].append(self.static_route_callback) + self.callbacks[self.state_db.getDbId()][swsscommon.STATE_BFD_SESSION_TABLE_NAME].append(self.bfd_state_callback) + + def run(self): + self.prepare_selector() + while g_run: + state, _ = self.selector.select(self.SELECT_TIMEOUT) + if state == self.selector.TIMEOUT: + continue + elif state == self.selector.ERROR: + raise Exception("Received error from select") + + if self.first_time: + self.first_time = False + self.reconciliation() + + for sub in self.subscribers: + while True: + key, op, fvs = sub.pop() + if len(key) == 0: + break + log_debug("Received message : '%s'" % str((key, op, fvs))) + for callback in self.callbacks[sub.getDbConnector().getDbId()][sub.getTableName()]: + callback(key, op, dict(fvs)) + +def do_work(): + sr_bfd = StaticRouteBfd() + sr_bfd.run() + +def main(): + rc = 0 + try: + syslog.openlog('staticroutebfd') + signal.signal(signal.SIGTERM, signal_handler) + signal.signal(signal.SIGINT, signal_handler) + do_work() + except KeyboardInterrupt: + syslog.syslog(syslog.LOG_NOTICE, "Keyboard interrupt") + pass + except RuntimeError as exc: + syslog.syslog(syslog.LOG_CRIT, str(exc)) + rc = -2 + if g_debug: + raise + except Exception as exc: + syslog.syslog(syslog.LOG_CRIT, "Got an exception %s: Traceback: %s" % (str(exc), traceback.format_exc())) + rc = -1 + if g_debug: + raise + finally: + syslog.closelog() + try: + sys.exit(rc) + except SystemExit: + os._exit(rc) diff --git a/src/sonic-bgpcfgd/staticroutebfd/vars.py b/src/sonic-bgpcfgd/staticroutebfd/vars.py new file mode 100644 index 000000000000..e880844687f6 --- /dev/null +++ b/src/sonic-bgpcfgd/staticroutebfd/vars.py @@ -0,0 +1,5 @@ +g_debug = True +bfd_multihop = "true" +bfd_rx_interval = "50" +bfd_tx_interval = "50" +bfd_multiplier = "3" diff --git a/src/sonic-bgpcfgd/tests/test_static_rt.py b/src/sonic-bgpcfgd/tests/test_static_rt.py index 881bba1563b7..3d947a47ac73 100644 --- a/src/sonic-bgpcfgd/tests/test_static_rt.py +++ b/src/sonic-bgpcfgd/tests/test_static_rt.py @@ -604,7 +604,7 @@ def test_set_no_action(mocked_log_debug): True, [] ) - mocked_log_debug.assert_called_with("Nothing to update for static route default|10.1.1.0/24") + mocked_log_debug.assert_called_with("CONFIG_DB Nothing to update for static route default|10.1.1.0/24") @patch('bgpcfgd.managers_static_rt.log_debug') def test_del_no_action(mocked_log_debug): @@ -616,7 +616,7 @@ def test_del_no_action(mocked_log_debug): True, [] ) - mocked_log_debug.assert_called_with("Nothing to update for static route default|10.1.1.0/24") + mocked_log_debug.assert_called_with("CONFIG_DB Nothing to update for static route default|10.1.1.0/24") def test_set_invalid_arg(): mgr = constructor() @@ -821,3 +821,109 @@ def test_set_tag_change(): "ip route 10.1.0.0/24 10.0.0.57 tag 2", ] ) + +def test_set_bfd_false(): + mgr = constructor() + set_del_test( + mgr, + "SET", + ("10.1.0.0/24", { + "bfd": "false", + "nexthop": "PortChannel0001", + }), + True, + [ + "ip route 10.1.0.0/24 PortChannel0001 tag 1", + "route-map STATIC_ROUTE_FILTER permit 10", + " match tag 1", + "router bgp 65100", + " address-family ipv4", + " redistribute static route-map STATIC_ROUTE_FILTER", + " address-family ipv6", + " redistribute static route-map STATIC_ROUTE_FILTER" + ] + ) + + set_del_test( + mgr, + "DEL", + ("10.1.0.0/24",), + True, + [ + "no ip route 10.1.0.0/24 PortChannel0001 tag 1", + "router bgp 65100", + " address-family ipv4", + " no redistribute static route-map STATIC_ROUTE_FILTER", + " address-family ipv6", + " no redistribute static route-map STATIC_ROUTE_FILTER", + "no route-map STATIC_ROUTE_FILTER" + ] + ) + +def test_set_bfd_true(): + mgr = constructor() + set_del_test( + mgr, + "SET", + ("10.1.0.0/24", { + "bfd": "false", + "nexthop": "PortChannel0001", + }), + True, + [ + "ip route 10.1.0.0/24 PortChannel0001 tag 1", + "route-map STATIC_ROUTE_FILTER permit 10", + " match tag 1", + "router bgp 65100", + " address-family ipv4", + " redistribute static route-map STATIC_ROUTE_FILTER", + " address-family ipv6", + " redistribute static route-map STATIC_ROUTE_FILTER" + ] + ) + #do nothing for adding smae route second time + set_del_test( + mgr, + "SET", + ("10.1.0.0/24", { + "bfd": "false", + "nexthop": "PortChannel0001", + }), + True, + [ + ] + ) + #clear internal cache if bfd flag is true + set_del_test( + mgr, + "SET", + ("10.1.0.0/24", { + "bfd": "true", + "nexthop": "PortChannel0001", + }), + True, + [ + ] + ) + + #install the route becasue that cache was cleared above + set_del_test( + mgr, + "SET", + ("10.1.0.0/24", { + "bfd": "false", + "nexthop": "PortChannel0001", + }), + True, + [ + "ip route 10.1.0.0/24 PortChannel0001 tag 1", + "route-map STATIC_ROUTE_FILTER permit 10", + " match tag 1", + "router bgp 65100", + " address-family ipv4", + " redistribute static route-map STATIC_ROUTE_FILTER", + " address-family ipv6", + " redistribute static route-map STATIC_ROUTE_FILTER" + ] + ) + diff --git a/src/sonic-bgpcfgd/tests/test_static_rt_bfd.py b/src/sonic-bgpcfgd/tests/test_static_rt_bfd.py new file mode 100644 index 000000000000..0e4d62475988 --- /dev/null +++ b/src/sonic-bgpcfgd/tests/test_static_rt_bfd.py @@ -0,0 +1,605 @@ +from unittest.mock import patch +#from unittest.mock import MagicMock, patch + +from staticroutebfd.main import * +from swsscommon import swsscommon + +@patch('swsscommon.swsscommon.DBConnector.__init__') +@patch('swsscommon.swsscommon.ProducerStateTable.__init__') +@patch('swsscommon.swsscommon.Table.__init__') +def constructor(mock_db, mock_producer, mock_tbl): + mock_db.return_value = None + mock_producer.return_value = None + mock_tbl.return_value = None + + srt_bfd = StaticRouteBfd() + return srt_bfd + +def set_del_test(dut, hdlr, op, args, e_bfd_dict, e_srt_dict): + set_del_test.bfd_dict = {} + set_del_test.srt_dict = {} + + def bfd_app_set(key, data): + set_del_test.bfd_dict["set_"+key] = data.copy() + def bfd_app_del(key): + set_del_test.bfd_dict["del_"+key] = {} + def srt_app_set(key, data): + set_del_test.srt_dict["set_"+key] = data.copy() + def srt_app_del(key): + set_del_test.srt_dict["del_"+key] = {} + + def compare_dict(r, e): + if len(r) == 0 and len(e) == 0: + return True + if len(r) != len(e): + return False + for k in e: + if k not in r: + return False + if type(e[k]) is str: + r_sort = "".join(sorted([x.strip() for x in r[k].split(',')])) + e_sort = "".join(sorted([x.strip() for x in e[k].split(',')])) + if r_sort != e_sort: + return False + if type(e[k]) is dict: + ret = compare_dict(r[k], e[k]) + if not ret: + return False + return True + + dut.set_bfd_session_into_appl_db = bfd_app_set + dut.del_bfd_session_from_appl_db = bfd_app_del + dut.set_static_route_into_appl_db = srt_app_set + dut.del_static_route_from_appl_db = srt_app_del + + if op == "SET": + if hdlr == "bfd": + dut.bfd_state_set_handler(*args) + if hdlr == "srt": + dut.static_route_set_handler(*args) + if hdlr == "intf": + dut.interface_set_handler(*args) + elif op == "DEL": + if hdlr == "bfd": + dut.bfd_state_del_handler(*args) + if hdlr == "srt": + dut.static_route_del_handler(*args) + if hdlr == "intf": + dut.interface_del_handler(*args) + else: + assert False, "Wrong operation" + + assert compare_dict(set_del_test.bfd_dict, e_bfd_dict) + assert compare_dict(set_del_test.srt_dict, e_srt_dict) + +def intf_setup(dut): + set_del_test(dut, "intf", + "SET", + ("if1|192.168.1.1/24", {} + ), + {}, + {} + ) + set_del_test(dut, "intf", + "SET", + ("if2|192.168.2.1/24", {} + ), + {}, + {} + ) + set_del_test(dut, "intf", + "SET", + ("if3|192.168.3.1/24", {} + ), + {}, + {} + ) + +def test_set_del(): + dut = constructor() + intf_setup(dut) + + #test #1 + set_del_test(dut, "srt", + "SET", + ("2.2.2.0/24", { + "bfd": "true", + "nexthop": "192.168.1.2 , 192.168.2.2, 192.168.3.2", + "ifname": "if1, if2, if3", + }), + { + "set_default:default:192.168.1.2" : {'multihop': 'true', 'rx_interval': '50', 'tx_interval': '50', 'multiplier': '3', 'local_addr': '192.168.1.1'}, + "set_default:default:192.168.2.2" : {'multihop': 'true', 'rx_interval': '50', 'tx_interval': '50', 'multiplier': '3', 'local_addr': '192.168.2.1'}, + "set_default:default:192.168.3.2" : {'multihop': 'true', 'rx_interval': '50', 'tx_interval': '50', 'multiplier': '3', 'local_addr': '192.168.3.1'} + }, + {} + ) + + set_del_test(dut, "bfd", + "SET", + ("192.168.1.2", { + "state": "Up" + }), + {}, + {'set_default:2.2.2.0/24': {'nexthop': '192.168.1.2', 'ifname': 'if1', 'nexthop-vrf': 'default', 'expiry': 'false'}} + ) + set_del_test(dut, "bfd", + "SET", + ("192.168.2.2", { + "state": "Up" + }), + {}, + {'set_default:2.2.2.0/24': {'nexthop': '192.168.2.2,192.168.1.2 ', 'ifname': 'if2,if1', 'nexthop-vrf': 'default,default', 'expiry': 'false'}} + ) + set_del_test(dut, "bfd", + "SET", + ("192.168.3.2", { + "state": "Up" + }), + {}, + {'set_default:2.2.2.0/24': {'nexthop': '192.168.2.2,192.168.1.2,192.168.3.2 ', 'ifname': 'if2,if1,if3', 'nexthop-vrf': 'default,default,default', 'expiry': 'false'}} + ) + + #test #2 + set_del_test(dut, "srt", + "SET", + ("2.2.2.0/24", { + "bfd": "true", + "nexthop": "192.168.1.2 , 192.168.2.2", + "ifname": "if1, if2", + }), + { + "del_default:default:192.168.3.2" : {} + }, + {'set_default:2.2.2.0/24': {'nexthop': '192.168.2.2,192.168.1.2 ', 'ifname': 'if2,if1', 'nexthop-vrf': 'default,default', 'expiry': 'false'}} + ) + + #test #3 + set_del_test(dut, "srt", + "DEL", + ("2.2.2.0/24", { + "bfd": "true", + "nexthop": "192.168.1.2 , 192.168.2.2", + "ifname": "if1, if2", + }), + { + "del_default:default:192.168.1.2" : {}, + "del_default:default:192.168.2.2" : {} + }, + {'del_default:2.2.2.0/24': {}} + ) + + # test add a non-bfd static route + set_del_test(dut, "srt", + "SET", + ("3.3.3.0/24", { + "nexthop": "192.168.1.2 , 192.168.2.2", + "ifname": "if1, if2", + }), + {}, + {} + ) + + # test delete a non-bfd static route + set_del_test(dut, "srt", + "DEL", + ("3.3.3.0/24", {}), + {}, + {} + ) + +def test_set_del_vrf(): + dut = constructor() + intf_setup(dut) + + set_del_test(dut, "srt", + "SET", + ("vrfred|2.2.2.0/24", { + "bfd": "true", + "nexthop": "192.168.1.2 , 192.168.2.2, 192.168.3.2", + "ifname": "if1, if2, if3", + "nexthop-vrf": "testvrf1, , default", + }), + { + "set_testvrf1:default:192.168.1.2" : {'multihop': 'true', 'rx_interval': '50', 'tx_interval': '50', 'multiplier': '3', 'local_addr': '192.168.1.1'}, + "set_vrfred:default:192.168.2.2" : {'multihop': 'true', 'rx_interval': '50', 'tx_interval': '50', 'multiplier': '3', 'local_addr': '192.168.2.1'}, + "set_default:default:192.168.3.2" : {'multihop': 'true', 'rx_interval': '50', 'tx_interval': '50', 'multiplier': '3', 'local_addr': '192.168.3.1'} + }, + {} + ) + + set_del_test(dut, "bfd", + "SET", + ("testvrf1|default|192.168.1.2", { + "state": "Up" + }), + {}, + {'set_vrfred:2.2.2.0/24': {'nexthop': '192.168.1.2', 'ifname': 'if1', 'nexthop-vrf': 'testvrf1', 'expiry': 'false'}} + ) + set_del_test(dut, "bfd", + "SET", + ("vrfred|default|192.168.2.2", { + "state": "Up" + }), + {}, + {'set_vrfred:2.2.2.0/24': {'nexthop': '192.168.2.2,192.168.1.2 ', 'ifname': 'if2,if1', 'nexthop-vrf': 'testvrf1,vrfred', 'expiry': 'false'}} + ) + set_del_test(dut, "bfd", + "SET", + ("default|default|192.168.3.2", { + "state": "Up" + }), + {}, + {'set_vrfred:2.2.2.0/24': {'nexthop': '192.168.2.2,192.168.1.2,192.168.3.2 ', 'ifname': 'if2,if1,if3', 'nexthop-vrf': 'testvrf1,vrfred,default', 'expiry': 'false'}} + ) + + set_del_test(dut, "srt", + "SET", + ("vrfred|2.2.2.0/24", { + "bfd": "true", + "nexthop": "192.168.1.2 , 192.168.2.2", + "ifname": "if1, if2", + "nexthop-vrf": "testvrf1,", + }), + { + "del_default:default:192.168.3.2" : {} + }, + {'set_vrfred:2.2.2.0/24': {'nexthop': '192.168.2.2,192.168.1.2 ', 'ifname': 'if2,if1', 'nexthop-vrf': 'vrfred, testvrf1', 'expiry': 'false'}} + ) + + set_del_test(dut, "srt", + "DEL", + ("vrfred|2.2.2.0/24", { }), + { + "del_testvrf1:default:192.168.1.2" : {}, + "del_vrfred:default:192.168.2.2" : {} + }, + {'del_vrfred:2.2.2.0/24': {}} + ) + +def test_bfd_del(): + dut = constructor() + intf_setup(dut) + + set_del_test(dut, "srt", + "SET", + ("2.2.2.0/24", { + "bfd": "true", + "nexthop": "192.168.1.2 , 192.168.2.2, 192.168.3.2", + "ifname": "if1, if2, if3", + }), + { + "set_default:default:192.168.1.2" : {'multihop': 'true', 'rx_interval': '50', 'tx_interval': '50', 'multiplier': '3', 'local_addr': '192.168.1.1'}, + "set_default:default:192.168.2.2" : {'multihop': 'true', 'rx_interval': '50', 'tx_interval': '50', 'multiplier': '3', 'local_addr': '192.168.2.1'}, + "set_default:default:192.168.3.2" : {'multihop': 'true', 'rx_interval': '50', 'tx_interval': '50', 'multiplier': '3', 'local_addr': '192.168.3.1'} + }, + {} + ) + + set_del_test(dut, "bfd", + "SET", + ("192.168.1.2", { + "state": "Up" + }), + {}, + {'set_default:2.2.2.0/24': {'nexthop': '192.168.1.2', 'ifname': 'if1', 'nexthop-vrf': 'default', 'expiry': 'false'}} + ) + set_del_test(dut, "bfd", + "SET", + ("192.168.2.2", { + "state": "Up" + }), + {}, + {'set_default:2.2.2.0/24': {'nexthop': '192.168.2.2,192.168.1.2 ', 'ifname': 'if2,if1', 'nexthop-vrf': 'default,default', 'expiry': 'false'}} + ) + set_del_test(dut, "bfd", + "SET", + ("192.168.3.2", { + "state": "Up" + }), + {}, + {'set_default:2.2.2.0/24': {'nexthop': '192.168.2.2,192.168.1.2,192.168.3.2 ', 'ifname': 'if2,if1,if3', 'nexthop-vrf': 'default,default,default', 'expiry': 'false'}} + ) + + #test bfd state del + set_del_test(dut, "bfd", + "DEL", + ({"192.168.2.2"}), + {}, + {'set_default:2.2.2.0/24': {'nexthop': '192.168.1.2,192.168.3.2 ', 'ifname': 'if1,if3', 'nexthop-vrf': 'default,default', 'expiry': 'false'}} + ) + +def test_set_2routes(): + dut = constructor() + intf_setup(dut) + + #test #4 + set_del_test(dut, "srt", + "SET", + ("2.2.2.0/24", { + "bfd": "true", + "nexthop": "192.168.1.2 , 192.168.2.2, 192.168.3.2", + "ifname": "if1, if2, if3", + }), + { + "set_default:default:192.168.1.2" : {'multihop': 'true', 'rx_interval': '50', 'tx_interval': '50', 'multiplier': '3', 'local_addr': '192.168.1.1'}, + "set_default:default:192.168.2.2" : {'multihop': 'true', 'rx_interval': '50', 'tx_interval': '50', 'multiplier': '3', 'local_addr': '192.168.2.1'}, + "set_default:default:192.168.3.2" : {'multihop': 'true', 'rx_interval': '50', 'tx_interval': '50', 'multiplier': '3', 'local_addr': '192.168.3.1'} + }, + {} + ) + + set_del_test(dut, "bfd", + "SET", + ("192.168.1.2", { + "state": "Up" + }), + {}, + {'set_default:2.2.2.0/24': {'nexthop': '192.168.1.2', 'ifname': 'if1', 'nexthop-vrf': 'default', 'expiry': 'false'}} + ) + set_del_test(dut, "bfd", + "SET", + ("192.168.2.2", { + "state": "Up" + }), + {}, + {'set_default:2.2.2.0/24': {'nexthop': '192.168.2.2,192.168.1.2 ', 'ifname': 'if2,if1', 'nexthop-vrf': 'default,default', 'expiry': 'false'}} + ) + set_del_test(dut, "bfd", + "SET", + ("192.168.3.2", { + "state": "Up" + }), + {}, + {'set_default:2.2.2.0/24': {'nexthop': '192.168.2.2,192.168.1.2,192.168.3.2 ', 'ifname': 'if2,if1,if3', 'nexthop-vrf': 'default,default,default', 'expiry': 'false'}} + ) + + set_del_test(dut, "srt", + "SET", + ("3.3.3.0/24", { + "bfd": "true", + "nexthop": "192.168.2.2", + "ifname": "if2", + }), + {}, + {'set_default:3.3.3.0/24': {'nexthop': '192.168.2.2', 'ifname': 'if2', 'nexthop-vrf': 'default', 'expiry': 'false'}} + ) + + #test #5 + set_del_test(dut, "bfd", + "SET", + ("192.168.2.2", { + "state": "Down" + }), + {}, + {'set_default:2.2.2.0/24': {'nexthop': '192.168.3.2,192.168.1.2 ', 'ifname': 'if3,if1', 'nexthop-vrf': 'default,default', 'expiry': 'false'}, 'del_default:3.3.3.0/24': {}} + ) + + #test #6 + set_del_test(dut, "bfd", + "SET", + ("192.168.2.2", { + "state": "Up" + }), + {}, + {'set_default:2.2.2.0/24': {'nexthop': '192.168.2.2,192.168.1.2,192.168.3.2 ', 'ifname': 'if2,if1,if3', 'nexthop-vrf': 'default,default,default', 'expiry': 'false'}, + 'set_default:3.3.3.0/24': {'nexthop': '192.168.2.2', 'ifname': 'if2', 'nexthop-vrf': 'default', 'expiry': 'false'}} + ) + +def test_set_bfd_change_hold(): + dut = constructor() + intf_setup(dut) + + #test #9 bfd: true -> false + set_del_test(dut, "srt", + "SET", + ("2.2.2.0/24", { + "bfd": "true", + "nexthop": "192.168.1.2 , 192.168.2.2, 192.168.3.2", + "ifname": "if1, if2, if3", + }), + { + "set_default:default:192.168.1.2" : {'multihop': 'true', 'rx_interval': '50', 'tx_interval': '50', 'multiplier': '3', 'local_addr': '192.168.1.1'}, + "set_default:default:192.168.2.2" : {'multihop': 'true', 'rx_interval': '50', 'tx_interval': '50', 'multiplier': '3', 'local_addr': '192.168.2.1'}, + "set_default:default:192.168.3.2" : {'multihop': 'true', 'rx_interval': '50', 'tx_interval': '50', 'multiplier': '3', 'local_addr': '192.168.3.1'} + }, + {} + ) + + set_del_test(dut, "bfd", + "SET", + ("192.168.1.2", { + "state": "Up" + }), + {}, + {'set_default:2.2.2.0/24': {'nexthop': '192.168.1.2', 'ifname': 'if1', 'nexthop-vrf': 'default', 'expiry': 'false'}} + ) + set_del_test(dut, "bfd", + "SET", + ("192.168.2.2", { + "state": "Up" + }), + {}, + {'set_default:2.2.2.0/24': {'nexthop': '192.168.2.2,192.168.1.2 ', 'ifname': 'if2,if1', 'nexthop-vrf': 'default,default', 'expiry': 'false'}} + ) + set_del_test(dut, "bfd", + "SET", + ("192.168.3.2", { + "state": "Up" + }), + {}, + {'set_default:2.2.2.0/24': {'nexthop': '192.168.2.2,192.168.1.2,192.168.3.2 ', 'ifname': 'if2,if1,if3', 'nexthop-vrf': 'default,default,default', 'expiry': 'false'}} + ) + + set_del_test(dut, "srt", + "SET", + ("2.2.2.0/24", { + "bfd": "false", + "nexthop": "192.168.1.2 , 192.168.2.2, 192.168.3.2", + "ifname": "if1, if2, if3", + }), + { + "del_default:default:192.168.1.2" : {}, + "del_default:default:192.168.2.2" : {}, + "del_default:default:192.168.3.2" : {} + }, + { + 'del_default:2.2.2.0/24': {} + } + ) + return + + #test #10 'bfd': false --> true, write original rout first + set_del_test(dut, "srt", + "SET", + ("2.2.2.0/24", { + "bfd": "true", + "nexthop": "192.168.1.2 , 192.168.2.2, 192.168.3.2", + "ifname": "if1, if2, if3", + }), + { + "set_default:default:192.168.1.2" : {'multihop': 'true', 'rx_interval': '50', 'tx_interval': '50', 'multiplier': '3', 'local_addr': '192.168.1.1'}, + "set_default:default:192.168.2.2" : {'multihop': 'true', 'rx_interval': '50', 'tx_interval': '50', 'multiplier': '3', 'local_addr': '192.168.2.1'}, + "set_default:default:192.168.3.2" : {'multihop': 'true', 'rx_interval': '50', 'tx_interval': '50', 'multiplier': '3', 'local_addr': '192.168.3.1'} + }, + {'set_default:2.2.2.0/24': {'bfd':'false', 'nexthop': '192.168.2.2,192.168.1.2,192.168.3.2 ', 'ifname': 'if2,if1,if3', 'expiry': 'false'}} + ) + + set_del_test(dut, "bfd", + "SET", + ("192.168.1.2", { + "state": "Up" + }), + {}, + {'set_default:2.2.2.0/24': {'nexthop': '192.168.1.2', 'ifname': 'if1', 'nexthop-vrf': 'default', 'expiry': 'false'}} + ) + set_del_test(dut, "bfd", + "SET", + ("192.168.2.2", { + "state": "Up" + }), + {}, + {'set_default:2.2.2.0/24': {'nexthop': '192.168.2.2,192.168.1.2 ', 'ifname': 'if2,if1', 'nexthop-vrf': 'default,default', 'expiry': 'false'}} + ) + set_del_test(dut, "bfd", + "SET", + ("192.168.3.2", { + "state": "Up" + }), + {}, + {'set_default:2.2.2.0/24': {'nexthop': '192.168.2.2,192.168.1.2,192.168.3.2 ', 'ifname': 'if2,if1,if3', 'nexthop-vrf': 'default,default,default', 'expiry': 'false'}} + ) + + +def test_set_bfd_change_no_hold(): + dut = constructor() + intf_setup(dut) + + #setup runtime "bfd"="false" condition`` + set_del_test(dut, "srt", + "SET", + ("2.2.2.0/24", { + "bfd": "true", + "nexthop": "192.168.1.2 , 192.168.2.2, 192.168.3.2", + "ifname": "if1, if2, if3", + }), + { + "set_default:default:192.168.1.2" : {'multihop': 'true', 'rx_interval': '50', 'tx_interval': '50', 'multiplier': '3', 'local_addr': '192.168.1.1'}, + "set_default:default:192.168.2.2" : {'multihop': 'true', 'rx_interval': '50', 'tx_interval': '50', 'multiplier': '3', 'local_addr': '192.168.2.1'}, + "set_default:default:192.168.3.2" : {'multihop': 'true', 'rx_interval': '50', 'tx_interval': '50', 'multiplier': '3', 'local_addr': '192.168.3.1'} + }, + {} + ) + set_del_test(dut, "bfd", + "SET", + ("192.168.1.2", { + "state": "Up" + }), + {}, + {'set_default:2.2.2.0/24': {'nexthop': '192.168.1.2', 'ifname': 'if1', 'nexthop-vrf': 'default', 'expiry': 'false'}} + ) + set_del_test(dut, "bfd", + "SET", + ("192.168.2.2", { + "state": "Up" + }), + {}, + {'set_default:2.2.2.0/24': {'nexthop': '192.168.2.2,192.168.1.2 ', 'ifname': 'if2,if1', 'nexthop-vrf': 'default,default', 'expiry': 'false'}} + ) + set_del_test(dut, "bfd", + "SET", + ("192.168.3.2", { + "state": "Up" + }), + {}, + {'set_default:2.2.2.0/24': {'nexthop': '192.168.2.2,192.168.1.2,192.168.3.2 ', 'ifname': 'if2,if1,if3', 'nexthop-vrf': 'default,default,default', 'expiry': 'false'}} + ) + set_del_test(dut, "srt", + "SET", + ("3.3.3.0/24", { + "bfd": "true", + "nexthop": "192.168.2.2", + "ifname": "if2", + }), + {}, + {'set_default:3.3.3.0/24': {'nexthop': '192.168.2.2', 'ifname': 'if2', 'nexthop-vrf': 'default', 'expiry': 'false'}} + ) + + set_del_test(dut, "srt", + "SET", + ("2.2.2.0/24", { + "bfd": "false", + "nexthop": "192.168.1.2 , 192.168.2.2, 192.168.3.2", + "ifname": "if1, if2, if3", + }), + { + "del_default:default:192.168.1.2" : {}, + "del_default:default:192.168.3.2" : {} + }, + { + 'del_default:2.2.2.0/24': {} + } + ) + + #test #10 change 'bfd': false to true, because the bfd session "default:default:192.168.2.2" is up, so add that nexthop right after "bfd" change to "true" + set_del_test(dut, "srt", + "SET", + ("2.2.2.0/24", { + "bfd": "true", + "nexthop": "192.168.1.2 , 192.168.2.2, 192.168.3.2", + "ifname": "if1, if2, if3", + }), + { + "set_default:default:192.168.1.2" : {'multihop': 'true', 'rx_interval': '50', 'tx_interval': '50', 'multiplier': '3', 'local_addr': '192.168.1.1'}, + "set_default:default:192.168.3.2" : {'multihop': 'true', 'rx_interval': '50', 'tx_interval': '50', 'multiplier': '3', 'local_addr': '192.168.3.1'} + }, + {'set_default:2.2.2.0/24': {'nexthop': '192.168.2.2', 'ifname': 'if2', 'nexthop-vrf': 'default', 'expiry': 'false'}} + ) + + set_del_test(dut, "bfd", + "SET", + ("192.168.1.2", { + "state": "Up" + }), + {}, + {'set_default:2.2.2.0/24': {'nexthop': '192.168.2.2,192.168.1.2 ', 'ifname': 'if2,if1', 'nexthop-vrf': 'default,default', 'expiry': 'false'}} + ) + set_del_test(dut, "bfd", + "SET", + ("192.168.2.2", { + "state": "Up" + }), + {}, + {} + ) + set_del_test(dut, "bfd", + "SET", + ("192.168.3.2", { + "state": "Up" + }), + {}, + {'set_default:2.2.2.0/24': {'nexthop': '192.168.2.2,192.168.1.2,192.168.3.2 ', 'ifname': 'if2,if1,if3', 'nexthop-vrf': 'default,default,default', 'expiry': 'false'}} + ) + + +