diff --git a/orchagent/aclorch.cpp b/orchagent/aclorch.cpp index 9df574b7eb..2f98be5f83 100644 --- a/orchagent/aclorch.cpp +++ b/orchagent/aclorch.cpp @@ -993,11 +993,6 @@ bool AclRuleMirror::validateAddAction(string attr_name, string attr_value) return false; } - if (!m_pMirrorOrch->sessionExists(attr_value)) - { - return false; - } - m_sessionName = attr_value; return true; @@ -1084,6 +1079,14 @@ bool AclRuleMirror::create() bool state = false; sai_object_id_t oid = SAI_NULL_OBJECT_ID; + SWSS_LOG_NOTICE("Creating mirror rule %s", m_sessionName.c_str()); + + if (!m_pMirrorOrch->sessionExists(m_sessionName)) + { + SWSS_LOG_ERROR("Mirror rule references mirror session \"%s\" that does not exist yet", m_sessionName.c_str()); + return false; + } + if (!m_pMirrorOrch->getSessionStatus(m_sessionName, state)) { throw runtime_error("Failed to get mirror session state"); @@ -2536,7 +2539,16 @@ void AclOrch::doAclRuleTask(Consumer &consumer) } - newRule = AclRule::makeShared(type, this, m_mirrorOrch, m_dTelOrch, rule_id, table_id, t); + try + { + newRule = AclRule::makeShared(type, this, m_mirrorOrch, m_dTelOrch, rule_id, table_id, t); + } + catch (exception &e) + { + SWSS_LOG_ERROR("Error while creating ACL rule %s: %s", rule_id.c_str(), e.what()); + it = consumer.m_toSync.erase(it); + return; + } for (const auto& itr : kfvFieldsValues(t)) { diff --git a/orchagent/aclorch.h b/orchagent/aclorch.h index d5d215c3e8..fb2efb74f1 100644 --- a/orchagent/aclorch.h +++ b/orchagent/aclorch.h @@ -411,6 +411,8 @@ class AclOrch : public Orch, public Observer bool m_isCombinedMirrorV6Table = true; map m_mirrorTableCapabilities; + using Orch::doTask; // Allow access to the basic doTask + private: void doTask(Consumer &consumer); void doAclTableTask(Consumer &consumer); diff --git a/orchagent/mirrororch.cpp b/orchagent/mirrororch.cpp index 7af653c037..4336977e4c 100644 --- a/orchagent/mirrororch.cpp +++ b/orchagent/mirrororch.cpp @@ -82,9 +82,6 @@ bool MirrorOrch::bake() { SWSS_LOG_ENTER(); - // Freeze the route update during orchagent restoration - m_freeze = true; - deque entries; vector keys; m_mirrorTable.getKeys(keys); @@ -129,23 +126,6 @@ bool MirrorOrch::bake() return Orch::bake(); } -bool MirrorOrch::postBake() -{ - SWSS_LOG_ENTER(); - - SWSS_LOG_NOTICE("Start MirrorOrch post-baking"); - - // Unfreeze the route update - m_freeze = false; - - Orch::doTask(); - - // Clean up the recovery cache - m_recoverySessionMap.clear(); - - return Orch::postBake(); -} - void MirrorOrch::update(SubjectType type, void *cntx) { SWSS_LOG_ENTER(); @@ -260,7 +240,7 @@ bool MirrorOrch::decreaseRefCount(const string& name) return true; } -void MirrorOrch::createEntry(const string& key, const vector& data) +task_process_status MirrorOrch::createEntry(const string& key, const vector& data) { SWSS_LOG_ENTER(); @@ -269,7 +249,7 @@ void MirrorOrch::createEntry(const string& key, const vector& d { SWSS_LOG_NOTICE("Failed to create session, session %s already exists", key.c_str()); - return; + return task_process_status::task_duplicated; } string platform = getenv("platform") ? getenv("platform") : ""; @@ -284,7 +264,7 @@ void MirrorOrch::createEntry(const string& key, const vector& d if (!entry.srcIp.isV4()) { SWSS_LOG_ERROR("Unsupported version of sessions %s source IP address\n", key.c_str()); - return; + return task_process_status::task_invalid_entry; } } else if (fvField(i) == MIRROR_SESSION_DST_IP) @@ -293,7 +273,7 @@ void MirrorOrch::createEntry(const string& key, const vector& d if (!entry.dstIp.isV4()) { SWSS_LOG_ERROR("Unsupported version of sessions %s destination IP address\n", key.c_str()); - return; + return task_process_status::task_invalid_entry; } } else if (fvField(i) == MIRROR_SESSION_GRE_TYPE) @@ -318,7 +298,7 @@ void MirrorOrch::createEntry(const string& key, const vector& d { SWSS_LOG_ERROR("Failed to get policer %s", fvValue(i).c_str()); - return; + return task_process_status::task_need_retry; } m_policerOrch->increaseRefCount(fvValue(i)); @@ -327,18 +307,18 @@ void MirrorOrch::createEntry(const string& key, const vector& d else { SWSS_LOG_ERROR("Failed to parse session %s configuration. Unknown attribute %s.\n", key.c_str(), fvField(i).c_str()); - return; + return task_process_status::task_invalid_entry; } } catch (const exception& e) { SWSS_LOG_ERROR("Failed to parse session %s attribute %s error: %s.", key.c_str(), fvField(i).c_str(), e.what()); - return; + return task_process_status::task_invalid_entry; } catch (...) { SWSS_LOG_ERROR("Failed to parse session %s attribute %s. Unknown error has been occurred", key.c_str(), fvField(i).c_str()); - return; + return task_process_status::task_failed; } } @@ -350,6 +330,8 @@ void MirrorOrch::createEntry(const string& key, const vector& d // Attach the destination IP to the routeOrch m_routeOrch->attach(this, entry.dstIp); + + return task_process_status::task_success; } task_process_status MirrorOrch::deleteEntry(const string& name) @@ -1135,11 +1117,6 @@ void MirrorOrch::doTask(Consumer& consumer) { SWSS_LOG_ENTER(); - if (m_freeze) - { - return; - } - if (!gPortsOrch->isPortReady()) { return; @@ -1152,26 +1129,32 @@ void MirrorOrch::doTask(Consumer& consumer) string key = kfvKey(t); string op = kfvOp(t); + task_process_status task_status = task_process_status::task_failed; if (op == SET_COMMAND) { - createEntry(key, kfvFieldsValues(t)); + task_status = createEntry(key, kfvFieldsValues(t)); } else if (op == DEL_COMMAND) { - auto task_status = deleteEntry(key); - // Specifically retry the task when asked - if (task_status == task_process_status::task_need_retry) - { - it++; - continue; - } + task_status = deleteEntry(key); } else { SWSS_LOG_ERROR("Unknown operation type %s", op.c_str()); } - consumer.m_toSync.erase(it++); + // Specifically retry the task when asked + if (task_status == task_process_status::task_need_retry) + { + it++; + } + else + { + consumer.m_toSync.erase(it++); + } } + + // Clear any recovery state that might be leftover from warm reboot + m_recoverySessionMap.clear(); } diff --git a/orchagent/mirrororch.h b/orchagent/mirrororch.h index f9333c49c8..c967cb62f9 100644 --- a/orchagent/mirrororch.h +++ b/orchagent/mirrororch.h @@ -70,7 +70,6 @@ class MirrorOrch : public Orch, public Observer, public Subject PortsOrch *portOrch, RouteOrch *routeOrch, NeighOrch *neighOrch, FdbOrch *fdbOrch, PolicerOrch *policerOrch); bool bake() override; - bool postBake() override; void update(SubjectType, void *); bool sessionExists(const string&); bool getSessionStatus(const string&, bool&); @@ -78,6 +77,8 @@ class MirrorOrch : public Orch, public Observer, public Subject bool increaseRefCount(const string&); bool decreaseRefCount(const string&); + using Orch::doTask; // Allow access to the basic doTask + private: PortsOrch *m_portsOrch; RouteOrch *m_routeOrch; @@ -91,9 +92,7 @@ class MirrorOrch : public Orch, public Observer, public Subject // session_name -> VLAN | monitor_port_alias | next_hop_ip map m_recoverySessionMap; - bool m_freeze = false; - - void createEntry(const string&, const vector&); + task_process_status createEntry(const string&, const vector&); task_process_status deleteEntry(const string&); bool activateSession(const string&, MirrorEntry&); diff --git a/orchagent/orch.cpp b/orchagent/orch.cpp index 22f8c11910..5a3001c12f 100644 --- a/orchagent/orch.cpp +++ b/orchagent/orch.cpp @@ -256,13 +256,6 @@ bool Orch::bake() return true; } -bool Orch::postBake() -{ - SWSS_LOG_ENTER(); - - return true; -} - /* - Validates reference has proper format which is [table_name:object_name] - validates table_name exists diff --git a/orchagent/orch.h b/orchagent/orch.h index 496a33c518..a76f766a5d 100644 --- a/orchagent/orch.h +++ b/orchagent/orch.h @@ -49,7 +49,8 @@ typedef enum task_invalid_entry, task_failed, task_need_retry, - task_ignore + task_ignore, + task_duplicated } task_process_status; typedef map object_map; @@ -182,8 +183,6 @@ class Orch // Prepare for warm start if Redis contains valid input data // otherwise fallback to cold start virtual bool bake(); - // Clean up the state set in bake() - virtual bool postBake(); /* Iterate all consumers in m_consumerMap and run doTask(Consumer) */ virtual void doTask(); diff --git a/orchagent/orchdaemon.cpp b/orchagent/orchdaemon.cpp index a19143446e..684db18720 100644 --- a/orchagent/orchdaemon.cpp +++ b/orchagent/orchdaemon.cpp @@ -29,6 +29,7 @@ IntfsOrch *gIntfsOrch; NeighOrch *gNeighOrch; RouteOrch *gRouteOrch; AclOrch *gAclOrch; +MirrorOrch *gMirrorOrch; CrmOrch *gCrmOrch; BufferOrch *gBufferOrch; SwitchOrch *gSwitchOrch; @@ -124,7 +125,7 @@ bool OrchDaemon::init() TableConnector stateDbMirrorSession(m_stateDb, STATE_MIRROR_SESSION_TABLE_NAME); TableConnector confDbMirrorSession(m_configDb, CFG_MIRROR_SESSION_TABLE_NAME); - MirrorOrch *mirror_orch = new MirrorOrch(stateDbMirrorSession, confDbMirrorSession, gPortsOrch, gRouteOrch, gNeighOrch, gFdbOrch, policer_orch); + gMirrorOrch = new MirrorOrch(stateDbMirrorSession, confDbMirrorSession, gPortsOrch, gRouteOrch, gNeighOrch, gFdbOrch, policer_orch); TableConnector confDbAclTable(m_configDb, CFG_ACL_TABLE_NAME); TableConnector confDbAclRuleTable(m_configDb, CFG_ACL_RULE_TABLE_NAME); @@ -191,10 +192,10 @@ bool OrchDaemon::init() m_orchList.push_back(dtel_orch); } TableConnector stateDbSwitchTable(m_stateDb, "SWITCH_CAPABILITY"); - gAclOrch = new AclOrch(acl_table_connectors, stateDbSwitchTable, gPortsOrch, mirror_orch, gNeighOrch, gRouteOrch, dtel_orch); + gAclOrch = new AclOrch(acl_table_connectors, stateDbSwitchTable, gPortsOrch, gMirrorOrch, gNeighOrch, gRouteOrch, dtel_orch); m_orchList.push_back(gFdbOrch); - m_orchList.push_back(mirror_orch); + m_orchList.push_back(gMirrorOrch); m_orchList.push_back(gAclOrch); m_orchList.push_back(vnet_orch); m_orchList.push_back(vnet_rt_orch); @@ -486,18 +487,24 @@ bool OrchDaemon::warmRestoreAndSyncUp() for (auto it = 0; it < 4; it++) { - SWSS_LOG_DEBUG("The current iteration is %d", it); + SWSS_LOG_DEBUG("The current doTask iteration is %d", it); for (Orch *o : m_orchList) { + if (o == gMirrorOrch) { + SWSS_LOG_DEBUG("Skipping mirror processing until the end"); + continue; + } + o->doTask(); } } - for (Orch *o : m_orchList) - { - o->postBake(); - } + // MirrorOrch depends on everything else being settled before it can run, + // and mirror ACL rules depend on MirrorOrch, so run these two at the end + // after the rest of the data has been processed. + gMirrorOrch->doTask(); + gAclOrch->doTask(); /* * At this point, all the pre-existing data should have been processed properly, and diff --git a/tests/conftest.py b/tests/conftest.py index 7bf39ec21a..9f0108a7cb 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -12,6 +12,16 @@ import subprocess from datetime import datetime from swsscommon import swsscommon +from dvslib.dvs_database import DVSDatabase +from dvslib.dvs_common import PollingConfig, wait_for_result +from dvslib.dvs_acl import DVSAcl +from dvslib import dvs_mirror +from dvslib import dvs_policer + +# FIXME: For the sake of stabilizing the PR pipeline we currently assume there are 32 front-panel +# ports in the system (much like the rest of the test suite). This should be adjusted to accomodate +# a dynamic number of ports. GitHub Issue: Azure/sonic-swss#1384. +NUM_PORTS = 32 def ensure_system(cmd): rc = os.WEXITSTATUS(os.system(cmd)) @@ -23,66 +33,73 @@ def pytest_addoption(parser): help="dvs name") parser.addoption("--keeptb", action="store_true", default=False, help="keep testbed after test") + parser.addoption("--imgname", action="store", default="docker-sonic-vs", + help="image name") -class AsicDbValidator(object): - def __init__(self, dvs): - self.adb = swsscommon.DBConnector(1, dvs.redis_sock, 0) +class AsicDbValidator(DVSDatabase): + def __init__(self, db_id, connector): + DVSDatabase.__init__(self, db_id, connector) + self._wait_for_asic_db_to_initialize() + self._populate_default_asic_db_values() + self._generate_oid_to_interface_mapping() - # get default dot1q vlan id - atbl = swsscommon.Table(self.adb, "ASIC_STATE:SAI_OBJECT_TYPE_VLAN") + def _wait_for_asic_db_to_initialize(self): + """Wait up to 30 seconds for the default fields to appear in ASIC DB.""" + def _verify_db_contents(): + # We expect only the default VLAN + if len(self.get_keys("ASIC_STATE:SAI_OBJECT_TYPE_VLAN")) != 1: + return (False, None) - keys = atbl.getKeys() - assert len(keys) == 1 - self.default_vlan_id = keys[0] + if len(self.get_keys("ASIC_STATE:SAI_OBJECT_TYPE_HOSTIF")) < NUM_PORTS: + return (False, None) + + if len(self.get_keys("ASIC_STATE:SAI_OBJECT_TYPE_ACL_ENTRY")) != 2: + return (False, None) + + return (True, None) + + # Verify that ASIC DB has been fully initialized + init_polling_config = PollingConfig(2, 30, strict=True) + wait_for_result(_verify_db_contents, init_polling_config) - # build port oid to front port name mapping + def _generate_oid_to_interface_mapping(self): + """Generate the OID->Name mappings for ports and host interfaces.""" self.portoidmap = {} self.portnamemap = {} self.hostifoidmap = {} self.hostifnamemap = {} - atbl = swsscommon.Table(self.adb, "ASIC_STATE:SAI_OBJECT_TYPE_HOSTIF") - keys = atbl.getKeys() - assert len(keys) == 32 - for k in keys: - (status, fvs) = atbl.get(k) - - assert status == True - - for fv in fvs: - if fv[0] == "SAI_HOSTIF_ATTR_OBJ_ID": - port_oid = fv[1] - elif fv[0] == "SAI_HOSTIF_ATTR_NAME": - port_name = fv[1] + host_intfs = self.get_keys("ASIC_STATE:SAI_OBJECT_TYPE_HOSTIF") + for intf in host_intfs: + fvs = self.get_entry("ASIC_STATE:SAI_OBJECT_TYPE_HOSTIF", intf) + port_oid = fvs.get("SAI_HOSTIF_ATTR_OBJ_ID") + port_name = fvs.get("SAI_HOSTIF_ATTR_NAME") self.portoidmap[port_oid] = port_name self.portnamemap[port_name] = port_oid - self.hostifoidmap[k] = port_name - self.hostifnamemap[port_name] = k + self.hostifoidmap[intf] = port_name + self.hostifnamemap[port_name] = intf - # get default acl table and acl rules - atbl = swsscommon.Table(self.adb, "ASIC_STATE:SAI_OBJECT_TYPE_ACL_TABLE") - keys = atbl.getKeys() + def _populate_default_asic_db_values(self): + # Get default .1Q Vlan ID + self.default_vlan_id = self.get_keys("ASIC_STATE:SAI_OBJECT_TYPE_VLAN")[0] - assert len(keys) >= 1 - self.default_acl_tables = keys + self.default_acl_tables = self.get_keys("ASIC_STATE:SAI_OBJECT_TYPE_ACL_TABLE") + self.default_acl_entries = self.get_keys("ASIC_STATE:SAI_OBJECT_TYPE_ACL_ENTRY") - atbl = swsscommon.Table(self.adb, "ASIC_STATE:SAI_OBJECT_TYPE_ACL_ENTRY") - keys = atbl.getKeys() + self.default_copp_policers = self.get_keys("ASIC_STATE:SAI_OBJECT_TYPE_POLICER") - assert len(keys) == 2 - self.default_acl_entries = keys +class ApplDbValidator(DVSDatabase): + NEIGH_TABLE = "NEIGH_TABLE" -class ApplDbValidator(object): - def __init__(self, dvs): - appl_db = swsscommon.DBConnector(swsscommon.APPL_DB, dvs.redis_sock, 0) - self.neighTbl = swsscommon.Table(appl_db, "NEIGH_TABLE") + def __init__(self, db_id, connector): + DVSDatabase.__init__(self, db_id, connector) - def __del__(self): + def destroy(self): # Make sure no neighbors on physical interfaces - keys = self.neighTbl.getKeys() - for key in keys: - m = re.match("eth(\d+)", key) + neighbors = self.get_keys(self.NEIGH_TABLE) + for neighbor in neighbors: + m = re.match(r"eth(\d+)", neighbor) if not m: continue assert int(m.group(1)) > 0 @@ -142,7 +159,14 @@ def runcmd_output(self, cmd): return subprocess.check_output("ip netns exec %s %s" % (self.nsname, cmd), shell=True) class DockerVirtualSwitch(object): - def __init__(self, name=None, keeptb=False): + APPL_DB_ID = 0 + ASIC_DB_ID = 1 + COUNTERS_DB_ID = 2 + CONFIG_DB_ID = 4 + FLEX_COUNTER_DB_ID = 5 + STATE_DB_ID = 6 + + def __init__(self, name=None, imgname=None, keeptb=False, fakeplatform=None): self.basicd = ['redis-server', 'rsyslogd'] self.swssd = ['orchagent', @@ -158,6 +182,9 @@ def __init__(self, name=None, keeptb=False): self.alld = self.basicd + self.swssd + self.syncd + self.rtd + self.teamd self.client = docker.from_env() + if subprocess.check_call(["/sbin/modprobe", "team"]) != 0: + raise NameError("cannot install kernel team module") + self.ctn = None if keeptb: self.cleanup = False @@ -208,12 +235,20 @@ def __init__(self, name=None, keeptb=False): self.mount = "/var/run/redis-vs/{}".format(self.ctn_sw.name) os.system("mkdir -p {}".format(self.mount)) + self.environment = ["fake_platform={}".format(fakeplatform)] if fakeplatform else [] + # create virtual switch container - self.ctn = self.client.containers.run('docker-sonic-vs', privileged=True, detach=True, + self.ctn = self.client.containers.run(imgname, privileged=True, detach=True, + environment=self.environment, network_mode="container:%s" % self.ctn_sw.name, volumes={ self.mount: { 'bind': '/var/run/redis', 'mode': 'rw' } }) - self.appldb = None + self.app_db = None + self.asicdb = None + self.counters_db = None + self.config_db = None + self.flex_db = None + self.state_db = None self.redis_sock = self.mount + '/' + "redis.sock" try: # temp fix: remove them once they are moved to vs start.sh @@ -222,14 +257,14 @@ def __init__(self, name=None, keeptb=False): self.ctn.exec_run("sysctl -w net.ipv6.conf.eth%d.disable_ipv6=1" % (i + 1)) self.check_ready() self.init_asicdb_validator() - self.appldb = ApplDbValidator(self) + self.app_db = ApplDbValidator(self.APPL_DB_ID, self.redis_sock) except: self.destroy() raise def destroy(self): - if self.appldb: - del self.appldb + if self.app_db: + self.app_db.destroy() if self.cleanup: self.ctn.remove(force=True) self.ctn_sw.remove(force=True) @@ -279,6 +314,30 @@ def check_ready(self, timeout=30): time.sleep(1) + def check_swss_ready(self, timeout=300): + """Verify that SWSS is ready to receive inputs. + + Almost every part of orchagent depends on ports being created and initialized + before they can proceed with their processing. If we start the tests after orchagent + has started running but before it has had time to initialize all the ports, then the + first several tests will fail. + """ + num_ports = NUM_PORTS + + # Verify that all ports have been initialized and configured + app_db = self.get_app_db() + startup_polling_config = PollingConfig(5, timeout, strict=True) + + def _polling_function(): + port_table_keys = app_db.get_keys("PORT_TABLE") + return ("PortInitDone" in port_table_keys and "PortConfigDone" in port_table_keys, None) + + wait_for_result(_polling_function, startup_polling_config) + + # Verify that all ports have been created + asic_db = self.get_asic_db() + asic_db.wait_for_n_keys("ASIC_STATE:SAI_OBJECT_TYPE_PORT", num_ports + 1) # +1 CPU Port + def net_cleanup(self): """clean up network, remove extra links""" @@ -326,7 +385,7 @@ def start_zebra(dvs): time.sleep(5) def stop_zebra(dvs): - dvs.runcmd(['sh', '-c', 'pkill -x zebra']) + dvs.runcmd(['sh', '-c', 'pkill -9 zebra']) time.sleep(1) def start_fpmsyncd(dvs): @@ -340,7 +399,7 @@ def stop_fpmsyncd(dvs): time.sleep(1) def init_asicdb_validator(self): - self.asicdb = AsicDbValidator(self) + self.asicdb = AsicDbValidator(self.ASIC_DB_ID, self.redis_sock) def runcmd(self, cmd): res = self.ctn.exec_run(cmd) @@ -540,6 +599,20 @@ def get_map_iface_bridge_port_id(self, asic_db): return iface_2_bridge_port_id + def get_vlan_oid(self, asic_db, vlan_id): + tbl = swsscommon.Table(asic_db, "ASIC_STATE:SAI_OBJECT_TYPE_VLAN") + keys = tbl.getKeys() + + for key in keys: + status, fvs = tbl.get(key) + assert status, "Error reading from table %s" % "ASIC_STATE:SAI_OBJECT_TYPE_VLAN" + + for k, v in fvs: + if k == "SAI_VLAN_ATTR_VLAN_ID" and v == vlan_id: + return True, key + + return False, "Not found vlan id %s" % vlan_id + def is_table_entry_exists(self, db, table, keyregex, attributes): tbl = swsscommon.Table(db, table) keys = tbl.getKeys() @@ -629,11 +702,15 @@ def is_fdb_entry_exists(self, db, table, key_values, attributes): except ValueError: d_key = json.loads('{' + key + '}') + key_found = True + for k, v in key_values: if k not in d_key or v != d_key[k]: - continue + key_found = False + break - key_found = True + if not key_found: + continue status, fvs = tbl.get(key) assert status, "Error reading from table %s" % table @@ -705,6 +782,7 @@ def add_ip_address(self, interface, ip): tbl_name = "INTERFACE" tbl = swsscommon.Table(self.cdb, tbl_name) fvs = swsscommon.FieldValuePairs([("NULL", "NULL")]) + tbl.set(interface, fvs) tbl.set(interface + "|" + ip, fvs) time.sleep(1) @@ -716,7 +794,8 @@ def remove_ip_address(self, interface, ip): else: tbl_name = "INTERFACE" tbl = swsscommon.Table(self.cdb, tbl_name) - tbl._del(interface + "|" + ip); + tbl._del(interface + "|" + ip) + tbl._del(interface) time.sleep(1) def set_mtu(self, interface, mtu): @@ -738,6 +817,51 @@ def add_neighbor(self, interface, ip, mac): tbl.set(interface + ":" + ip, fvs) time.sleep(1) + def remove_neighbor(self, interface, ip): + tbl = swsscommon.ProducerStateTable(self.pdb, "NEIGH_TABLE") + tbl._del(interface + ":" + ip) + time.sleep(1) + + # deps: mirror_port_erspan, warm_reboot + def add_route(self, prefix, nexthop): + self.runcmd("ip route add " + prefix + " via " + nexthop) + time.sleep(1) + + # deps: mirror_port_erspan, warm_reboot + def change_route(self, prefix, nexthop): + self.runcmd("ip route change " + prefix + " via " + nexthop) + time.sleep(1) + + # deps: warm_reboot + def change_route_ecmp(self, prefix, nexthops): + cmd = "" + for nexthop in nexthops: + cmd += " nexthop via " + nexthop + + self.runcmd("ip route change " + prefix + cmd) + time.sleep(1) + + # deps: acl, mirror_port_erspan + def remove_route(self, prefix): + self.runcmd("ip route del " + prefix) + time.sleep(1) + + # deps: mirror_port_erspan + def create_fdb(self, vlan, mac, interface): + tbl = swsscommon.ProducerStateTable(self.pdb, "FDB_TABLE") + fvs = swsscommon.FieldValuePairs([("port", interface), + ("type", "dynamic")]) + tbl.set("Vlan" + vlan + ":" + mac, fvs) + time.sleep(1) + + # deps: mirror_port_erspan + def remove_fdb(self, vlan, mac): + tbl = swsscommon.ProducerStateTable(self.pdb, "FDB_TABLE") + tbl._del("Vlan" + vlan + ":" + mac) + time.sleep(1) + + # deps: acl, fdb_update, fdb, intf_mac, mirror_port_erspan, mirror_port_span, + # policer, port_dpb_vlan, vlan def setup_db(self): self.pdb = swsscommon.DBConnector(0, self.redis_sock, 0) self.adb = swsscommon.DBConnector(1, self.redis_sock, 0) @@ -771,13 +895,66 @@ def setReadOnlyAttr(self, obj, attr, val): fvp = swsscommon.FieldValuePairs([(attr, val)]) key = "SAI_OBJECT_TYPE_SWITCH:" + swRid - ntf.send("set_ro", key, fvp) + # explicit convert unicode string to str for python2 + ntf.send("set_ro", str(key), fvp) + + # FIXME: Now that ApplDbValidator is using DVSDatabase we should converge this with + # that implementation. Save it for a follow-up PR. + def get_app_db(self): + if not self.app_db: + self.app_db = DVSDatabase(self.APPL_DB_ID, self.redis_sock) + + return self.app_db + + # FIXME: Now that AsicDbValidator is using DVSDatabase we should converge this with + # that implementation. Save it for a follow-up PR. + def get_asic_db(self): + if not self.asicdb: + db = DVSDatabase(self.ASIC_DB_ID, self.redis_sock) + db.default_acl_tables = self.asicdb.default_acl_tables + db.default_acl_entries = self.asicdb.default_acl_entries + db.default_copp_policers = self.asicdb.default_copp_policers + db.port_name_map = self.asicdb.portnamemap + db.default_vlan_id = self.asicdb.default_vlan_id + db.port_to_id_map = self.asicdb.portoidmap + db.hostif_name_map = self.asicdb.hostifnamemap + self.asicdb = db + + return self.asicdb + + def get_counters_db(self): + if not self.counters_db: + self.counters_db = DVSDatabase(self.COUNTERS_DB_ID, self.redis_sock) + + return self.counters_db + + def get_config_db(self): + if not self.config_db: + self.config_db = DVSDatabase(self.CONFIG_DB_ID, self.redis_sock) + + return self.config_db + + def get_flex_db(self): + if not self.flex_db: + self.flex_db = DVSDatabase(self.FLEX_COUNTER_DB_ID, self.redis_sock) + + return self.flex_db + + def get_state_db(self): + if not self.state_db: + self.state_db = DVSDatabase(self.STATE_DB_ID, self.redis_sock) + + return self.state_db + + @pytest.yield_fixture(scope="module") def dvs(request): name = request.config.getoption("--dvsname") keeptb = request.config.getoption("--keeptb") - dvs = DockerVirtualSwitch(name, keeptb) + imgname = request.config.getoption("--imgname") + fakeplatform = getattr(request.module, "DVS_FAKE_PLATFORM", None) + dvs = DockerVirtualSwitch(name, imgname, keeptb, fakeplatform) yield dvs if name == None: dvs.get_logs(request.module.__name__) @@ -790,3 +967,28 @@ def testlog(request, dvs): dvs.runcmd("logger === start test %s ===" % request.node.name) yield testlog dvs.runcmd("logger === finish test %s ===" % request.node.name) + +################# DVSLIB module manager fixtures ############################# +@pytest.fixture(scope="module") +def dvs_acl(dvs): + return DVSAcl(dvs.get_asic_db(), + dvs.get_config_db(), + dvs.get_state_db(), + dvs.get_counters_db()) + + +@pytest.yield_fixture(scope="module") +def dvs_mirror_manager(dvs): + return dvs_mirror.DVSMirror(dvs.get_asic_db(), + dvs.get_config_db(), + dvs.get_state_db(), + dvs.get_counters_db(), + dvs.get_app_db()) + + +@pytest.yield_fixture(scope="module") +def dvs_policer_manager(dvs): + return dvs_policer.DVSPolicer(dvs.get_asic_db(), + dvs.get_config_db()) + + diff --git a/tests/dvslib/__init__.py b/tests/dvslib/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/dvslib/dvs_acl.py b/tests/dvslib/dvs_acl.py new file mode 100644 index 0000000000..062c710205 --- /dev/null +++ b/tests/dvslib/dvs_acl.py @@ -0,0 +1,543 @@ +"""Utilities for interacting with ACLs when writing VS tests.""" + +class DVSAcl: + """Manage ACL tables and rules on the virtual switch.""" + + CDB_ACL_TABLE_NAME = "ACL_TABLE" + + CDB_MIRROR_ACTION_LOOKUP = { + "ingress": "MIRROR_INGRESS_ACTION", + "egress": "MIRROR_EGRESS_ACTION" + } + + ADB_ACL_TABLE_NAME = "ASIC_STATE:SAI_OBJECT_TYPE_ACL_TABLE" + ADB_ACL_GROUP_TABLE_NAME = "ASIC_STATE:SAI_OBJECT_TYPE_ACL_TABLE_GROUP" + ADB_ACL_GROUP_MEMBER_TABLE_NAME = "ASIC_STATE:SAI_OBJECT_TYPE_ACL_TABLE_GROUP_MEMBER" + + ADB_ACL_STAGE_LOOKUP = { + "ingress": "SAI_ACL_STAGE_INGRESS", + "egress": "SAI_ACL_STAGE_EGRESS" + } + + ADB_PACKET_ACTION_LOOKUP = { + "FORWARD": "SAI_PACKET_ACTION_FORWARD", + "DROP": "SAI_PACKET_ACTION_DROP" + } + + ADB_MIRROR_ACTION_LOOKUP = { + "ingress": "SAI_ACL_ENTRY_ATTR_ACTION_MIRROR_INGRESS", + "egress": "SAI_ACL_ENTRY_ATTR_ACTION_MIRROR_EGRESS" + } + + ADB_PORT_ATTR_LOOKUP = { + "ingress": "SAI_PORT_ATTR_INGRESS_ACL", + "egress": "SAI_PORT_ATTR_EGRESS_ACL" + } + + def __init__(self, asic_db, config_db, state_db, counters_db): + """Create a new DVS ACL Manager.""" + self.asic_db = asic_db + self.config_db = config_db + self.state_db = state_db + self.counters_db = counters_db + + def create_acl_table( + self, + table_name, + table_type, + ports, + stage = None + ): + """Create a new ACL table in Config DB. + + Args: + table_name: The name for the new ACL table. + table_type: The type of table to create. + ports: A list of ports to bind to the ACL table. + stage: The stage for the ACL table. {ingress, egress} + """ + table_attrs = { + "policy_desc": table_name, + "type": table_type, + "ports": ",".join(ports) + } + + if stage: + table_attrs["stage"] = stage + + self.config_db.create_entry(self.CDB_ACL_TABLE_NAME, table_name, table_attrs) + + def create_control_plane_acl_table( + self, + table_name, + services + ): + """Create a new Control Plane ACL table in Config DB. + + Args: + table_name: The name for the new ACL table. + services: A list of services to bind to the ACL table. + """ + table_attrs = { + "policy_desc": table_name, + "type": "CTRLPLANE", + "services": ",".join(services) + } + + self.config_db.create_entry(self.CDB_ACL_TABLE_NAME, table_name, table_attrs) + + def update_acl_table_port_list(self, table_name, ports): + """Update the port binding list for a given ACL table. + + Args: + table_name: The name of the ACL table to update. + ports: The new list of ports to bind to the ACL table. + """ + table_attrs = {"ports": ",".join(ports)} + self.config_db.update_entry(self.CDB_ACL_TABLE_NAME, table_name, table_attrs) + + def remove_acl_table(self, table_name): + """Remove an ACL table from Config DB. + + Args: + table_name: The name of the ACL table to delete. + """ + self.config_db.delete_entry(self.CDB_ACL_TABLE_NAME, table_name) + + def get_acl_table_ids(self, expected): + """Get all of the ACL table IDs in ASIC DB. + + This method will wait for the expected number of tables to exist, or fail. + + Args: + expected: The number of tables that are expected to be present in ASIC DB. + + Returns: + The list of ACL table IDs in ASIC DB. + """ + num_keys = len(self.asic_db.default_acl_tables) + expected + keys = self.asic_db.wait_for_n_keys(self.ADB_ACL_TABLE_NAME, num_keys) + for k in self.asic_db.default_acl_tables: + assert k in keys + + acl_tables = [k for k in keys if k not in self.asic_db.default_acl_tables] + + return acl_tables + + def verify_acl_table_count(self, expected): + """Verify that some number of tables exists in ASIC DB. + + This method will wait for the expected number of tables to exist, or fail. + + Args: + expected: The number of tables that are expected to be present in ASIC DB. + """ + self.get_acl_table_ids(expected) + + def get_acl_table_group_ids(self, expected): + """Get all of the ACL group IDs in ASIC DB. + + This method will wait for the expected number of groups to exist, or fail. + + Args: + expected: The number of groups that are expected to be present in ASIC DB. + + Returns: + The list of ACL group IDs in ASIC DB. + """ + acl_table_groups = self.asic_db.wait_for_n_keys(self.ADB_ACL_GROUP_TABLE_NAME, expected) + return acl_table_groups + + # FIXME: This method currently assumes only ingress xor egress tables exist. + def verify_acl_table_groups(self, expected, stage = "ingress"): + """Verify that the expected ACL table groups exist in ASIC DB. + + This method will wait for the expected number of groups to exist, or fail. + + Args: + expected: The number of groups that are expected to be present in ASIC DB. + stage: The stage of the ACL table that was created. + """ + acl_table_groups = self.get_acl_table_group_ids(expected) + + for group in acl_table_groups: + fvs = self.asic_db.wait_for_entry(self.ADB_ACL_GROUP_TABLE_NAME, group) + for k, v in fvs.items(): + if k == "SAI_ACL_TABLE_GROUP_ATTR_ACL_STAGE": + assert v == self.ADB_ACL_STAGE_LOOKUP[stage] + elif k == "SAI_ACL_TABLE_GROUP_ATTR_ACL_BIND_POINT_TYPE_LIST": + assert v == "1:SAI_ACL_BIND_POINT_TYPE_PORT" + elif k == "SAI_ACL_TABLE_GROUP_ATTR_TYPE": + assert v == "SAI_ACL_TABLE_GROUP_TYPE_PARALLEL" + else: + assert False + + def verify_acl_table_group_members(self, acl_table_id, acl_table_group_ids, num_tables): + """Verify that the expected ACL table group members exist in ASIC DB. + + Args: + acl_table_id: The ACL table that the group members belong to. + acl_table_group_ids: The ACL table groups to check. + num_tables: The total number of ACL tables in ASIC DB. + """ + members = self.asic_db.wait_for_n_keys(self.ADB_ACL_GROUP_MEMBER_TABLE_NAME, + len(acl_table_group_ids) * num_tables) + + member_groups = [] + for member in members: + fvs = self.asic_db.wait_for_entry(self.ADB_ACL_GROUP_MEMBER_TABLE_NAME, member) + group_id = fvs.get("SAI_ACL_TABLE_GROUP_MEMBER_ATTR_ACL_TABLE_GROUP_ID") + table_id = fvs.get("SAI_ACL_TABLE_GROUP_MEMBER_ATTR_ACL_TABLE_ID") + + if group_id in acl_table_group_ids and table_id == acl_table_id: + member_groups.append(group_id) + + assert set(member_groups) == set(acl_table_group_ids) + + def verify_acl_table_port_binding( + self, + acl_table_id, + bind_ports, + num_tables, + stage = "ingress" + ): + """Verify that the ACL table has been bound to the given list of ports. + + Args: + acl_table_id: The ACL table that is being checked. + bind_ports: The ports that should be bound to the given ACL table. + num_tables: The total number of ACL tables in ASIC DB. + stage: The stage of the ACL table that was created. + """ + acl_table_group_ids = self.asic_db.wait_for_n_keys(self.ADB_ACL_GROUP_TABLE_NAME, len(bind_ports)) + + port_groups = [] + for port in bind_ports: + port_oid = self.counters_db.get_entry("COUNTERS_PORT_NAME_MAP", "").get(port) + fvs = self.asic_db.wait_for_entry("ASIC_STATE:SAI_OBJECT_TYPE_PORT", port_oid) + + acl_table_group_id = fvs.pop(self.ADB_PORT_ATTR_LOOKUP[stage], None) + assert acl_table_group_id in acl_table_group_ids + port_groups.append(acl_table_group_id) + + assert len(port_groups) == len(bind_ports) + assert set(port_groups) == set(acl_table_group_ids) + + self.verify_acl_table_group_members(acl_table_id, acl_table_group_ids, num_tables) + + def create_acl_rule( + self, + table_name, + rule_name, + qualifiers, + action = "FORWARD", + priority = "2020" + ): + """Create a new ACL rule in the given table. + + Args: + table_name: The name of the ACL table to add the rule to. + rule_name: The name of the ACL rule. + qualifiers: The list of qualifiers to add to the rule. + action: The packet action of the rule. + priority: The priority of the rule. + """ + fvs = { + "priority": priority, + "PACKET_ACTION": action + } + + for k, v in qualifiers.items(): + fvs[k] = v + + self.config_db.create_entry("ACL_RULE", "{}|{}".format(table_name, rule_name), fvs) + + def create_redirect_acl_rule( + self, + table_name, + rule_name, + qualifiers, + intf, + ip = None, + priority = "2020" + ): + """Create a new ACL redirect rule in the given table. + + Args: + table_name: The name of the ACL table to add the rule to. + rule_name: The name of the ACL rule. + qualifiers: The list of qualifiers to add to the rule. + intf: The interface to redirect packets to. + ip: The IP to redirect packets to, if the redirect is a Next Hop. + priority: The priority of the rule. + """ + if ip: + redirect_action = "{}@{}".format(ip, intf) + else: + redirect_action = intf + + fvs = { + "priority": priority, + "REDIRECT_ACTION": redirect_action + } + + for k, v in qualifiers.items(): + fvs[k] = v + + self.config_db.create_entry("ACL_RULE", "{}|{}".format(table_name, rule_name), fvs) + + def create_mirror_acl_rule( + self, + table_name, + rule_name, + qualifiers, + session_name, + stage = None, + priority = "2020" + ): + """Create a new ACL mirror rule in the given table. + + Args: + table_name: The name of the ACL table to add the rule to. + rule_name: The name of the ACL rule. + qualifiers: The list of qualifiers to add to the rule. + session_name: The name of the session to mirror to. + stage: The type of mirroring to use. {ingress, egress} + priority: The priority of the rule. + """ + if not stage: + mirror_action = "MIRROR_ACTION" + else: + mirror_action = self.CDB_MIRROR_ACTION_LOOKUP[stage] + + fvs = { + "priority": priority, + mirror_action: session_name + } + + for k, v in qualifiers.items(): + fvs[k] = v + + self.config_db.create_entry("ACL_RULE", "{}|{}".format(table_name, rule_name), fvs) + + def remove_acl_rule(self, table_name, rule_name): + """Remove the ACL rule from the table. + + Args: + table_name: The name of the table to remove the rule from. + rule_name: The name of the rule to remove. + """ + self.config_db.delete_entry("ACL_RULE", "{}|{}".format(table_name, rule_name)) + + def verify_acl_rule_count(self, expected): + """Verify that there are N rules in the ASIC DB.""" + num_keys = len(self.asic_db.default_acl_entries) + self.asic_db.wait_for_n_keys("ASIC_STATE:SAI_OBJECT_TYPE_ACL_ENTRY", num_keys + expected) + + def verify_no_acl_rules(self): + """Verify that there are no ACL rules in the ASIC DB.""" + num_keys = len(self.asic_db.default_acl_entries) + keys = self.asic_db.wait_for_n_keys("ASIC_STATE:SAI_OBJECT_TYPE_ACL_ENTRY", num_keys) + assert set(keys) == set(self.asic_db.default_acl_entries) + + def verify_acl_rule( + self, + sai_qualifiers, + action = "FORWARD", + priority = "2020", + acl_rule_id = None + ): + """Verify that an ACL rule has the correct ASIC DB representation. + + Args: + sai_qualifiers: The expected set of SAI qualifiers to be found in ASIC DB. + action: The type of PACKET_ACTION the given rule has. + priority: The priority of the rule. + acl_rule_id: A specific OID to check in ASIC DB. If left empty, this method + assumes that only one rule exists in ASIC DB. + """ + if not acl_rule_id: + acl_rule_id = self._get_acl_rule_id() + + fvs = self.asic_db.wait_for_entry("ASIC_STATE:SAI_OBJECT_TYPE_ACL_ENTRY", acl_rule_id) + self._check_acl_entry_base(fvs, sai_qualifiers, action, priority) + self._check_acl_entry_packet_action(fvs, action) + + def verify_redirect_acl_rule( + self, + sai_qualifiers, + expected_destination, + priority = "2020", + acl_rule_id=None + ): + """Verify that an ACL redirect rule has the correct ASIC DB representation. + + Args: + sai_qualifiers: The expected set of SAI qualifiers to be found in ASIC DB. + expected_destination: Where we expect the rule to redirect to. This can be an interface or + a next hop. + priority: The priority of the rule. + acl_rule_id: A specific OID to check in ASIC DB. If left empty, this method + assumes that only one rule exists in ASIC DB. + """ + if not acl_rule_id: + acl_rule_id = self._get_acl_rule_id() + + fvs = self.asic_db.wait_for_entry("ASIC_STATE:SAI_OBJECT_TYPE_ACL_ENTRY", acl_rule_id) + self._check_acl_entry_base(fvs, sai_qualifiers, "REDIRECT", priority) + self._check_acl_entry_redirect_action(fvs, expected_destination) + + def verify_mirror_acl_rule( + self, + sai_qualifiers, + session_oid, + stage = "ingress", + priority = "2020", + acl_rule_id = None + ): + """Verify that an ACL mirror rule has the correct ASIC DB representation. + + Args: + sai_qualifiers: The expected set of SAI qualifiers to be found in ASIC DB. + session_oid: The OID of the mirror session this rule is using. + stage: What stage/type of mirroring this rule is using. + priority: The priority of the rule. + acl_rule_id: A specific OID to check in ASIC DB. If left empty, this method + assumes that only one rule exists in ASIC DB. + """ + if not acl_rule_id: + acl_rule_id = self._get_acl_rule_id() + + fvs = self.asic_db.wait_for_entry("ASIC_STATE:SAI_OBJECT_TYPE_ACL_ENTRY", acl_rule_id) + self._check_acl_entry_base(fvs, sai_qualifiers, "MIRROR", priority) + self._check_acl_entry_mirror_action(fvs, session_oid, stage) + + def verify_acl_rule_set( + self, + priorities, + in_actions, + expected + ): + """Verify that a set of rules with PACKET_ACTIONs have the correct ASIC DB representation. + + Args: + priorities: A list of the priorities of each rule to be checked. + in_actions: The type of action each rule has, keyed by priority. + expected: The expected SAI qualifiers for each rule, keyed by priority. + """ + num_keys = len(self.asic_db.default_acl_entries) + len(priorities) + keys = self.asic_db.wait_for_n_keys("ASIC_STATE:SAI_OBJECT_TYPE_ACL_ENTRY", num_keys) + + acl_entries = [k for k in keys if k not in self.asic_db.default_acl_entries] + for entry in acl_entries: + rule = self.asic_db.wait_for_entry("ASIC_STATE:SAI_OBJECT_TYPE_ACL_ENTRY", entry) + priority = rule.get("SAI_ACL_ENTRY_ATTR_PRIORITY", None) + assert priority in priorities + self.verify_acl_rule(expected[priority], in_actions[priority], priority, entry) + + # FIXME: This `get_x_comparator` abstraction is a bit clunky, we should try to improve this later. + def get_simple_qualifier_comparator(self, expected_qualifier): + """Generate a method that compares if a given SAI qualifer matches `expected_qualifier`. + + Args: + expected_qualifier: The SAI qualifier that the generated method will check against. + + Returns: + A method that will compare qualifiers to `expected_qualifier`. + """ + def _match_qualifier(sai_qualifier): + return expected_qualifier == sai_qualifier + + return _match_qualifier + + def get_port_list_comparator(self, expected_ports): + """Generate a method that compares if a list of SAI ports matches the ports from `expected_ports`. + + Args: + expected_ports: The port list that the generated method will check against. + + Returns: + A method that will compare SAI port lists against `expected_ports`. + """ + def _match_port_list(sai_port_list): + if not sai_port_list.startswith("{}:".format(len(expected_ports))): + return False + for port in expected_ports: + if self.asic_db.port_name_map[port] not in sai_port_list: + return False + + return True + + return _match_port_list + + def get_acl_range_comparator(self, expected_type, expected_ports): + """Generate a method that compares if a SAI range object matches the range from `expected ports`. + + Args: + expected_type: The expected type of range. + expected_ports: A range of numbers described as "lower_bound,upper_bound". + + Returns: + A method that will compare SAI ranges against `expected_type` and `expected_ports`. + """ + def _match_acl_range(sai_acl_range): + range_id = sai_acl_range.split(":", 1)[1] + fvs = self.asic_db.wait_for_entry("ASIC_STATE:SAI_OBJECT_TYPE_ACL_RANGE", range_id) + for k, v in fvs.items(): + if k == "SAI_ACL_RANGE_ATTR_TYPE" and v == expected_type: + continue + elif k == "SAI_ACL_RANGE_ATTR_LIMIT" and v == expected_ports: + continue + else: + return False + + return True + + return _match_acl_range + + def _get_acl_rule_id(self): + num_keys = len(self.asic_db.default_acl_entries) + 1 + keys = self.asic_db.wait_for_n_keys("ASIC_STATE:SAI_OBJECT_TYPE_ACL_ENTRY", num_keys) + + acl_entries = [k for k in keys if k not in self.asic_db.default_acl_entries] + return acl_entries[0] + + def _check_acl_entry_base( + self, + entry, + qualifiers, + action, + priority + ): + acl_table_id = self.get_acl_table_ids(1)[0] + + for k, v in entry.items(): + if k == "SAI_ACL_ENTRY_ATTR_TABLE_ID": + assert v == acl_table_id + elif k == "SAI_ACL_ENTRY_ATTR_ADMIN_STATE": + assert v == "true" + elif k == "SAI_ACL_ENTRY_ATTR_PRIORITY": + assert v == priority + elif k == "SAI_ACL_ENTRY_ATTR_ACTION_COUNTER": + assert True + elif k == "SAI_ACL_ENTRY_ATTR_ACTION_PACKET_ACTION": + assert action in self.ADB_PACKET_ACTION_LOOKUP + elif k == "SAI_ACL_ENTRY_ATTR_ACTION_REDIRECT": + assert action == "REDIRECT" + elif "SAI_ACL_ENTRY_ATTR_ACTION_MIRROR" in k: + assert action == "MIRROR" + elif k in qualifiers: + assert qualifiers[k](v) + else: + assert False + + def _check_acl_entry_packet_action(self, entry, action): + assert "SAI_ACL_ENTRY_ATTR_ACTION_PACKET_ACTION" in entry + assert self.ADB_PACKET_ACTION_LOOKUP.get(action, None) == entry["SAI_ACL_ENTRY_ATTR_ACTION_PACKET_ACTION"] + + def _check_acl_entry_redirect_action(self, entry, expected_destination): + assert entry.get("SAI_ACL_ENTRY_ATTR_ACTION_REDIRECT", None) == expected_destination + + def _check_acl_entry_mirror_action(self, entry, session_oid, stage): + assert stage in self.ADB_MIRROR_ACTION_LOOKUP + assert entry.get(self.ADB_MIRROR_ACTION_LOOKUP[stage]) == session_oid diff --git a/tests/dvslib/dvs_common.py b/tests/dvslib/dvs_common.py new file mode 100644 index 0000000000..e37bb95c2b --- /dev/null +++ b/tests/dvslib/dvs_common.py @@ -0,0 +1,55 @@ +"""Common infrastructure for writing VS tests.""" + +import collections +import time + + +_PollingConfig = collections.namedtuple('PollingConfig', 'polling_interval timeout strict') + + +class PollingConfig(_PollingConfig): + """PollingConfig provides parameters that are used to control polling behavior. + + Attributes: + polling_interval (int): How often to poll, in seconds. + timeout (int): The maximum amount of time to wait, in seconds. + strict (bool): If the strict flag is set, reaching the timeout will cause tests to fail. + """ + + +def wait_for_result( + polling_function, + polling_config, +): + """Run `polling_function` periodically using the specified `polling_config`. + + Args: + polling_function: The function being polled. The function cannot take any arguments and + must return a status which indicates if the function was succesful or not, as well as + some return value. + polling_config: The parameters to use to poll the polling function. + + Returns: + If the polling function succeeds, then this method will return True and the output of the + polling function. + + If it does not succeed within the provided timeout, it will return False and whatever the + output of the polling function was on the final attempt. + """ + if polling_config.polling_interval == 0: + iterations = 1 + else: + iterations = int(polling_config.timeout // polling_config.polling_interval) + 1 + + for _ in range(iterations): + status, result = polling_function() + + if status: + return (True, result) + + time.sleep(polling_config.polling_interval) + + if polling_config.strict: + assert False, "Operation timed out after {} seconds".format(polling_config.timeout) + + return (False, result) diff --git a/tests/dvslib/dvs_database.py b/tests/dvslib/dvs_database.py new file mode 100644 index 0000000000..003a21925e --- /dev/null +++ b/tests/dvslib/dvs_database.py @@ -0,0 +1,400 @@ +"""Utilities for interacting with redis when writing VS tests. + +FIXME: + - Reference DBs by name rather than ID/socket + - Move DEFAULT_POLLING_CONFIG to Common + - Add support for ProducerStateTable +""" +from swsscommon import swsscommon +from dvslib.dvs_common import wait_for_result, PollingConfig + + +class DVSDatabase: + """DVSDatabase provides access to redis databases on the virtual switch. + + By default, database operations are configured to use `DEFAULT_POLLING_CONFIG`. Users can + specify their own PollingConfig, but this shouldn't typically be necessary. + """ + + DEFAULT_POLLING_CONFIG = PollingConfig(polling_interval=0.01, timeout=5, strict=True) + + def __init__(self, db_id, connector): + """Initialize a DVSDatabase instance. + + Args: + db_id: The integer ID used to identify the given database instance in redis. + connector: The I/O connection used to communicate with + redis (e.g. UNIX socket, TCP socket, etc.). + """ + self.db_connection = swsscommon.DBConnector(db_id, connector, 0) + + def create_entry(self, table_name, key, entry): + """Add the mapping {`key` -> `entry`} to the specified table. + + Args: + table_name: The name of the table to add the entry to. + key: The key that maps to the entry. + entry: A set of key-value pairs to be stored. + """ + table = swsscommon.Table(self.db_connection, table_name) + formatted_entry = swsscommon.FieldValuePairs(list(entry.items())) + table.set(key, formatted_entry) + + def update_entry(self, table_name, key, entry): + """Update entry of an existing key in the specified table. + + Args: + table_name: The name of the table. + key: The key that needs to be updated. + entry: A set of key-value pairs to be updated. + """ + table = swsscommon.Table(self.db_connection, table_name) + formatted_entry = swsscommon.FieldValuePairs(list(entry.items())) + table.set(key, formatted_entry) + + def get_entry(self, table_name, key): + """Get the entry stored at `key` in the specified table. + + Args: + table_name: The name of the table where the entry is stored. + key: The key that maps to the entry being retrieved. + + Returns: + The entry stored at `key`. If no entry is found, then an empty Dict is returned. + """ + table = swsscommon.Table(self.db_connection, table_name) + (status, fv_pairs) = table.get(key) + + if not status: + return {} + + return dict(fv_pairs) + + def delete_entry(self, table_name, key): + """Remove the entry stored at `key` in the specified table. + + Args: + table_name: The name of the table where the entry is being removed. + key: The key that maps to the entry being removed. + """ + table = swsscommon.Table(self.db_connection, table_name) + table._del(key) # pylint: disable=protected-access + + def get_keys(self, table_name): + """Get all of the keys stored in the specified table. + + Args: + table_name: The name of the table from which to fetch the keys. + + Returns: + The keys stored in the table. If no keys are found, then an empty List is returned. + """ + table = swsscommon.Table(self.db_connection, table_name) + keys = table.getKeys() + + return keys if keys else [] + + def wait_for_entry( + self, + table_name, + key, + polling_config = DEFAULT_POLLING_CONFIG + ): + """Wait for the entry stored at `key` in the specified table to exist and retrieve it. + + Args: + table_name: The name of the table where the entry is stored. + key: The key that maps to the entry being retrieved. + polling_config: The parameters to use to poll the db. + + Returns: + The entry stored at `key`. If no entry is found, then an empty Dict is returned. + """ + def __access_function(): + fv_pairs = self.get_entry(table_name, key) + return (bool(fv_pairs), fv_pairs) + + status, result = wait_for_result( + __access_function, + self._disable_strict_polling(polling_config)) + + if not status: + assert not polling_config.strict, \ + "Entry not found: key=\"{}\", table=\"{}\"".format(key, table_name) + + return result + + def wait_for_fields( + self, + table_name, + key, + expected_fields, + polling_config = DEFAULT_POLLING_CONFIG + ): + """Wait for the entry stored at `key` to have the specified fields and retrieve it. + + This method is useful if you only care about a subset of the fields stored in the + specified entry. + + Args: + table_name: The name of the table where the entry is stored. + key: The key that maps to the entry being checked. + expected_fields: The fields that we expect to see in the entry. + polling_config: The parameters to use to poll the db. + + Returns: + The entry stored at `key`. If no entry is found, then an empty Dict is returned. + """ + def __access_function(): + fv_pairs = self.get_entry(table_name, key) + return (all(field in fv_pairs for field in expected_fields), fv_pairs) + + status, result = wait_for_result( + __access_function, + self._disable_strict_polling(polling_config)) + + if not status: + assert not polling_config.strict, \ + "Expected fields not found: expected={}, \ + received={}, key=\"{}\", table=\"{}\"".format(expected_fields, result, key, table_name) + + return result + + def wait_for_field_match( + self, + table_name, + key, + expected_fields, + polling_config = DEFAULT_POLLING_CONFIG + ): + """Wait for the entry stored at `key` to have the specified field/value pairs and retrieve it. + + This method is useful if you only care about the contents of a subset of the fields stored in the + specified entry. + + Args: + table_name: The name of the table where the entry is stored. + key: The key that maps to the entry being checked. + expected_fields: The fields and their values we expect to see in the entry. + polling_config: The parameters to use to poll the db. + + Returns: + The entry stored at `key`. If no entry is found, then an empty Dict is returned. + """ + def __access_function(): + fv_pairs = self.get_entry(table_name, key) + return (all(fv_pairs.get(k) == v for k, v in expected_fields.items()), fv_pairs) + + status, result = wait_for_result( + __access_function, + self._disable_strict_polling(polling_config)) + + if not status: + assert not polling_config.strict, \ + "Expected field/value pairs not found: expected={}, \ + received={}, key=\"{}\", table=\"{}\"".format(expected_fields, result, key, table_name) + + return result + + def wait_for_field_negative_match( + self, + table_name, + key, + old_fields, + polling_config = DEFAULT_POLLING_CONFIG + ): + """Wait for the entry stored at `key` to have different field/value pairs than the ones specified. + + This method is useful if you expect some field to change, but you don't know their exact values. + + Args: + table_name: The name of the table where the entry is stored. + key: The key that maps to the entry being checked. + old_fields: The original field/value pairs we expect to change. + polling_config: The parameters to use to poll the db. + + Returns: + The entry stored at `key`. If no entry is found, then an empty Dict is returned. + """ + def __access_function(): + fv_pairs = self.get_entry(table_name, key) + return (all(k in fv_pairs and fv_pairs[k] != v for k, v in old_fields.items()), fv_pairs) + + status, result = wait_for_result( + __access_function, + self._disable_strict_polling(polling_config)) + + if not status: + assert not polling_config.strict, \ + "Did not expect field/values to match, but they did: provided={}, \ + received={}, key=\"{}\", table=\"{}\"".format(old_fields, result, key, table_name) + + return result + + def wait_for_exact_match( + self, + table_name, + key, + expected_entry, + polling_config = DEFAULT_POLLING_CONFIG + ): + """Wait for the entry stored at `key` to match `expected_entry` and retrieve it. + + This method is useful if you care about *all* the fields stored in the specfied entry. + + Args: + table_name: The name of the table where the entry is stored. + key: The key that maps to the entry being checked. + expected_entry: The entry we expect to see. + polling_config: The parameters to use to poll the db. + + Returns: + The entry stored at `key`. If no entry is found, then an empty Dict is returned. + """ + + def __access_function(): + fv_pairs = self.get_entry(table_name, key) + return (fv_pairs == expected_entry, fv_pairs) + + status, result = wait_for_result( + __access_function, + self._disable_strict_polling(polling_config)) + + if not status: + assert not polling_config.strict, \ + "Exact match not found: expected={}, received={}, \ + key=\"{}\", table=\"{}\"".format(expected_entry, result, key, table_name) + + return result + + def wait_for_deleted_entry( + self, + table_name, + key, + polling_config = DEFAULT_POLLING_CONFIG + ): + """Wait for no entry to exist at `key` in the specified table. + + Args: + table_name: The name of the table being checked. + key: The key to be checked. + polling_config: The parameters to use to poll the db. + + Returns: + The entry stored at `key`. If no entry is found, then an empty Dict is returned. + """ + def __access_function(): + fv_pairs = self.get_entry(table_name, key) + return (not bool(fv_pairs), fv_pairs) + + status, result = wait_for_result( + __access_function, + self._disable_strict_polling(polling_config)) + + if not status: + assert not polling_config.strict, \ + "Entry still exists: entry={}, key=\"{}\", \ + table=\"{}\"".format(result, key, table_name) + + return result + + def wait_for_n_keys( + self, + table_name, + num_keys, + polling_config = DEFAULT_POLLING_CONFIG + ): + """Wait for the specified number of keys to exist in the table. + + Args: + table_name: The name of the table from which to fetch the keys. + num_keys: The expected number of keys to retrieve from the table. + polling_config: The parameters to use to poll the db. + + Returns: + The keys stored in the table. If no keys are found, then an empty List is returned. + """ + def __access_function(): + keys = self.get_keys(table_name) + return (len(keys) == num_keys, keys) + + status, result = wait_for_result( + __access_function, + self._disable_strict_polling(polling_config)) + + if not status: + assert not polling_config.strict, \ + "Unexpected number of keys: expected={}, \ + received={} ({}), table=\"{}\"".format(num_keys, len(result), result, table_name) + + return result + + def wait_for_matching_keys( + self, + table_name, + expected_keys, + polling_config = DEFAULT_POLLING_CONFIG + ): + """Wait for the specified keys to exist in the table. + + Args: + table_name: The name of the table from which to fetch the keys. + expected_keys: The keys we expect to see in the table. + polling_config: The parameters to use to poll the db. + + Returns: + The keys stored in the table. If no keys are found, then an empty List is returned. + """ + def __access_function(): + keys = self.get_keys(table_name) + return (all(key in keys for key in expected_keys), keys) + + status, result = wait_for_result( + __access_function, + self._disable_strict_polling(polling_config)) + + if not status: + assert not polling_config.strict, \ + "Expected keys not found: expected={}, received={}, \ + table=\"{}\"".format(expected_keys, result, table_name) + + return result + + def wait_for_deleted_keys( + self, + table_name, + deleted_keys, + polling_config = DEFAULT_POLLING_CONFIG + ): + """Wait for the specfied keys to no longer exist in the table. + + Args: + table_name: The name of the table from which to fetch the keys. + deleted_keys: The keys we expect to be removed from the table. + polling_config: The parameters to use to poll the db. + + Returns: + The keys stored in the table. If no keys are found, then an empty List is returned. + """ + def __access_function(): + keys = self.get_keys(table_name) + return (all(key not in keys for key in deleted_keys), keys) + + status, result = wait_for_result( + __access_function, + self._disable_strict_polling(polling_config)) + + if not status: + expected = [key for key in result if key not in deleted_keys] + assert not polling_config.strict, \ + "Unexpected keys found: expected={}, received={}, \ + table=\"{}\"".format(expected, result, table_name) + + return result + + @staticmethod + def _disable_strict_polling(polling_config): + disabled_config = PollingConfig(polling_interval=polling_config.polling_interval, + timeout=polling_config.timeout, + strict=False) + return disabled_config diff --git a/tests/dvslib/dvs_lag.py b/tests/dvslib/dvs_lag.py new file mode 100644 index 0000000000..06dd0c4217 --- /dev/null +++ b/tests/dvslib/dvs_lag.py @@ -0,0 +1,29 @@ +class DVSLag(object): + def __init__(self, adb, cdb): + self.asic_db = adb + self.config_db = cdb + + def create_port_channel(self, lag_id, admin_status="up", mtu="1500"): + lag = "PortChannel{}".format(lag_id) + lag_entry = {"admin_status": admin_status, "mtu": mtu} + self.config_db.create_entry("PORTCHANNEL", lag, lag_entry) + + def remove_port_channel(self, lag_id): + lag = "PortChannel{}".format(lag_id) + self.config_db.delete_entry("PORTCHANNEL", lag) + + def create_port_channel_member(self, lag_id, interface): + member = "PortChannel{}|{}".format(lag_id, interface) + member_entry = {"NULL": "NULL"} + self.config_db.create_entry("PORTCHANNEL_MEMBER", member, member_entry) + + def remove_port_channel_member(self, lag_id, interface): + member = "PortChannel{}|{}".format(lag_id, interface) + self.config_db.delete_entry("PORTCHANNEL_MEMBER", member) + + def get_and_verify_port_channel_members(self, expected_num): + return self.asic_db.wait_for_n_keys("ASIC_STATE:SAI_OBJECT_TYPE_LAG_MEMBER", expected_num) + + def get_and_verify_port_channel(self, expected_num): + return self.asic_db.wait_for_n_keys("ASIC_STATE:SAI_OBJECT_TYPE_LAG", expected_num) + diff --git a/tests/dvslib/dvs_mirror.py b/tests/dvslib/dvs_mirror.py new file mode 100644 index 0000000000..b93b0a5eb7 --- /dev/null +++ b/tests/dvslib/dvs_mirror.py @@ -0,0 +1,96 @@ +class DVSMirror(object): + def __init__(self, adb, cdb, sdb, cntrdb, appdb): + self.asic_db = adb + self.config_db = cdb + self.state_db = sdb + self.counters_db = cntrdb + self.app_db = appdb + + def create_span_session(self, name, dst_port, src_ports=None, direction="BOTH", queue=None, policer=None): + mirror_entry = {"type": "SPAN"} + if dst_port: + mirror_entry["dst_port"] = dst_port + + if src_ports: + mirror_entry["src_port"] = src_ports + + if queue: + mirror_entry["queue"] = queue + if policer: + mirror_entry["policer"] = policer + self.config_db.create_entry("MIRROR_SESSION", name, mirror_entry) + + def create_erspan_session(self, name, src, dst, gre, dscp, ttl, queue, policer=None, src_ports=None, direction="BOTH"): + mirror_entry = { + "src_ip": src, + "dst_ip": dst, + "gre_type": gre, + "dscp": dscp, + "ttl": ttl, + "queue": queue, + } + + if policer: + mirror_entry["policer"] = policer + + if src_ports: + mirror_entry["src_port"] = src_ports + + self.config_db.create_entry("MIRROR_SESSION", name, mirror_entry) + + def remove_mirror_session(self, name): + self.config_db.delete_entry("MIRROR_SESSION", name) + + def verify_no_mirror(self): + self.config_db.wait_for_n_keys("MIRROR_SESSION", 0) + self.state_db.wait_for_n_keys("MIRROR_SESSION_TABLE", 0) + + def verify_session_status(self, name, status="active", expected=1): + self.state_db.wait_for_n_keys("MIRROR_SESSION_TABLE", expected) + if expected: + self.state_db.wait_for_field_match("MIRROR_SESSION_TABLE", name, {"status": status}) + + def verify_port_mirror_config(self, dvs, ports, direction, session_oid="null"): + fvs = dvs.counters_db.get_entry("COUNTERS_PORT_NAME_MAP", "") + fvs = dict(fvs) + for p in ports: + port_oid = fvs.get(p) + member = dvs.asic_db.wait_for_entry("ASIC_STATE:SAI_OBJECT_TYPE_PORT", port_oid) + if direction in {"RX", "BOTH"}: + assert member["SAI_PORT_ATTR_INGRESS_MIRROR_SESSION"] == "1:"+session_oid + else: + assert "SAI_PORT_ATTR_INGRESS_MIRROR_SESSION" not in member.keys() or member["SAI_PORT_ATTR_INGRESS_MIRROR_SESSION"] == "0:null" + if direction in {"TX", "BOTH"}: + assert member["SAI_PORT_ATTR_EGRESS_MIRROR_SESSION"] == "1:"+session_oid + else: + assert "SAI_PORT_ATTR_EGRESS_MIRROR_SESSION" not in member.keys() or member["SAI_PORT_ATTR_EGRESS_MIRROR_SESSION"] == "0:null" + + def verify_session_db(self, dvs, name, asic_table=None, asic=None, state=None, asic_size=None): + if asic: + dvs.asic_db.wait_for_field_match("ASIC_STATE:SAI_OBJECT_TYPE_MIRROR_SESSION", asic_table, asic) + if state: + dvs.state_db.wait_for_field_match("MIRROR_SESSION_TABLE", name, state) + + def verify_session_policer(self, dvs, policer_oid, cir): + if cir: + entry = dvs.asic_db.wait_for_entry("ASIC_STATE:SAI_OBJECT_TYPE_POLICER", policer_oid) + assert entry["SAI_POLICER_ATTR_CIR"] == cir + + def verify_session(self, dvs, name, asic_db=None, state_db=None, dst_oid=None, src_ports=None, direction="BOTH", policer=None, expected = 1, asic_size=None): + member_ids = self.asic_db.wait_for_n_keys("ASIC_STATE:SAI_OBJECT_TYPE_MIRROR_SESSION", expected) + session_oid=member_ids[0] + # with multiple sessions, match on dst_oid to get session_oid + if dst_oid: + for member in member_ids: + entry=dvs.asic_db.wait_for_entry("ASIC_STATE:SAI_OBJECT_TYPE_MIRROR_SESSION", member) + if entry["SAI_MIRROR_SESSION_ATTR_MONITOR_PORT"] == dst_oid: + session_oid = member + + self.verify_session_db(dvs, name, session_oid, asic=asic_db, state=state_db, asic_size=asic_size) + if policer: + cir = dvs.config_db.wait_for_entry("POLICER", policer)["cir"] + entry=dvs.asic_db.wait_for_entry("ASIC_STATE:SAI_OBJECT_TYPE_MIRROR_SESSION", session_oid) + self.verify_session_policer(dvs, entry["SAI_MIRROR_SESSION_ATTR_POLICER"], cir) + if src_ports: + self.verify_port_mirror_config(dvs, src_ports, direction, session_oid=session_oid) + diff --git a/tests/dvslib/dvs_policer.py b/tests/dvslib/dvs_policer.py new file mode 100644 index 0000000000..1a16fdf323 --- /dev/null +++ b/tests/dvslib/dvs_policer.py @@ -0,0 +1,21 @@ +class DVSPolicer(object): + def __init__(self, adb, cdb): + self.asic_db = adb + self.config_db = cdb + + def create_policer(self, name, type="packets", cir="600", cbs="600", mode="sr_tcm", red_action="drop"): + policer_entry = {"meter_type": type, "mode": mode, + "cir": cir, "cbs": cbs, "red_packet_action": red_action} + self.config_db.create_entry("POLICER", name, policer_entry) + + def remove_policer(self, name): + self.config_db.delete_entry("POLICER", name) + + def verify_policer(self, name, expected=1): + self.asic_db.wait_for_n_keys( + "ASIC_STATE:SAI_OBJECT_TYPE_POLICER", + expected + len(self.asic_db.default_copp_policers) + ) + + def verify_no_policer(self): + self.asic_db.wait_for_n_keys("ASIC_STATE:SAI_OBJECT_TYPE_POLICER", 0) diff --git a/tests/dvslib/dvs_vlan.py b/tests/dvslib/dvs_vlan.py new file mode 100644 index 0000000000..adca18b0e7 --- /dev/null +++ b/tests/dvslib/dvs_vlan.py @@ -0,0 +1,76 @@ +from .dvs_database import DVSDatabase + +class DVSVlan(object): + def __init__(self, adb, cdb, sdb, cntrdb, appdb): + self.asic_db = adb + self.config_db = cdb + self.state_db = sdb + self.counters_db = cntrdb + self.app_db = appdb + + def create_vlan(self, vlan): + vlan = "Vlan{}".format(vlan) + vlan_entry = {"vlanid": vlan} + self.config_db.create_entry("VLAN", vlan, vlan_entry) + + def remove_vlan(self, vlan): + vlan = "Vlan{}".format(vlan) + self.config_db.delete_entry("VLAN", vlan) + + def create_vlan_member(self, vlan, interface, tagging_mode="untagged"): + member = "Vlan{}|{}".format(vlan, interface) + if tagging_mode: + member_entry = {"tagging_mode": tagging_mode} + else: + member_entry = {"no_tag_mode": ""} + + self.config_db.create_entry("VLAN_MEMBER", member, member_entry) + + def remove_vlan_member(self, vlan, interface): + member = "Vlan{}|{}".format(vlan, interface) + self.config_db.delete_entry("VLAN_MEMBER", member) + + def check_app_db_vlan_fields(self, fvs, admin_status="up", mtu="9100"): + assert fvs.get("admin_status") == admin_status + assert fvs.get("mtu") == mtu + + def check_app_db_vlan_member_fields(self, fvs, tagging_mode="untagged"): + assert fvs.get("tagging_mode") == tagging_mode + + def check_state_db_vlan_fields(self, fvs, state="ok"): + assert fvs.get("state") == state + + def check_state_db_vlan_member_fields(self, fvs, state="ok"): + assert fvs.get("state") == state + + def verify_vlan(self, vlan_oid, vlan_id): + vlan = self.asic_db.wait_for_entry("ASIC_STATE:SAI_OBJECT_TYPE_VLAN", vlan_oid) + assert vlan.get("SAI_VLAN_ATTR_VLAN_ID") == vlan_id + + def get_and_verify_vlan_ids(self, + expected_num, + polling_config=DVSDatabase.DEFAULT_POLLING_CONFIG): + vlan_entries = self.asic_db.wait_for_n_keys("ASIC_STATE:SAI_OBJECT_TYPE_VLAN", + expected_num + 1, + polling_config) + return [v for v in vlan_entries if v != self.asic_db.default_vlan_id] + + def verify_vlan_member(self, vlan_oid, iface, tagging_mode="SAI_VLAN_TAGGING_MODE_UNTAGGED"): + member_ids = self.asic_db.wait_for_n_keys("ASIC_STATE:SAI_OBJECT_TYPE_VLAN_MEMBER", 1) + member = self.asic_db.wait_for_entry("ASIC_STATE:SAI_OBJECT_TYPE_VLAN_MEMBER", member_ids[0]) + assert member == {"SAI_VLAN_MEMBER_ATTR_VLAN_TAGGING_MODE": tagging_mode, + "SAI_VLAN_MEMBER_ATTR_VLAN_ID": vlan_oid, + "SAI_VLAN_MEMBER_ATTR_BRIDGE_PORT_ID": self.get_bridge_port_id(iface)} + + def get_and_verify_vlan_member_ids(self, expected_num): + return self.asic_db.wait_for_n_keys("ASIC_STATE:SAI_OBJECT_TYPE_VLAN_MEMBER", expected_num) + + def get_bridge_port_id(self, expected_iface): + bridge_port_id = self.asic_db.wait_for_n_keys("ASIC_STATE:SAI_OBJECT_TYPE_BRIDGE_PORT", 1)[0] + bridge_port = self.asic_db.wait_for_entry("ASIC_STATE:SAI_OBJECT_TYPE_BRIDGE_PORT", bridge_port_id) + #TBD: port_to_id_map may NOT be updated one in case port is deleted and re-created. + # Hence the map needs refreshed. Need to think trough and decide when and where + # to do it. + assert self.asic_db.port_to_id_map[bridge_port["SAI_BRIDGE_PORT_ATTR_PORT_ID"]] == expected_iface + return bridge_port_id + diff --git a/tests/test_warm_reboot.py b/tests/test_warm_reboot.py index ef43078d63..28546ccfa6 100644 --- a/tests/test_warm_reboot.py +++ b/tests/test_warm_reboot.py @@ -5,6 +5,8 @@ import json import pytest + + # Get restore count of all processes supporting warm restart def swss_get_RestoreCount(dvs, state_db): restore_count = {} @@ -124,6 +126,8 @@ def test_PortSyncdWarmRestart(dvs, testlog): fvs = swsscommon.FieldValuePairs([("NULL","NULL")]) intf_tbl.set("Ethernet16|11.0.0.1/29", fvs) intf_tbl.set("Ethernet20|11.0.0.9/29", fvs) + intf_tbl.set("Ethernet16", fvs) + intf_tbl.set("Ethernet20", fvs) dvs.runcmd("ifconfig Ethernet16 up") dvs.runcmd("ifconfig Ethernet20 up") @@ -190,6 +194,8 @@ def test_PortSyncdWarmRestart(dvs, testlog): intf_tbl._del("Ethernet16|11.0.0.1/29") intf_tbl._del("Ethernet20|11.0.0.9/29") + intf_tbl._del("Ethernet16") + intf_tbl._del("Ethernet20") time.sleep(2) @@ -248,6 +254,8 @@ def test_VlanMgrdWarmRestart(dvs, testlog): fvs = swsscommon.FieldValuePairs([("NULL","NULL")]) intf_tbl.set("Vlan16|11.0.0.1/29", fvs) intf_tbl.set("Vlan20|11.0.0.9/29", fvs) + intf_tbl.set("Vlan16", fvs) + intf_tbl.set("Vlan20", fvs) dvs.runcmd("ifconfig Vlan16 up") dvs.runcmd("ifconfig Vlan20 up") @@ -303,6 +311,10 @@ def test_VlanMgrdWarmRestart(dvs, testlog): intf_tbl._del("Vlan16|11.0.0.1/29") intf_tbl._del("Vlan20|11.0.0.9/29") + intf_tbl._del("Vlan16") + intf_tbl._del("Vlan20") + del_entry_tbl(conf_db, "VLAN_MEMBER", "Vlan16|Ethernet16") + del_entry_tbl(conf_db, "VLAN_MEMBER", "Vlan20|Ethernet20") time.sleep(2) def stop_neighsyncd(dvs): @@ -407,6 +419,10 @@ def test_swss_neighbor_syncup(dvs, testlog): intf_tbl.set("{}|28.0.0.9/24".format(intfs[1]), fvs) intf_tbl.set("{}|2400::1/64".format(intfs[0]), fvs) intf_tbl.set("{}|2800::1/64".format(intfs[1]), fvs) + intf_tbl.set("{}".format(intfs[0]), fvs) + intf_tbl.set("{}".format(intfs[1]), fvs) + intf_tbl.set("{}".format(intfs[0]), fvs) + intf_tbl.set("{}".format(intfs[1]), fvs) dvs.runcmd("ifconfig {} up".format(intfs[0])) dvs.runcmd("ifconfig {} up".format(intfs[1])) @@ -731,10 +747,18 @@ def test_swss_neighbor_syncup(dvs, testlog): # check restore Count swss_app_check_RestoreCount_single(state_db, restore_count, "neighsyncd") + # post-cleanup + dvs.runcmd("ip -s neigh flush all") + dvs.runcmd("ip -6 -s neigh flush all") + intf_tbl._del("{}|24.0.0.1/24".format(intfs[0])) intf_tbl._del("{}|28.0.0.9/24".format(intfs[1])) intf_tbl._del("{}|2400::1/64".format(intfs[0])) intf_tbl._del("{}|2800::1/64".format(intfs[1])) + intf_tbl._del("{}".format(intfs[0])) + intf_tbl._del("{}".format(intfs[1])) + intf_tbl._del("{}".format(intfs[0])) + intf_tbl._del("{}".format(intfs[1])) time.sleep(2) @@ -1654,11 +1678,13 @@ def test_system_warmreboot_neighbor_syncup(dvs, testlog): # bring servers' interface up, save the macs dvs.runcmd("sysctl -w net.ipv4.neigh.Ethernet{}.base_reachable_time_ms=1800000".format(i*4)) dvs.runcmd("sysctl -w net.ipv6.neigh.Ethernet{}.base_reachable_time_ms=1800000".format(i*4)) - dvs.runcmd("sysctl -w net.ipv4.neigh.Ethernet{}.gc_stale_time=180".format(i*4)) - dvs.runcmd("sysctl -w net.ipv6.neigh.Ethernet{}.gc_stale_time=180".format(i*4)) + dvs.runcmd("sysctl -w net.ipv4.neigh.Ethernet{}.gc_stale_time=600".format(i*4)) + dvs.runcmd("sysctl -w net.ipv6.neigh.Ethernet{}.gc_stale_time=600".format(i*4)) dvs.runcmd("ip addr flush dev Ethernet{}".format(i*4)) intf_tbl.set("Ethernet{}|{}.0.0.1/24".format(i*4, i*4), fvs) intf_tbl.set("Ethernet{}|{}00::1/64".format(i*4, i*4), fvs) + intf_tbl.set("Ethernet{}".format(i*4), fvs) + intf_tbl.set("Ethernet{}".format(i*4), fvs) dvs.runcmd("ip link set Ethernet{} up".format(i*4, i*4)) dvs.servers[i].runcmd("ip link set up dev eth0") dvs.servers[i].runcmd("ip addr flush dev eth0") @@ -1893,4 +1919,150 @@ def test_system_warmreboot_neighbor_syncup(dvs, testlog): for i in range(8, 8+NUM_INTF): intf_tbl._del("Ethernet{}|{}.0.0.1/24".format(i*4, i*4)) intf_tbl._del("Ethernet{}|{}00::1/64".format(i*4, i*4)) + intf_tbl._del("Ethernet{}".format(i*4)) + intf_tbl._del("Ethernet{}".format(i*4)) + + flush_neigh_entries(dvs) + +@pytest.fixture(scope="module") +def setup_erspan_neighbors(dvs): + dvs.setup_db() + + dvs.set_interface_status("Ethernet12", "up") + dvs.set_interface_status("Ethernet16", "up") + dvs.set_interface_status("Ethernet20", "up") + + dvs.add_ip_address("Ethernet12", "10.0.0.0/31") + dvs.add_ip_address("Ethernet16", "11.0.0.0/31") + dvs.add_ip_address("Ethernet20", "12.0.0.0/31") + + dvs.add_neighbor("Ethernet12", "10.0.0.1", "02:04:06:08:10:12") + dvs.add_neighbor("Ethernet16", "11.0.0.1", "03:04:06:08:10:12") + dvs.add_neighbor("Ethernet20", "12.0.0.1", "04:04:06:08:10:12") + + dvs.add_route("2.2.2.2", "10.0.0.1") + + yield + + dvs.remove_route("2.2.2.2") + + dvs.remove_neighbor("Ethernet12", "10.0.0.1") + dvs.remove_neighbor("Ethernet16", "11.0.0.1") + dvs.remove_neighbor("Ethernet20", "12.0.0.1") + + dvs.remove_ip_address("Ethernet12", "10.0.0.0/31") + dvs.remove_ip_address("Ethernet16", "11.0.0.0/31") + dvs.remove_ip_address("Ethernet20", "12.0.0.1/31") + + dvs.set_interface_status("Ethernet12", "down") + dvs.set_interface_status("Ethernet16", "down") + dvs.set_interface_status("Ethernet20", "down") + +def test_MirrorSessionWarmReboot(dvs, dvs_mirror_manager, setup_erspan_neighbors): + dvs_mirror = dvs_mirror_manager + + dvs.setup_db() + + # Setup the mirror session + dvs_mirror.create_erspan_session("test_session", "1.1.1.1", "2.2.2.2", "0x6558", "8", "100", "0") + + # Verify the monitor port + state_db = dvs.get_state_db() + state_db.wait_for_field_match("MIRROR_SESSION_TABLE", "test_session", {"monitor_port": "Ethernet12"}) + + # Setup ECMP routes to the session destination + dvs.change_route_ecmp("2.2.2.2", ["12.0.0.1", "11.0.0.1", "10.0.0.1"]) + + # Monitor port should not change b/c routes are ECMP + state_db.wait_for_field_match("MIRROR_SESSION_TABLE", "test_session", {"monitor_port": "Ethernet12"}) + + dvs.runcmd("config warm_restart enable swss") + dvs.stop_swss() + dvs.start_swss() + + dvs.check_swss_ready() + + # Monitor port should not change b/c destination is frozen + state_db.wait_for_field_match("MIRROR_SESSION_TABLE", "test_session", {"monitor_port": "Ethernet12"}) + + dvs_mirror.remove_mirror_session("test_session") + + # Revert the route back to the fixture-defined route + dvs.change_route("2.2.2.2", "10.0.0.1") + + # Reset for test cases after this one + dvs.stop_swss() + dvs.start_swss() + dvs.check_swss_ready() + + +def test_EverflowWarmReboot(dvs, dvs_acl, dvs_mirror_manager, dvs_policer_manager, setup_erspan_neighbors): + dvs_policer = dvs_policer_manager + dvs_mirror = dvs_mirror_manager + # Setup the policer + dvs_policer.create_policer("test_policer") + dvs_policer.verify_policer("test_policer") + + # Setup the mirror session + dvs_mirror.create_erspan_session("test_session", "1.1.1.1", "2.2.2.2", "0x6558", "8", "100", "0", policer="test_policer") + + state_db = dvs.get_state_db() + state_db.wait_for_field_match("MIRROR_SESSION_TABLE", "test_session", {"status": "active"}) + + # Create the mirror table + dvs_acl.create_acl_table("EVERFLOW_TEST", "MIRROR", ["Ethernet12"]) + + + # TODO: The standard methods for counting ACL tables and ACL rules break down after warm reboot b/c the OIDs + # of the default tables change. We also can't just re-initialize the default value b/c we've added another + # table and rule that aren't part of the base device config. We should come up with a way to handle warm reboot + # changes more gracefully to make it easier for future tests. + asic_db = dvs.get_asic_db() + asic_db.wait_for_n_keys("ASIC_STATE:SAI_OBJECT_TYPE_ACL_TABLE", 1 + len(asic_db.default_acl_tables)) + + # Create a mirror rule + dvs_acl.create_mirror_acl_rule("EVERFLOW_TEST", "TEST_RULE", {"SRC_IP": "20.0.0.2"}, "test_session") + asic_db.wait_for_n_keys("ASIC_STATE:SAI_OBJECT_TYPE_ACL_ENTRY", 1 + len(asic_db.default_acl_entries)) + + # Execute the warm reboot + dvs.runcmd("config warm_restart enable swss") + dvs.stop_swss() + dvs.start_swss() + + # Make sure the system is stable + dvs.check_swss_ready() + + # Verify that the ASIC DB is intact + dvs_policer.verify_policer("test_policer") + state_db.wait_for_field_match("MIRROR_SESSION_TABLE", "test_session", {"status": "active"}) + + asic_db = dvs.get_asic_db() + asic_db.wait_for_n_keys("ASIC_STATE:SAI_OBJECT_TYPE_ACL_TABLE", 1 + len(asic_db.default_acl_tables)) + asic_db.wait_for_n_keys("ASIC_STATE:SAI_OBJECT_TYPE_ACL_ENTRY", 1 + len(asic_db.default_acl_entries)) + + # Clean up + dvs_acl.remove_acl_rule("EVERFLOW_TEST", "TEST_RULE") + asic_db.wait_for_n_keys("ASIC_STATE:SAI_OBJECT_TYPE_ACL_ENTRY", len(asic_db.default_acl_entries)) + + dvs_acl.remove_acl_table("EVERFLOW_TEST") + asic_db.wait_for_n_keys("ASIC_STATE:SAI_OBJECT_TYPE_ACL_TABLE", len(asic_db.default_acl_tables)) + + dvs_mirror.remove_mirror_session("test_session") + dvs_mirror.verify_no_mirror() + + dvs_policer.remove_policer("test_policer") + dvs_policer.verify_no_policer() + + # Reset for test cases after this one + dvs.stop_swss() + dvs.start_swss() + dvs.check_swss_ready() + + + +# Add Dummy always-pass test at end as workaroud +# for issue when Flaky fail on final test it invokes module tear-down before retrying +def test_nonflaky_dummy(): + pass +