diff --git a/orchagent/mirrororch.cpp b/orchagent/mirrororch.cpp index 35a58cb8..23dc6849 100644 --- a/orchagent/mirrororch.cpp +++ b/orchagent/mirrororch.cpp @@ -31,6 +31,8 @@ #define MIRROR_SESSION_DST_PORT "dst_port" #define MIRROR_SESSION_DIRECTION "direction" #define MIRROR_SESSION_TYPE "type" +#define MIRROR_SESSION_SAMPLE_RATE "sample_rate" +#define MIRROR_SESSION_TRUNCATE_SIZE "truncate_size" #define MIRROR_SESSION_DEFAULT_VLAN_PRI 0 #define MIRROR_SESSION_DEFAULT_VLAN_CFI 0 @@ -47,6 +49,7 @@ extern sai_switch_api_t *sai_switch_api; extern sai_mirror_api_t *sai_mirror_api; extern sai_port_api_t *sai_port_api; +extern sai_samplepacket_api_t *sai_samplepacket_api; extern sai_object_id_t gSwitchId; extern PortsOrch* gPortsOrch; @@ -59,6 +62,9 @@ MirrorEntry::MirrorEntry(const string& platform) : dscp(8), ttl(255), queue(0), + sample_rate(0), + truncate_size(0), + samplepacketId(SAI_NULL_OBJECT_ID), sessionId(0), refCount(0) { @@ -382,16 +388,16 @@ task_process_status MirrorOrch::createEntry(const string& key, const vector(fvValue(i)); + } catch (const exception& e) { + SWSS_LOG_ERROR("Invalid sample_rate for session %s: %s", key.c_str(), e.what()); + return task_process_status::task_invalid_entry; + } + } + else if (fvField(i) == MIRROR_SESSION_TRUNCATE_SIZE) + { + try { + entry.truncate_size = to_uint(fvValue(i)); + } catch (const exception& e) { + SWSS_LOG_ERROR("Invalid truncate_size for session %s: %s", key.c_str(), e.what()); + return task_process_status::task_invalid_entry; + } + } else { SWSS_LOG_ERROR("Failed to parse session %s configuration. Unknown attribute %s", key.c_str(), fvField(i).c_str()); @@ -497,6 +521,54 @@ task_process_status MirrorOrch::createEntry(const string& key, const vector 0 && entry.sample_rate == 0) + { + entry.sample_rate = 1; + SWSS_LOG_NOTICE("Truncate size configured without sample rate for session %s; " + "defaulting sample rate to 1", key.c_str()); + } + + // Sampled mirroring requires an explicit direction + if (entry.sample_rate > 0 && entry.direction.empty()) + { + SWSS_LOG_ERROR("Sampled mirroring requires RX, TX or BOTH direction for session %s", + key.c_str()); + return task_process_status::task_invalid_entry; + } + + // Platform capability: reject early if the per-direction sampled mirroring + // capability is not supported by the ASIC. + if (entry.sample_rate > 0) + { + bool needIngressSample = (entry.direction == MIRROR_RX_DIRECTION || + entry.direction == MIRROR_BOTH_DIRECTION); + bool needEgressSample = (entry.direction == MIRROR_TX_DIRECTION || + entry.direction == MIRROR_BOTH_DIRECTION); + if (needIngressSample && !m_switchOrch->isPortIngressSampleMirrorSupported()) + { + SWSS_LOG_ERROR("Ingress sampled mirroring not supported on this platform, " + "rejecting session %s", key.c_str()); + return task_process_status::task_invalid_entry; + } + if (needEgressSample && !m_switchOrch->isPortEgressSampleMirrorSupported()) + { + SWSS_LOG_ERROR("Egress sampled mirroring not supported on this platform, " + "rejecting session %s", key.c_str()); + return task_process_status::task_invalid_entry; + } + } + + // Platform capability: reject early if samplepacket truncation is not supported + if (entry.truncate_size > 0 && !m_switchOrch->isSamplepacketTruncationSupported()) + { + SWSS_LOG_ERROR("Samplepacket truncation not supported on this platform, " + "rejecting session %s", key.c_str()); + return task_process_status::task_invalid_entry; + } + if (!isHwResourcesAvailable()) { SWSS_LOG_ERROR("Failed to create session %s: HW resources are not available", key.c_str()); @@ -634,6 +706,30 @@ void MirrorOrch::setSessionState(const string& name, const MirrorEntry& session, fvVector.emplace_back(MIRROR_SESSION_NEXT_HOP_IP, value); } + if (attr.empty() || attr == MIRROR_SESSION_SAMPLE_RATE) + { + if (session.sample_rate > 0) + { + fvVector.emplace_back(MIRROR_SESSION_SAMPLE_RATE, to_string(session.sample_rate)); + } + else + { + m_mirrorTable.hdel(name, MIRROR_SESSION_SAMPLE_RATE); + } + } + + if (attr.empty() || attr == MIRROR_SESSION_TRUNCATE_SIZE) + { + if (session.truncate_size > 0) + { + fvVector.emplace_back(MIRROR_SESSION_TRUNCATE_SIZE, to_string(session.truncate_size)); + } + else + { + m_mirrorTable.hdel(name, MIRROR_SESSION_TRUNCATE_SIZE); + } + } + m_mirrorTable.set(name, fvVector); } @@ -811,7 +907,9 @@ bool MirrorOrch::updateSession(const string& name, MirrorEntry& session) bool MirrorOrch::setUnsetPortMirror(Port port, bool ingress, bool set, - sai_object_id_t sessionId) + sai_object_id_t sessionId, + sai_object_id_t samplepacketId, + uint32_t sample_rate) { // Check if the mirror direction is supported by the ASIC if (ingress && !m_switchOrch->isPortIngressMirrorSupported()) @@ -826,6 +924,48 @@ bool MirrorOrch::setUnsetPortMirror(Port port, } sai_status_t status; + + if (sample_rate > 0) + { + // Sampled mirroring path: dispatch to per-PHY helper; LAG must be + // expanded to its member ports because SAMPLEPACKET/SAMPLE_MIRROR_SESSION + // are port-level SAI attributes and a LAG OID is not a valid target. + if (port.m_type == Port::LAG) + { + std::vector portv; + m_portsOrch->getLagMember(port, portv); + for (const auto &p : portv) + { + if (p.m_type != Port::PHY) + { + SWSS_LOG_ERROR("Failed to locate PHY member port %s of LAG %s", + p.m_alias.c_str(), port.m_alias.c_str()); + return false; + } + if (!setUnsetSampledMirrorOnPhyPort(p.m_port_id, p.m_alias, set, + ingress ? MirrorBindDirection::Ingress : MirrorBindDirection::Egress, + sessionId, samplepacketId)) + { + return false; + } + } + return true; + } + else if (port.m_type == Port::PHY) + { + return setUnsetSampledMirrorOnPhyPort(port.m_port_id, port.m_alias, set, + ingress ? MirrorBindDirection::Ingress : MirrorBindDirection::Egress, + sessionId, samplepacketId); + } + else + { + SWSS_LOG_ERROR("Sampled mirror not supported on port %s of type %d", + port.m_alias.c_str(), port.m_type); + return false; + } + } + + // Full mirror path (existing behavior) sai_attribute_t port_attr; port_attr.id = ingress ? SAI_PORT_ATTR_INGRESS_MIRROR_SESSION: SAI_PORT_ATTR_EGRESS_MIRROR_SESSION; @@ -896,7 +1036,7 @@ bool MirrorOrch::configurePortMirrorSession(const string& name, MirrorEntry& ses } if (session.direction == MIRROR_RX_DIRECTION || session.direction == MIRROR_BOTH_DIRECTION) { - if (!setUnsetPortMirror(port, true, set, session.sessionId)) + if (!setUnsetPortMirror(port, true, set, session.sessionId, session.samplepacketId, session.sample_rate)) { SWSS_LOG_ERROR("Failed to configure mirror session %s port %s", name.c_str(), port.m_alias.c_str()); @@ -905,7 +1045,7 @@ bool MirrorOrch::configurePortMirrorSession(const string& name, MirrorEntry& ses } if (session.direction == MIRROR_TX_DIRECTION || session.direction == MIRROR_BOTH_DIRECTION) { - if (!setUnsetPortMirror(port, false, set, session.sessionId)) + if (!setUnsetPortMirror(port, false, set, session.sessionId, session.samplepacketId, session.sample_rate)) { SWSS_LOG_ERROR("Failed to configure mirror session %s port %s", name.c_str(), port.m_alias.c_str()); @@ -1079,12 +1219,30 @@ bool MirrorOrch::activateSession(const string& name, MirrorEntry& session) session.status = true; + // Create SamplePacket if sample_rate > 0 (capability already verified in createEntry) + if (session.sample_rate > 0) + { + if (!createSamplePacket(name, session)) + { + SWSS_LOG_ERROR("Failed to create samplepacket, removing mirror session %s", name.c_str()); + sai_mirror_api->remove_mirror_session(session.sessionId); + session.status = false; + return false; + } + } + if (!session.src_port.empty() && !session.direction.empty()) { status = configurePortMirrorSession(name, session, true); if (status == false) { SWSS_LOG_ERROR("Failed to activate port mirror session %s", name.c_str()); + // Clean up samplepacket if it was created + if (session.samplepacketId != SAI_NULL_OBJECT_ID) + { + removeSamplePacket(name, session); + } + sai_mirror_api->remove_mirror_session(session.sessionId); session.status = false; return false; } @@ -1120,6 +1278,16 @@ bool MirrorOrch::deactivateSession(const string& name, MirrorEntry& session) } } + // Remove SamplePacket if it exists + if (session.samplepacketId != SAI_NULL_OBJECT_ID) + { + if (!removeSamplePacket(name, session)) + { + SWSS_LOG_ERROR("Failed to remove samplepacket for session %s", name.c_str()); + return false; + } + } + status = sai_mirror_api->remove_mirror_session(session.sessionId); if (status != SAI_STATUS_SUCCESS) @@ -1178,6 +1346,177 @@ bool MirrorOrch::updateSessionDstMac(const string& name, MirrorEntry& session) return true; } + +bool MirrorOrch::setUnsetSampledMirrorOnPhyPort(sai_object_id_t phy_port_id, + const std::string& phy_port_alias, + bool set, + MirrorBindDirection direction, + sai_object_id_t sessionId, + sai_object_id_t samplepacketId) +{ + SWSS_LOG_ENTER(); + + sai_status_t status; + + const bool ingress = (direction == MirrorBindDirection::Ingress); + const sai_port_attr_t samplepacketEnableAttrId = ingress ? + SAI_PORT_ATTR_INGRESS_SAMPLEPACKET_ENABLE : + SAI_PORT_ATTR_EGRESS_SAMPLEPACKET_ENABLE; + const sai_port_attr_t sampleMirrorSessionAttrId = ingress ? + SAI_PORT_ATTR_INGRESS_SAMPLE_MIRROR_SESSION : + SAI_PORT_ATTR_EGRESS_SAMPLE_MIRROR_SESSION; + + sai_attribute_t sp_attr; + sp_attr.id = samplepacketEnableAttrId; + sp_attr.value.oid = set ? samplepacketId : SAI_NULL_OBJECT_ID; + + sai_attribute_t mirror_attr; + mirror_attr.id = sampleMirrorSessionAttrId; + if (set) + { + mirror_attr.value.objlist.count = 1; + mirror_attr.value.objlist.list = &sessionId; + } + else + { + mirror_attr.value.objlist.count = 0; + } + + // Check for samplepacket conflict before binding + if (set) + { + sai_attribute_t check_attr; + check_attr.id = samplepacketEnableAttrId; + if (sai_port_api->get_port_attribute(phy_port_id, 1, &check_attr) == SAI_STATUS_SUCCESS + && check_attr.value.oid != SAI_NULL_OBJECT_ID + && check_attr.value.oid != samplepacketId) + { + SWSS_LOG_ERROR("Port %s SAMPLEPACKET_ENABLE already bound to " + "OID 0x%" PRIx64 ", cannot bind sampled mirror", + phy_port_alias.c_str(), check_attr.value.oid); + return false; + } + } + + if (set) + { + // Set: SAMPLEPACKET_ENABLE first, then SAMPLE_MIRROR_SESSION + status = sai_port_api->set_port_attribute(phy_port_id, &sp_attr); + if (status != SAI_STATUS_SUCCESS) + { + SWSS_LOG_ERROR("Failed to set SAMPLEPACKET_ENABLE on port %s, status %d", + phy_port_alias.c_str(), status); + return false; + } + status = sai_port_api->set_port_attribute(phy_port_id, &mirror_attr); + if (status != SAI_STATUS_SUCCESS) + { + SWSS_LOG_ERROR("Failed to set SAMPLE_MIRROR_SESSION on port %s, status %d", + phy_port_alias.c_str(), status); + // Rollback: clear SAMPLEPACKET_ENABLE + sp_attr.value.oid = SAI_NULL_OBJECT_ID; + sai_port_api->set_port_attribute(phy_port_id, &sp_attr); + return false; + } + } + else + { + // Clear: SAMPLE_MIRROR_SESSION first, then SAMPLEPACKET_ENABLE (reverse order) + status = sai_port_api->set_port_attribute(phy_port_id, &mirror_attr); + if (status != SAI_STATUS_SUCCESS) + { + SWSS_LOG_ERROR("Failed to clear SAMPLE_MIRROR_SESSION on port %s, status %d", + phy_port_alias.c_str(), status); + return false; + } + status = sai_port_api->set_port_attribute(phy_port_id, &sp_attr); + if (status != SAI_STATUS_SUCCESS) + { + SWSS_LOG_ERROR("Failed to clear SAMPLEPACKET_ENABLE on port %s, status %d", + phy_port_alias.c_str(), status); + return false; + } + } + return true; +} + +bool MirrorOrch::createSamplePacket(const string& name, MirrorEntry& session) +{ + SWSS_LOG_ENTER(); + + sai_attribute_t attrs[4]; + uint32_t attr_count = 0; + + attrs[attr_count].id = SAI_SAMPLEPACKET_ATTR_SAMPLE_RATE; + attrs[attr_count].value.u32 = session.sample_rate; + attr_count++; + + attrs[attr_count].id = SAI_SAMPLEPACKET_ATTR_TYPE; + attrs[attr_count].value.s32 = SAI_SAMPLEPACKET_TYPE_MIRROR_SESSION; + attr_count++; + + // Truncation capability already verified in createEntry + if (session.truncate_size > 0) + { + attrs[attr_count].id = SAI_SAMPLEPACKET_ATTR_TRUNCATE_ENABLE; + attrs[attr_count].value.booldata = true; + attr_count++; + + attrs[attr_count].id = SAI_SAMPLEPACKET_ATTR_TRUNCATE_SIZE; + attrs[attr_count].value.u32 = session.truncate_size; + attr_count++; + } + + sai_status_t status = sai_samplepacket_api->create_samplepacket( + &session.samplepacketId, gSwitchId, attr_count, attrs); + + if (status != SAI_STATUS_SUCCESS) + { + SWSS_LOG_ERROR("Failed to create samplepacket for session %s, status %d", + name.c_str(), status); + session.samplepacketId = SAI_NULL_OBJECT_ID; + task_process_status handle_status = handleSaiCreateStatus(SAI_API_SAMPLEPACKET, status); + if (handle_status != task_success) + { + return parseHandleSaiStatusFailure(handle_status); + } + } + else + { + SWSS_LOG_NOTICE("Created samplepacket for session %s, rate %u, truncate %u", + name.c_str(), session.sample_rate, session.truncate_size); + } + + return (status == SAI_STATUS_SUCCESS); +} + +bool MirrorOrch::removeSamplePacket(const string& name, MirrorEntry& session) +{ + SWSS_LOG_ENTER(); + + if (session.samplepacketId == SAI_NULL_OBJECT_ID) + { + return true; + } + + sai_status_t status = sai_samplepacket_api->remove_samplepacket(session.samplepacketId); + + if (status != SAI_STATUS_SUCCESS) + { + SWSS_LOG_ERROR("Failed to remove samplepacket for session %s, status %d", + name.c_str(), status); + task_process_status handle_status = handleSaiRemoveStatus(SAI_API_SAMPLEPACKET, status); + if (handle_status != task_success) + { + return parseHandleSaiStatusFailure(handle_status); + } + } + + session.samplepacketId = SAI_NULL_OBJECT_ID; + SWSS_LOG_NOTICE("Removed samplepacket for session %s", name.c_str()); + return true; +} + bool MirrorOrch::updateSessionDstPort(const string& name, MirrorEntry& session) { SWSS_LOG_ENTER(); @@ -1473,11 +1812,13 @@ void MirrorOrch::updateLagMember(const LagMemberUpdate& update) { if (session.direction == MIRROR_RX_DIRECTION || session.direction == MIRROR_BOTH_DIRECTION) { - setUnsetPortMirror(update.member, true, update.add, session.sessionId); + setUnsetPortMirror(update.member, true, update.add, session.sessionId, + session.samplepacketId, session.sample_rate); } if (session.direction == MIRROR_TX_DIRECTION || session.direction == MIRROR_BOTH_DIRECTION) { - setUnsetPortMirror(update.member, false, update.add, session.sessionId); + setUnsetPortMirror(update.member, false, update.add, session.sessionId, + session.samplepacketId, session.sample_rate); } } diff --git a/orchagent/mirrororch.h b/orchagent/mirrororch.h index 15bdc789..48ef4f57 100644 --- a/orchagent/mirrororch.h +++ b/orchagent/mirrororch.h @@ -24,6 +24,12 @@ #define MIRROR_SESSION_SPAN "SPAN" #define MIRROR_SESSION_ERSPAN "ERSPAN" +enum class MirrorBindDirection +{ + Ingress, + Egress +}; + /* * Contains session data specified by user in config file * and data required for MAC address and port resolution @@ -57,6 +63,11 @@ struct MirrorEntry sai_object_id_t portId; } neighborInfo; + // Sampled mirroring fields + uint32_t sample_rate; // 0 = full mirror (default) + uint32_t truncate_size; // 0 = no truncation (default) + sai_object_id_t samplepacketId; // SAI_NULL_OBJECT_ID if not sampled + sai_object_id_t sessionId; int64_t refCount; @@ -136,9 +147,21 @@ class MirrorOrch : public Orch, public Observer, public Subject bool validateSrcPortList(const string& srcPort); bool validateDstPort(const string& dstPort); bool setUnsetPortMirror(Port port, bool ingress, bool set, - sai_object_id_t sessionId); + sai_object_id_t sessionId, + sai_object_id_t samplepacketId = SAI_NULL_OBJECT_ID, + uint32_t sample_rate = 0); bool configurePortMirrorSession(const string&, MirrorEntry&, bool enable); + // Sampled mirroring helpers + bool createSamplePacket(const string& name, MirrorEntry& session); + bool removeSamplePacket(const string& name, MirrorEntry& session); + bool setUnsetSampledMirrorOnPhyPort(sai_object_id_t phy_port_id, + const std::string& phy_port_alias, + bool set, + MirrorBindDirection direction, + sai_object_id_t sessionId, + sai_object_id_t samplepacketId); + void doTask(Consumer& consumer); }; diff --git a/orchagent/sfloworch.cpp b/orchagent/sfloworch.cpp index 2ec367b4..adbd181e 100644 --- a/orchagent/sfloworch.cpp +++ b/orchagent/sfloworch.cpp @@ -107,6 +107,36 @@ bool SflowOrch::sflowUpdateRate(sai_object_id_t port_id, uint32_t rate) return true; } +bool SflowOrch::isSflowSamplePacket(sai_object_id_t oid) +{ + for (auto& it : m_sflowRateSampleMap) + { + if (it.second.m_sample_id == oid) + return true; + } + return false; +} + +// Check-before-set: refuse to bind sFlow if this port's samplepacket attr is +// already owned by another (non-sFlow) feature +bool SflowOrch::isSamplepacketFreeForSflow(sai_object_id_t port_id, sai_port_attr_t attr_id, + sai_object_id_t sample_id, const char* dir_name) +{ + sai_attribute_t check_attr; + check_attr.id = attr_id; + if (sai_port_api->get_port_attribute(port_id, 1, &check_attr) == SAI_STATUS_SUCCESS + && check_attr.value.oid != SAI_NULL_OBJECT_ID + && check_attr.value.oid != sample_id + && !isSflowSamplePacket(check_attr.value.oid)) + { + SWSS_LOG_ERROR("Port %" PRIx64 " %s_SAMPLEPACKET_ENABLE already bound to " + "OID 0x%" PRIx64 ", cannot bind sFlow", + port_id, dir_name, check_attr.value.oid); + return false; + } + return true; +} + bool SflowOrch::sflowAddPort(sai_object_id_t sample_id, sai_object_id_t port_id, string direction) { sai_attribute_t attr; @@ -115,7 +145,22 @@ bool SflowOrch::sflowAddPort(sai_object_id_t sample_id, sai_object_id_t port_id, SWSS_LOG_DEBUG("sflowAddPort %" PRIx64 " portOid %" PRIx64 " dir %s", sample_id, port_id, direction.c_str()); - if (direction == "both" || direction == "rx") + bool need_ingress = (direction == "both" || direction == "rx"); + bool need_egress = (direction == "both" || direction == "tx"); + + // If the port's samplepacket is already owned by another (non-sFlow) feature, refuse to bind. + if (need_ingress && + !isSamplepacketFreeForSflow(port_id, SAI_PORT_ATTR_INGRESS_SAMPLEPACKET_ENABLE, sample_id, "INGRESS")) + { + return false; + } + if (need_egress && + !isSamplepacketFreeForSflow(port_id, SAI_PORT_ATTR_EGRESS_SAMPLEPACKET_ENABLE, sample_id, "EGRESS")) + { + return false; + } + + if (need_ingress) { attr.id = SAI_PORT_ATTR_INGRESS_SAMPLEPACKET_ENABLE; attr.value.oid = sample_id; @@ -132,7 +177,7 @@ bool SflowOrch::sflowAddPort(sai_object_id_t sample_id, sai_object_id_t port_id, } } - if (direction == "both" || direction == "tx") + if (need_egress) { attr.id = SAI_PORT_ATTR_EGRESS_SAMPLEPACKET_ENABLE; attr.value.oid = sample_id; @@ -224,6 +269,18 @@ bool SflowOrch::sflowUpdateSampleDirection(sai_object_id_t port_id, string old_d egr_sample_oid = port_info->second.m_sample_id; } + // Check-before-set: on a direction change + if (ing_sample_oid != SAI_NULL_OBJECT_ID && + !isSamplepacketFreeForSflow(port_id, SAI_PORT_ATTR_INGRESS_SAMPLEPACKET_ENABLE, ing_sample_oid, "INGRESS")) + { + return false; + } + if (egr_sample_oid != SAI_NULL_OBJECT_ID && + !isSamplepacketFreeForSflow(port_id, SAI_PORT_ATTR_EGRESS_SAMPLEPACKET_ENABLE, egr_sample_oid, "EGRESS")) + { + return false; + } + attr.id = SAI_PORT_ATTR_INGRESS_SAMPLEPACKET_ENABLE; attr.value.oid = ing_sample_oid; sai_rc = sai_port_api->set_port_attribute(port_id, &attr); diff --git a/orchagent/sfloworch.h b/orchagent/sfloworch.h index 508b22c0..9e215f03 100644 --- a/orchagent/sfloworch.h +++ b/orchagent/sfloworch.h @@ -47,4 +47,7 @@ class SflowOrch : public Orch uint32_t sflowSessionGetRate(sai_object_id_t sample_id); bool handleSflowSessionDel(sai_object_id_t port_id); void sflowExtractInfo(std::vector &fvs, bool &admin, uint32_t &rate, string &dir); + bool isSflowSamplePacket(sai_object_id_t oid); + bool isSamplepacketFreeForSflow(sai_object_id_t port_id, sai_port_attr_t attr_id, + sai_object_id_t sample_id, const char* dir_name); }; diff --git a/orchagent/switchorch.cpp b/orchagent/switchorch.cpp index 20bf4541..0273a263 100644 --- a/orchagent/switchorch.cpp +++ b/orchagent/switchorch.cpp @@ -166,6 +166,7 @@ SwitchOrch::SwitchOrch(DBConnector *db, vector& connectors, Tabl querySwitchTpidCapability(); querySwitchPortEgressSampleCapability(); querySwitchPortMirrorCapability(); + querySwitchSamplePacketCapability(); querySwitchHashDefaults(); setSwitchIcmpOffloadCapability(); setFastLinkupCapability(); @@ -1957,6 +1958,87 @@ void SwitchOrch::querySwitchPortMirrorCapability() set_switch_capability(fvVector); } +void SwitchOrch::querySwitchSamplePacketCapability() +{ + vector fvVector; + sai_status_t status = SAI_STATUS_SUCCESS; + sai_attr_capability_t capability; + + // Check if SAI is capable of handling Port ingress sample mirror session + status = sai_query_attribute_capability(gSwitchId, SAI_OBJECT_TYPE_PORT, + SAI_PORT_ATTR_INGRESS_SAMPLE_MIRROR_SESSION, &capability); + if (status != SAI_STATUS_SUCCESS) + { + SWSS_LOG_WARN("Could not query port ingress sample mirror capability %d", status); + fvVector.emplace_back(SWITCH_CAPABILITY_TABLE_PORT_INGRESS_SAMPLE_MIRROR_CAPABLE, "false"); + m_portIngressSampleMirrorSupported = false; + } + else + { + if (capability.set_implemented) + { + fvVector.emplace_back(SWITCH_CAPABILITY_TABLE_PORT_INGRESS_SAMPLE_MIRROR_CAPABLE, "true"); + m_portIngressSampleMirrorSupported = true; + } + else + { + fvVector.emplace_back(SWITCH_CAPABILITY_TABLE_PORT_INGRESS_SAMPLE_MIRROR_CAPABLE, "false"); + m_portIngressSampleMirrorSupported = false; + } + SWSS_LOG_NOTICE("port ingress sample mirror capability %d", capability.set_implemented); + } + + // Check if SAI is capable of handling Port egress sample mirror session + status = sai_query_attribute_capability(gSwitchId, SAI_OBJECT_TYPE_PORT, + SAI_PORT_ATTR_EGRESS_SAMPLE_MIRROR_SESSION, &capability); + if (status != SAI_STATUS_SUCCESS) + { + SWSS_LOG_WARN("Could not query port egress sample mirror capability %d", status); + fvVector.emplace_back(SWITCH_CAPABILITY_TABLE_PORT_EGRESS_SAMPLE_MIRROR_CAPABLE, "false"); + m_portEgressSampleMirrorSupported = false; + } + else + { + if (capability.set_implemented) + { + fvVector.emplace_back(SWITCH_CAPABILITY_TABLE_PORT_EGRESS_SAMPLE_MIRROR_CAPABLE, "true"); + m_portEgressSampleMirrorSupported = true; + } + else + { + fvVector.emplace_back(SWITCH_CAPABILITY_TABLE_PORT_EGRESS_SAMPLE_MIRROR_CAPABLE, "false"); + m_portEgressSampleMirrorSupported = false; + } + SWSS_LOG_NOTICE("port egress sample mirror capability %d", capability.set_implemented); + } + + // Check if SAI is capable of handling samplepacket truncation + status = sai_query_attribute_capability(gSwitchId, SAI_OBJECT_TYPE_SAMPLEPACKET, + SAI_SAMPLEPACKET_ATTR_TRUNCATE_ENABLE, &capability); + if (status != SAI_STATUS_SUCCESS) + { + SWSS_LOG_WARN("Could not query samplepacket truncation capability %d", status); + fvVector.emplace_back(SWITCH_CAPABILITY_TABLE_SAMPLEPACKET_TRUNCATION_CAPABLE, "false"); + m_samplepacketTruncationSupported = false; + } + else + { + if (capability.set_implemented) + { + fvVector.emplace_back(SWITCH_CAPABILITY_TABLE_SAMPLEPACKET_TRUNCATION_CAPABLE, "true"); + m_samplepacketTruncationSupported = true; + } + else + { + fvVector.emplace_back(SWITCH_CAPABILITY_TABLE_SAMPLEPACKET_TRUNCATION_CAPABLE, "false"); + m_samplepacketTruncationSupported = false; + } + SWSS_LOG_NOTICE("samplepacket truncation capability %d", capability.set_implemented); + } + + set_switch_capability(fvVector); +} + void SwitchOrch::querySwitchTpidCapability() { SWSS_LOG_ENTER(); diff --git a/orchagent/switchorch.h b/orchagent/switchorch.h index 380b94a2..a52be2d9 100644 --- a/orchagent/switchorch.h +++ b/orchagent/switchorch.h @@ -33,6 +33,9 @@ #define SWITCH_CAPABILITY_TABLE_REG_NOTICE_ASIC_SDK_HEALTH_CATEGORY "REG_NOTICE_ASIC_SDK_HEALTH_CATEGORY" #define SWITCH_CAPABILITY_TABLE_PORT_INGRESS_MIRROR_CAPABLE "PORT_INGRESS_MIRROR_CAPABLE" #define SWITCH_CAPABILITY_TABLE_PORT_EGRESS_MIRROR_CAPABLE "PORT_EGRESS_MIRROR_CAPABLE" +#define SWITCH_CAPABILITY_TABLE_PORT_INGRESS_SAMPLE_MIRROR_CAPABLE "PORT_INGRESS_SAMPLE_MIRROR_CAPABLE" +#define SWITCH_CAPABILITY_TABLE_PORT_EGRESS_SAMPLE_MIRROR_CAPABLE "PORT_EGRESS_SAMPLE_MIRROR_CAPABLE" +#define SWITCH_CAPABILITY_TABLE_SAMPLEPACKET_TRUNCATION_CAPABLE "SAMPLEPACKET_TRUNCATION_CAPABLE" #define SWITCH_STAT_COUNTER_FLEX_COUNTER_GROUP "SWITCH_STAT_COUNTER" @@ -88,6 +91,9 @@ class SwitchOrch : public Orch // Mirror capability interface for MirrorOrch bool isPortIngressMirrorSupported() const { return m_portIngressMirrorSupported; } bool isPortEgressMirrorSupported() const { return m_portEgressMirrorSupported; } + bool isPortIngressSampleMirrorSupported() const { return m_portIngressSampleMirrorSupported; } + bool isPortEgressSampleMirrorSupported() const { return m_portEgressSampleMirrorSupported; } + bool isSamplepacketTruncationSupported() const { return m_samplepacketTruncationSupported; } private: void doTask(Consumer &consumer); @@ -102,6 +108,7 @@ class SwitchOrch : public Orch void querySwitchTpidCapability(); void querySwitchPortEgressSampleCapability(); void querySwitchPortMirrorCapability(); + void querySwitchSamplePacketCapability(); // Statistics void generateSwitchCounterNameMap() const; @@ -184,6 +191,9 @@ class SwitchOrch : public Orch // Port mirror capabilities bool m_portIngressMirrorSupported = false; bool m_portEgressMirrorSupported = false; + bool m_portIngressSampleMirrorSupported = false; + bool m_portEgressSampleMirrorSupported = false; + bool m_samplepacketTruncationSupported = false; // ASIC SDK health event std::shared_ptr m_stateDbForNotification = nullptr; diff --git a/tests/dvslib/dvs_mirror.py b/tests/dvslib/dvs_mirror.py index 70c42bad..6ac06258 100644 --- a/tests/dvslib/dvs_mirror.py +++ b/tests/dvslib/dvs_mirror.py @@ -24,6 +24,7 @@ def create_span_session(self, name, dst_port, src_ports=None, direction="BOTH", def create_erspan_session(self, name, src, dst, gre, dscp, ttl, queue, policer=None, src_ports=None, direction="BOTH"): mirror_entry = { + "type": "ERSPAN", "src_ip": src, "dst_ip": dst, "gre_type": gre, @@ -41,6 +42,32 @@ def create_erspan_session(self, name, src, dst, gre, dscp, ttl, queue, policer=N self.config_db.create_entry("MIRROR_SESSION", name, mirror_entry) + + def create_erspan_session_sampled(self, name, src, dst, gre, dscp, ttl, queue, + policer=None, src_ports=None, direction="RX", + sample_rate=None, truncate_size=None): + mirror_entry = { + "type": "ERSPAN", + "src_ip": src, + "dst_ip": dst, + "gre_type": gre, + "dscp": dscp, + "ttl": ttl, + "queue": queue, + "direction": direction + } + + if policer: + mirror_entry["policer"] = policer + if src_ports: + mirror_entry["src_port"] = src_ports + if sample_rate: + mirror_entry["sample_rate"] = sample_rate + if truncate_size: + mirror_entry["truncate_size"] = truncate_size + + self.config_db.create_entry("MIRROR_SESSION", name, mirror_entry) + def remove_mirror_session(self, name): self.config_db.delete_entry("MIRROR_SESSION", name) diff --git a/tests/mock_tests/Makefile.am b/tests/mock_tests/Makefile.am index 08ee4c35..c3b52df5 100644 --- a/tests/mock_tests/Makefile.am +++ b/tests/mock_tests/Makefile.am @@ -94,6 +94,7 @@ tests_SOURCES = aclorch_ut.cpp \ saihelper_ut.cpp \ mock_saihelper.cpp \ mirrororch_ut.cpp \ + mirrororch_sample_port_sai_wrap.cpp \ hftelorch_ut.cpp \ hftelorch_notify_ut.cpp \ hftelorch_is_supported_sai_wrap.cpp \ diff --git a/tests/mock_tests/mirrororch_sample_port_sai_wrap.cpp b/tests/mock_tests/mirrororch_sample_port_sai_wrap.cpp new file mode 100644 index 00000000..bc2cfaa8 --- /dev/null +++ b/tests/mock_tests/mirrororch_sample_port_sai_wrap.cpp @@ -0,0 +1,203 @@ +/* + * Runtime emulation of the INGRESS/EGRESS sample port attributes for mirrororch_ut. + * See mirrororch_sample_port_sai_wrap.h for the rationale. + */ +#include "mirrororch_sample_port_sai_wrap.h" + +#include "saiport.h" +#include "saisamplepacket.h" +#include "saistatus.h" +#include "saitypes.h" + +#include + +extern sai_port_api_t *sai_port_api; +extern sai_samplepacket_api_t *sai_samplepacket_api; + +namespace +{ + sai_port_api_t *g_real_port_api = nullptr; + sai_port_api_t g_wrap_port_api; + + sai_samplepacket_api_t *g_real_samplepacket_api = nullptr; + sai_samplepacket_api_t g_wrap_samplepacket_api; + + // Per-port emulated value of SAI_PORT_ATTR_{INGRESS,EGRESS}_SAMPLEPACKET_ENABLE. + std::map g_ingress_samplepacket; + std::map g_egress_samplepacket; + + bool isIntercepted(sai_attr_id_t id) + { + return id == SAI_PORT_ATTR_INGRESS_SAMPLEPACKET_ENABLE + || id == SAI_PORT_ATTR_INGRESS_SAMPLE_MIRROR_SESSION + || id == SAI_PORT_ATTR_EGRESS_SAMPLEPACKET_ENABLE + || id == SAI_PORT_ATTR_EGRESS_SAMPLE_MIRROR_SESSION; + } + + extern "C" sai_status_t wrap_set_port_attribute( + sai_object_id_t port_id, + const sai_attribute_t *attr) + { + if (attr == nullptr) + { + return SAI_STATUS_INVALID_PARAMETER; + } + + if (attr->id == SAI_PORT_ATTR_INGRESS_SAMPLEPACKET_ENABLE + || attr->id == SAI_PORT_ATTR_EGRESS_SAMPLEPACKET_ENABLE) + { + if (mirror_sample_port_wrap_ut::g_fail_samplepacket_enable_set) + { + return SAI_STATUS_FAILURE; + } + auto &store = (attr->id == SAI_PORT_ATTR_INGRESS_SAMPLEPACKET_ENABLE) + ? g_ingress_samplepacket : g_egress_samplepacket; + if (attr->value.oid == SAI_NULL_OBJECT_ID) + { + store.erase(port_id); + } + else + { + store[port_id] = attr->value.oid; + } + return SAI_STATUS_SUCCESS; + } + + if (attr->id == SAI_PORT_ATTR_INGRESS_SAMPLE_MIRROR_SESSION + || attr->id == SAI_PORT_ATTR_EGRESS_SAMPLE_MIRROR_SESSION) + { + if (mirror_sample_port_wrap_ut::g_fail_mirror_session_set) + { + return SAI_STATUS_FAILURE; + } + // Validate the call framing the production code uses (bind: a + // single non-null session; unbind: an empty list) so a malformed + // attribute is still caught, without depending on libsaivs. + if (attr->value.objlist.count == 0) + { + return SAI_STATUS_SUCCESS; + } + if (attr->value.objlist.count == 1 + && attr->value.objlist.list != nullptr + && attr->value.objlist.list[0] != SAI_NULL_OBJECT_ID) + { + return SAI_STATUS_SUCCESS; + } + return SAI_STATUS_INVALID_PARAMETER; + } + + return g_real_port_api->set_port_attribute(port_id, attr); + } + + extern "C" sai_status_t wrap_get_port_attribute( + sai_object_id_t port_id, + uint32_t attr_count, + sai_attribute_t *attr_list) + { + if (attr_list != nullptr && attr_count == 1 && isIntercepted(attr_list[0].id)) + { + if (attr_list[0].id == SAI_PORT_ATTR_INGRESS_SAMPLEPACKET_ENABLE + || attr_list[0].id == SAI_PORT_ATTR_EGRESS_SAMPLEPACKET_ENABLE) + { + auto &store = + (attr_list[0].id == SAI_PORT_ATTR_INGRESS_SAMPLEPACKET_ENABLE) + ? g_ingress_samplepacket : g_egress_samplepacket; + auto it = store.find(port_id); + attr_list[0].value.oid = + (it != store.end()) ? it->second : SAI_NULL_OBJECT_ID; + return SAI_STATUS_SUCCESS; + } + + // {INGRESS,EGRESS}_SAMPLE_MIRROR_SESSION: report unbound. + attr_list[0].value.objlist.count = 0; + return SAI_STATUS_SUCCESS; + } + + return g_real_port_api->get_port_attribute(port_id, attr_count, attr_list); + } + + extern "C" sai_status_t wrap_create_samplepacket( + sai_object_id_t *samplepacket_id, + sai_object_id_t switch_id, + uint32_t attr_count, + const sai_attribute_t *attr_list) + { + if (mirror_sample_port_wrap_ut::g_fail_samplepacket_create) + { + // Use a retryable status so handleSaiCreateStatus maps it to + // task_need_retry and createSamplePacket actually returns false + return SAI_STATUS_INSUFFICIENT_RESOURCES; + } + return g_real_samplepacket_api->create_samplepacket( + samplepacket_id, switch_id, attr_count, attr_list); + } + + extern "C" sai_status_t wrap_remove_samplepacket(sai_object_id_t samplepacket_id) + { + if (mirror_sample_port_wrap_ut::g_fail_samplepacket_remove) + { + // SAI_STATUS_OBJECT_IN_USE is the retryable status + // that maps to task_need_retry so removeSamplePacket returns false. + return SAI_STATUS_OBJECT_IN_USE; + } + return g_real_samplepacket_api->remove_samplepacket(samplepacket_id); + } +} + +namespace mirror_sample_port_wrap_ut +{ + bool g_fail_mirror_session_set = false; + bool g_fail_samplepacket_enable_set = false; + bool g_fail_samplepacket_create = false; + bool g_fail_samplepacket_remove = false; + + void install() + { + g_fail_mirror_session_set = false; + g_fail_samplepacket_enable_set = false; + g_fail_samplepacket_create = false; + g_fail_samplepacket_remove = false; + + if (sai_port_api == &g_wrap_port_api + && sai_samplepacket_api == &g_wrap_samplepacket_api) + { + // Already wrapped (defensive against accidental nesting): keep the + // existing emulated state instead of wiping it. + return; + } + + g_real_port_api = sai_port_api; + g_wrap_port_api = *sai_port_api; + g_wrap_port_api.set_port_attribute = wrap_set_port_attribute; + g_wrap_port_api.get_port_attribute = wrap_get_port_attribute; + + g_real_samplepacket_api = sai_samplepacket_api; + g_wrap_samplepacket_api = *sai_samplepacket_api; + g_wrap_samplepacket_api.create_samplepacket = wrap_create_samplepacket; + g_wrap_samplepacket_api.remove_samplepacket = wrap_remove_samplepacket; + + g_ingress_samplepacket.clear(); + g_egress_samplepacket.clear(); + sai_port_api = &g_wrap_port_api; + sai_samplepacket_api = &g_wrap_samplepacket_api; + } + + void uninstall() + { + g_fail_mirror_session_set = false; + g_fail_samplepacket_enable_set = false; + g_fail_samplepacket_create = false; + g_fail_samplepacket_remove = false; + + if (sai_port_api == &g_wrap_port_api && g_real_port_api != nullptr) + { + sai_port_api = g_real_port_api; + } + if (sai_samplepacket_api == &g_wrap_samplepacket_api && g_real_samplepacket_api != nullptr) + { + sai_samplepacket_api = g_real_samplepacket_api; + } + g_ingress_samplepacket.clear(); + g_egress_samplepacket.clear(); + } +} diff --git a/tests/mock_tests/mirrororch_sample_port_sai_wrap.h b/tests/mock_tests/mirrororch_sample_port_sai_wrap.h new file mode 100644 index 00000000..675f356d --- /dev/null +++ b/tests/mock_tests/mirrororch_sample_port_sai_wrap.h @@ -0,0 +1,32 @@ +#pragma once + +/* + * Test-only: libsaivs handles SAI_PORT_ATTR_INGRESS_SAMPLEPACKET_ENABLE by + * running real `tc qdisc ... ingress` on a tap, which fails in UT (no taps). + * PortSampleSaiGuard RAII-swaps sai_port_api to emulate the INGRESS sample + * attrs in memory and delegates everything else to the real libsaivs. + */ +namespace mirror_sample_port_wrap_ut +{ + // Test-only fault injection: when set, the wrapped set_port_attribute + // returns SAI_STATUS_FAILURE for the matching attribute so the error/rollback + // paths in mirrororch can be exercised. Reset by install()/uninstall(). + extern bool g_fail_mirror_session_set; + extern bool g_fail_samplepacket_enable_set; + // Fault injection for the sai_samplepacket_api: fail create/remove so the + // samplepacket create/remove error paths in mirrororch can be exercised. + extern bool g_fail_samplepacket_create; + extern bool g_fail_samplepacket_remove; + + void install(); + void uninstall(); + + struct PortSampleSaiGuard + { + PortSampleSaiGuard() { install(); } + ~PortSampleSaiGuard() { uninstall(); } + + PortSampleSaiGuard(const PortSampleSaiGuard&) = delete; + PortSampleSaiGuard& operator=(const PortSampleSaiGuard&) = delete; + }; +} diff --git a/tests/mock_tests/mirrororch_ut.cpp b/tests/mock_tests/mirrororch_ut.cpp index 7e3f9c94..a11b17ef 100644 --- a/tests/mock_tests/mirrororch_ut.cpp +++ b/tests/mock_tests/mirrororch_ut.cpp @@ -16,6 +16,11 @@ #include "mirrororch.h" #undef private #include "mock_orch_test.h" +#include "mirrororch_sample_port_sai_wrap.h" +#include +#include + +extern sai_mirror_api_t *sai_mirror_api; namespace mirrororch_test { @@ -53,5 +58,1094 @@ namespace mirrororch_test auto ret = gMirrorOrch->setUnsetPortMirror(dummyPort, /*ingress*/ false, /*set*/ true, /*sessionId*/ SAI_NULL_OBJECT_ID); ASSERT_FALSE(ret); } -} + TEST_F(MirrorOrchTest, CreateEntryRejectsSampledWhenUnsupported) + { + // Verify createEntry rejects sample_rate > 0 when platform does not + // advertise PORT_INGRESS_SAMPLE_MIRROR capability. + ASSERT_NE(gSwitchOrch, nullptr); + ASSERT_NE(gMirrorOrch, nullptr); + + gSwitchOrch->m_portIngressMirrorSupported = true; + gSwitchOrch->m_portIngressSampleMirrorSupported = false; + gSwitchOrch->m_portEgressSampleMirrorSupported = true; + gSwitchOrch->m_samplepacketTruncationSupported = true; + + std::vector data; + data.emplace_back("type", "ERSPAN"); + data.emplace_back("src_ip", "10.0.0.1"); + data.emplace_back("dst_ip", "10.0.0.2"); + data.emplace_back("gre_type", "0x88be"); + data.emplace_back("dscp", "0"); + data.emplace_back("ttl", "64"); + data.emplace_back("queue", "0"); + data.emplace_back("src_port", "Ethernet0"); + data.emplace_back("direction", "RX"); + data.emplace_back("sample_rate", "50000"); + + auto status = gMirrorOrch->createEntry("test_reject_sampled", data); + ASSERT_EQ(status, task_process_status::task_invalid_entry); + + // No session should have been registered + ASSERT_EQ(gMirrorOrch->m_syncdMirrors.count("test_reject_sampled"), (size_t)0); + } + + TEST_F(MirrorOrchTest, CreateEntryRejectsTruncationWhenUnsupported) + { + // Verify createEntry rejects truncate_size > 0 when platform does not + // advertise SAMPLEPACKET_TRUNCATION capability. + ASSERT_NE(gSwitchOrch, nullptr); + ASSERT_NE(gMirrorOrch, nullptr); + + gSwitchOrch->m_portIngressMirrorSupported = true; + gSwitchOrch->m_portIngressSampleMirrorSupported = true; + gSwitchOrch->m_samplepacketTruncationSupported = false; + + std::vector data; + data.emplace_back("type", "ERSPAN"); + data.emplace_back("src_ip", "10.0.0.1"); + data.emplace_back("dst_ip", "10.0.0.2"); + data.emplace_back("gre_type", "0x88be"); + data.emplace_back("dscp", "0"); + data.emplace_back("ttl", "64"); + data.emplace_back("queue", "0"); + data.emplace_back("src_port", "Ethernet0"); + data.emplace_back("direction", "RX"); + data.emplace_back("sample_rate", "50000"); + data.emplace_back("truncate_size", "128"); + + auto status = gMirrorOrch->createEntry("test_reject_trunc", data); + ASSERT_EQ(status, task_process_status::task_invalid_entry); + + ASSERT_EQ(gMirrorOrch->m_syncdMirrors.count("test_reject_trunc"), (size_t)0); + } + + TEST_F(MirrorOrchTest, RemoveSamplePacketHandlesNullOid) + { + // Verify removeSamplePacket handles NULL samplepacketId gracefully + ASSERT_NE(gMirrorOrch, nullptr); + + MirrorEntry entry(""); + entry.samplepacketId = SAI_NULL_OBJECT_ID; + + bool ret = gMirrorOrch->removeSamplePacket("test_session", entry); + ASSERT_TRUE(ret); + ASSERT_EQ(entry.samplepacketId, SAI_NULL_OBJECT_ID); + } + + TEST_F(MirrorOrchTest, MirrorEntryDefaultValues) + { + // Verify MirrorEntry initializes new fields correctly + MirrorEntry entry(""); + ASSERT_EQ(entry.sample_rate, (uint32_t)0); + ASSERT_EQ(entry.truncate_size, (uint32_t)0); + ASSERT_EQ(entry.samplepacketId, SAI_NULL_OBJECT_ID); + } + + TEST_F(MirrorOrchTest, CreateEntryTruncateWithoutSampleRateDefaultsRateToOne) + { + // Truncation implies sampled mirroring. When truncate_size is configured + // without an explicit sample_rate, createEntry must default the rate to 1 + // (sample every packet). + ASSERT_NE(gSwitchOrch, nullptr); + ASSERT_NE(gMirrorOrch, nullptr); + + gSwitchOrch->m_portIngressSampleMirrorSupported = true; + gSwitchOrch->m_portEgressSampleMirrorSupported = false; + gSwitchOrch->m_samplepacketTruncationSupported = true; + + vector data; + data.emplace_back("type", "ERSPAN"); + data.emplace_back("src_ip", "10.0.0.1"); + data.emplace_back("dst_ip", "10.0.0.2"); + data.emplace_back("gre_type", "0x8949"); + data.emplace_back("dscp", "8"); + data.emplace_back("ttl", "64"); + data.emplace_back("queue", "0"); + data.emplace_back("direction", "RX"); + data.emplace_back("truncate_size", "128"); + + auto status = gMirrorOrch->createEntry("truncate_default_session", data); + ASSERT_EQ(status, task_process_status::task_success); + + ASSERT_TRUE(gMirrorOrch->sessionExists("truncate_default_session")); + auto& session = gMirrorOrch->m_syncdMirrors.find("truncate_default_session")->second; + ASSERT_EQ(session.sample_rate, (uint32_t)1); + ASSERT_EQ(session.truncate_size, (uint32_t)128); + + // Cleanup + gMirrorOrch->m_syncdMirrors.erase("truncate_default_session"); + } + + TEST_F(MirrorOrchTest, CreateEntryRejectsTruncateWithoutDirection) + { + // After defaulting the sample rate to 1, the session is sampled and must + // still carry an explicit direction. Truncation configured without a + // direction is therefore rejected. + ASSERT_NE(gSwitchOrch, nullptr); + ASSERT_NE(gMirrorOrch, nullptr); + + gSwitchOrch->m_portIngressSampleMirrorSupported = true; + gSwitchOrch->m_portEgressSampleMirrorSupported = true; + gSwitchOrch->m_samplepacketTruncationSupported = true; + + vector data; + data.emplace_back("type", "ERSPAN"); + data.emplace_back("src_ip", "10.0.0.1"); + data.emplace_back("dst_ip", "10.0.0.2"); + data.emplace_back("dscp", "8"); + data.emplace_back("ttl", "64"); + data.emplace_back("truncate_size", "128"); + + auto status = gMirrorOrch->createEntry("truncate_nodir_session", data); + ASSERT_EQ(status, task_process_status::task_invalid_entry); + ASSERT_FALSE(gMirrorOrch->sessionExists("truncate_nodir_session")); + } + + TEST_F(MirrorOrchTest, CreateEntryRejectsTruncateDefaultedWhenTruncationUnsupported) + { + // Defaulting the sample rate to 1 must not bypass the truncation + // capability gate: a truncate-only session with a valid direction and + // sample-mirror capability is still rejected when the platform does not + // advertise SAMPLEPACKET_TRUNCATION support. + ASSERT_NE(gSwitchOrch, nullptr); + ASSERT_NE(gMirrorOrch, nullptr); + + gSwitchOrch->m_portIngressSampleMirrorSupported = true; + gSwitchOrch->m_portEgressSampleMirrorSupported = false; + gSwitchOrch->m_samplepacketTruncationSupported = false; + + vector data; + data.emplace_back("type", "ERSPAN"); + data.emplace_back("src_ip", "10.0.0.1"); + data.emplace_back("dst_ip", "10.0.0.2"); + data.emplace_back("dscp", "8"); + data.emplace_back("ttl", "64"); + data.emplace_back("direction", "RX"); + data.emplace_back("truncate_size", "128"); + + auto status = gMirrorOrch->createEntry("truncate_nocap_session", data); + ASSERT_EQ(status, task_process_status::task_invalid_entry); + ASSERT_FALSE(gMirrorOrch->sessionExists("truncate_nocap_session")); + } + + TEST_F(MirrorOrchTest, CreateEntryRejectsNonNumericSampleRate) + { + // A non-numeric sample_rate cannot be parsed by to_uint and must be + // rejected. + ASSERT_NE(gMirrorOrch, nullptr); + + vector data; + data.emplace_back("type", "ERSPAN"); + data.emplace_back("src_ip", "10.0.0.1"); + data.emplace_back("dst_ip", "10.0.0.2"); + data.emplace_back("dscp", "8"); + data.emplace_back("ttl", "64"); + data.emplace_back("direction", "RX"); + data.emplace_back("sample_rate", "not_a_number"); + + auto status = gMirrorOrch->createEntry("bad_rate_session", data); + ASSERT_EQ(status, task_process_status::task_invalid_entry); + ASSERT_FALSE(gMirrorOrch->sessionExists("bad_rate_session")); + } + + TEST_F(MirrorOrchTest, CreateEntryRejectsNonNumericTruncateSize) + { + // A non-numeric truncate_size cannot be parsed by to_uint and must be + // rejected. + ASSERT_NE(gMirrorOrch, nullptr); + + vector data; + data.emplace_back("type", "ERSPAN"); + data.emplace_back("src_ip", "10.0.0.1"); + data.emplace_back("dst_ip", "10.0.0.2"); + data.emplace_back("dscp", "8"); + data.emplace_back("ttl", "64"); + data.emplace_back("direction", "RX"); + data.emplace_back("truncate_size", "not_a_number"); + + auto status = gMirrorOrch->createEntry("bad_truncate_session", data); + ASSERT_EQ(status, task_process_status::task_invalid_entry); + ASSERT_FALSE(gMirrorOrch->sessionExists("bad_truncate_session")); + } + + TEST_F(MirrorOrchTest, CreateEntryWithSampleRate) + { + // Verify createEntry correctly parses sample_rate and truncate_size. + // RX needs only the ingress sample-mirror capability. + ASSERT_NE(gSwitchOrch, nullptr); + ASSERT_NE(gMirrorOrch, nullptr); + + gSwitchOrch->m_portIngressSampleMirrorSupported = true; + gSwitchOrch->m_portEgressSampleMirrorSupported = false; + gSwitchOrch->m_samplepacketTruncationSupported = true; + + vector data; + data.emplace_back("type", "ERSPAN"); + data.emplace_back("src_ip", "10.0.0.1"); + data.emplace_back("dst_ip", "10.0.0.2"); + data.emplace_back("gre_type", "0x8949"); + data.emplace_back("dscp", "8"); + data.emplace_back("ttl", "64"); + data.emplace_back("queue", "0"); + data.emplace_back("direction", "RX"); + data.emplace_back("sample_rate", "50000"); + data.emplace_back("truncate_size", "128"); + + auto status = gMirrorOrch->createEntry("sampled_session", data); + ASSERT_EQ(status, task_process_status::task_success); + + // Verify session was created with correct values + ASSERT_TRUE(gMirrorOrch->sessionExists("sampled_session")); + auto& session = gMirrorOrch->m_syncdMirrors.find("sampled_session")->second; + ASSERT_EQ(session.sample_rate, (uint32_t)50000); + ASSERT_EQ(session.truncate_size, (uint32_t)128); + ASSERT_EQ(session.type, "ERSPAN"); + + // Cleanup + gMirrorOrch->m_syncdMirrors.erase("sampled_session"); + } + + TEST_F(MirrorOrchTest, CreateEntryRejectsTxWhenEgressUnsupported) + { + // A TX sampled session needs the EGRESS sample-mirror capability; + // reject when the platform only advertises the INGRESS capability. + ASSERT_NE(gSwitchOrch, nullptr); + ASSERT_NE(gMirrorOrch, nullptr); + + gSwitchOrch->m_portIngressSampleMirrorSupported = true; + gSwitchOrch->m_portEgressSampleMirrorSupported = false; + + vector data; + data.emplace_back("type", "ERSPAN"); + data.emplace_back("src_ip", "10.0.0.1"); + data.emplace_back("dst_ip", "10.0.0.2"); + data.emplace_back("dscp", "8"); + data.emplace_back("ttl", "64"); + data.emplace_back("direction", "TX"); + data.emplace_back("sample_rate", "50000"); + + auto status = gMirrorOrch->createEntry("tx_no_egress_session", data); + ASSERT_EQ(status, task_process_status::task_invalid_entry); + ASSERT_FALSE(gMirrorOrch->sessionExists("tx_no_egress_session")); + } + + TEST_F(MirrorOrchTest, CreateEntryAcceptsTxWhenEgressSupported) + { + // A TX sampled session is accepted when the EGRESS sample-mirror + // capability is advertised. + ASSERT_NE(gSwitchOrch, nullptr); + ASSERT_NE(gMirrorOrch, nullptr); + + gSwitchOrch->m_portIngressSampleMirrorSupported = false; + gSwitchOrch->m_portEgressSampleMirrorSupported = true; + gSwitchOrch->m_samplepacketTruncationSupported = true; + + vector data; + data.emplace_back("type", "ERSPAN"); + data.emplace_back("src_ip", "10.0.0.1"); + data.emplace_back("dst_ip", "10.0.0.2"); + data.emplace_back("gre_type", "0x88be"); + data.emplace_back("dscp", "8"); + data.emplace_back("ttl", "64"); + data.emplace_back("queue", "0"); + data.emplace_back("direction", "TX"); + data.emplace_back("sample_rate", "50000"); + + auto status = gMirrorOrch->createEntry("tx_egress_session", data); + ASSERT_EQ(status, task_process_status::task_success); + ASSERT_TRUE(gMirrorOrch->sessionExists("tx_egress_session")); + gMirrorOrch->m_syncdMirrors.erase("tx_egress_session"); + } + + TEST_F(MirrorOrchTest, CreateEntryRejectsBothWhenIngressUnsupported) + { + // direction=BOTH binds both directions, so it requires BOTH the + // ingress and egress capabilities; reject when ingress is missing. + ASSERT_NE(gSwitchOrch, nullptr); + ASSERT_NE(gMirrorOrch, nullptr); + + gSwitchOrch->m_portIngressSampleMirrorSupported = false; + gSwitchOrch->m_portEgressSampleMirrorSupported = true; + + vector data; + data.emplace_back("type", "ERSPAN"); + data.emplace_back("src_ip", "10.0.0.1"); + data.emplace_back("dst_ip", "10.0.0.2"); + data.emplace_back("dscp", "8"); + data.emplace_back("ttl", "64"); + data.emplace_back("direction", "BOTH"); + data.emplace_back("sample_rate", "50000"); + + auto status = gMirrorOrch->createEntry("both_no_ingress_session", data); + ASSERT_EQ(status, task_process_status::task_invalid_entry); + ASSERT_FALSE(gMirrorOrch->sessionExists("both_no_ingress_session")); + } + + TEST_F(MirrorOrchTest, CreateEntryRejectsBothWhenEgressUnsupported) + { + // direction=BOTH requires both capabilities; ingress passes the first + // check, so this exercises the BOTH egress-capability branch. + ASSERT_NE(gSwitchOrch, nullptr); + ASSERT_NE(gMirrorOrch, nullptr); + + gSwitchOrch->m_portIngressSampleMirrorSupported = true; + gSwitchOrch->m_portEgressSampleMirrorSupported = false; + + vector data; + data.emplace_back("type", "ERSPAN"); + data.emplace_back("src_ip", "10.0.0.1"); + data.emplace_back("dst_ip", "10.0.0.2"); + data.emplace_back("dscp", "8"); + data.emplace_back("ttl", "64"); + data.emplace_back("direction", "BOTH"); + data.emplace_back("sample_rate", "50000"); + + auto status = gMirrorOrch->createEntry("both_no_egress_session", data); + ASSERT_EQ(status, task_process_status::task_invalid_entry); + ASSERT_FALSE(gMirrorOrch->sessionExists("both_no_egress_session")); + } + + TEST_F(MirrorOrchTest, CreateEntryAcceptsBothWhenBothSupported) + { + // direction=BOTH is accepted when both the ingress and egress + // sample-mirror capabilities are advertised. + ASSERT_NE(gSwitchOrch, nullptr); + ASSERT_NE(gMirrorOrch, nullptr); + + gSwitchOrch->m_portIngressSampleMirrorSupported = true; + gSwitchOrch->m_portEgressSampleMirrorSupported = true; + + vector data; + data.emplace_back("type", "ERSPAN"); + data.emplace_back("src_ip", "10.0.0.1"); + data.emplace_back("dst_ip", "10.0.0.2"); + data.emplace_back("gre_type", "0x88be"); + data.emplace_back("dscp", "8"); + data.emplace_back("ttl", "64"); + data.emplace_back("queue", "0"); + data.emplace_back("direction", "BOTH"); + data.emplace_back("sample_rate", "50000"); + + auto status = gMirrorOrch->createEntry("both_supported_session", data); + ASSERT_EQ(status, task_process_status::task_success); + ASSERT_TRUE(gMirrorOrch->sessionExists("both_supported_session")); + gMirrorOrch->m_syncdMirrors.erase("both_supported_session"); + } + + TEST_F(MirrorOrchTest, CreateEntryRejectsSampledEmptyDirection) + { + // A sampled session with no direction field is rejected outright. + ASSERT_NE(gSwitchOrch, nullptr); + ASSERT_NE(gMirrorOrch, nullptr); + + gSwitchOrch->m_portIngressSampleMirrorSupported = true; + gSwitchOrch->m_portEgressSampleMirrorSupported = true; + + vector data; + data.emplace_back("type", "ERSPAN"); + data.emplace_back("src_ip", "10.0.0.1"); + data.emplace_back("dst_ip", "10.0.0.2"); + data.emplace_back("dscp", "8"); + data.emplace_back("ttl", "64"); + data.emplace_back("sample_rate", "50000"); + + auto status = gMirrorOrch->createEntry("empty_dir_session", data); + ASSERT_EQ(status, task_process_status::task_invalid_entry); + ASSERT_FALSE(gMirrorOrch->sessionExists("empty_dir_session")); + } + + TEST_F(MirrorOrchTest, CreateEntryDuplicateRejected) + { + // A SET on an already-existing session is rejected as a duplicate; + ASSERT_NE(gMirrorOrch, nullptr); + + vector data; + data.emplace_back("type", "ERSPAN"); + data.emplace_back("src_ip", "10.0.0.1"); + data.emplace_back("dst_ip", "10.0.0.2"); + data.emplace_back("gre_type", "0x8949"); + data.emplace_back("dscp", "8"); + data.emplace_back("ttl", "64"); + data.emplace_back("queue", "0"); + data.emplace_back("direction", "RX"); + data.emplace_back("sample_rate", "50000"); + + auto status = gMirrorOrch->createEntry("dup_session", data); + ASSERT_EQ(status, task_process_status::task_success); + + // Re-applying the same key must be rejected, not updated. + status = gMirrorOrch->createEntry("dup_session", data); + ASSERT_EQ(status, task_process_status::task_duplicated); + + gMirrorOrch->m_syncdMirrors.erase("dup_session"); + } + + TEST_F(MirrorOrchTest, SampledPathPortBinding) + { + // Verify setUnsetPortMirror enters sampled path when sample_rate > 0 + ASSERT_NE(gSwitchOrch, nullptr); + ASSERT_NE(gMirrorOrch, nullptr); + + gSwitchOrch->m_portIngressMirrorSupported = true; + gSwitchOrch->m_portIngressSampleMirrorSupported = true; + + Port dummyPort; + dummyPort.m_port_id = SAI_NULL_OBJECT_ID; + + auto ret = gMirrorOrch->setUnsetPortMirror(dummyPort, /*ingress*/ true, /*set*/ true, + /*sessionId*/ SAI_NULL_OBJECT_ID, + /*samplepacketId*/ (sai_object_id_t)0x1234, + /*sample_rate*/ 50000); + // SAI mock returns failure on null port OID - expected in mock environment + ASSERT_FALSE(ret); + } + + TEST_F(MirrorOrchTest, CreateSamplePacketWithTruncation) + { + // Verify createSamplePacket sets truncation attributes when supported + ASSERT_NE(gSwitchOrch, nullptr); + ASSERT_NE(gMirrorOrch, nullptr); + + gSwitchOrch->m_portIngressSampleMirrorSupported = true; + gSwitchOrch->m_samplepacketTruncationSupported = true; + + MirrorEntry entry(""); + entry.sample_rate = 50000; + entry.truncate_size = 128; + + gMirrorOrch->createSamplePacket("truncate_session", entry); + // SAI mock may succeed or fail, but truncation path should be entered + // truncate_size should remain 128 (not cleared) since truncation is supported + ASSERT_EQ(entry.truncate_size, (uint32_t)128); + } + + class MirrorOrchPortTest : public MirrorOrchTest + { + void ApplyInitialConfigs() override + { + Table port_table = Table(m_app_db.get(), APP_PORT_TABLE_NAME); + auto ports = ut_helper::getInitialSaiPorts(); + for (const auto &it : ports) + { + port_table.set(it.first, it.second); + } + port_table.set("PortConfigDone", { { "count", to_string(ports.size()) } }); + port_table.set("PortInitDone", { {} }); + + gPortsOrch->addExistingData(&port_table); + static_cast(gPortsOrch)->doTask(); + } + }; + + TEST_F(MirrorOrchTest, SetSessionStateHdelsClearedSampleRate) + { + // When sample_rate becomes 0, setSessionState must HDEL the field + // from STATE_DB so consumers do not see a stale rate. + ASSERT_NE(gMirrorOrch, nullptr); + + string key = "hdel_sample_rate_session"; + swss::Table stateTable(m_state_db.get(), STATE_MIRROR_SESSION_TABLE_NAME); + stateTable.hset(key, "sample_rate", "50000"); + + string seeded; + ASSERT_TRUE(stateTable.hget(key, "sample_rate", seeded)); + ASSERT_EQ(seeded, "50000"); + + MirrorEntry session(""); + session.type = "ERSPAN"; + session.sample_rate = 0; + session.truncate_size = 0; + + gMirrorOrch->setSessionState(key, session, "sample_rate"); + + string after; + ASSERT_FALSE(stateTable.hget(key, "sample_rate", after)); + + stateTable.del(key); + } + + TEST_F(MirrorOrchTest, SetSessionStateHdelsClearedTruncateSize) + { + // When truncate_size becomes 0, setSessionState must HDEL the field + // from STATE_DB so consumers do not see a stale size. + ASSERT_NE(gMirrorOrch, nullptr); + + string key = "hdel_truncate_session"; + swss::Table stateTable(m_state_db.get(), STATE_MIRROR_SESSION_TABLE_NAME); + stateTable.hset(key, "truncate_size", "128"); + + string seeded; + ASSERT_TRUE(stateTable.hget(key, "truncate_size", seeded)); + ASSERT_EQ(seeded, "128"); + + MirrorEntry session(""); + session.type = "ERSPAN"; + session.sample_rate = 0; + session.truncate_size = 0; + + gMirrorOrch->setSessionState(key, session, "truncate_size"); + + string after; + ASSERT_FALSE(stateTable.hget(key, "truncate_size", after)); + + stateTable.del(key); + } + + TEST_F(MirrorOrchTest, RemoveSamplePacketSuccess) + { + // Cover removeSamplePacket success path on a real (non-NULL) OID: + // remove_samplepacket call, success branch, and OID reset. + ASSERT_NE(gMirrorOrch, nullptr); + + MirrorEntry entry(""); + entry.type = "ERSPAN"; + entry.sample_rate = 50000; + entry.truncate_size = 0; + + // createSamplePacket yields a real samplepacket OID from the VS switch. + ASSERT_TRUE(gMirrorOrch->createSamplePacket("rm_sp_session", entry)); + ASSERT_NE(entry.samplepacketId, SAI_NULL_OBJECT_ID); + + // Exercise the real removal branch (non-NULL OID). + ASSERT_TRUE(gMirrorOrch->removeSamplePacket("rm_sp_session", entry)); + ASSERT_EQ(entry.samplepacketId, SAI_NULL_OBJECT_ID); + } + + // Build a real ERSPAN mirror-session OID (GRE protocol type 0x8949) bound to a real monitor port. + static sai_object_id_t createErspanSessionOid(sai_object_id_t monitor_port_id) + { + std::vector attrs; + sai_attribute_t attr; + + attr.id = SAI_MIRROR_SESSION_ATTR_MONITOR_PORT; + attr.value.oid = monitor_port_id; + attrs.push_back(attr); + + attr.id = SAI_MIRROR_SESSION_ATTR_TYPE; + attr.value.s32 = SAI_MIRROR_SESSION_TYPE_ENHANCED_REMOTE; + attrs.push_back(attr); + + attr.id = SAI_MIRROR_SESSION_ATTR_ERSPAN_ENCAPSULATION_TYPE; + attr.value.s32 = SAI_ERSPAN_ENCAPSULATION_TYPE_MIRROR_L3_GRE_TUNNEL; + attrs.push_back(attr); + + attr.id = SAI_MIRROR_SESSION_ATTR_IPHDR_VERSION; + attr.value.u8 = 4; + attrs.push_back(attr); + + attr.id = SAI_MIRROR_SESSION_ATTR_TOS; + attr.value.u16 = 0; + attrs.push_back(attr); + + attr.id = SAI_MIRROR_SESSION_ATTR_TTL; + attr.value.u8 = 64; + attrs.push_back(attr); + + attr.id = SAI_MIRROR_SESSION_ATTR_SRC_IP_ADDRESS; + attr.value.ipaddr.addr_family = SAI_IP_ADDR_FAMILY_IPV4; + attr.value.ipaddr.addr.ip4 = htonl(0x0a000001); // 10.0.0.1 + attrs.push_back(attr); + + attr.id = SAI_MIRROR_SESSION_ATTR_DST_IP_ADDRESS; + attr.value.ipaddr.addr_family = SAI_IP_ADDR_FAMILY_IPV4; + attr.value.ipaddr.addr.ip4 = htonl(0x0a000002); // 10.0.0.2 + attrs.push_back(attr); + + sai_mac_t src_mac = {0x00, 0x11, 0x22, 0x33, 0x44, 0x55}; + attr.id = SAI_MIRROR_SESSION_ATTR_SRC_MAC_ADDRESS; + memcpy(attr.value.mac, src_mac, sizeof(sai_mac_t)); + attrs.push_back(attr); + + sai_mac_t dst_mac = {0x00, 0xaa, 0xbb, 0xcc, 0xdd, 0xee}; + attr.id = SAI_MIRROR_SESSION_ATTR_DST_MAC_ADDRESS; + memcpy(attr.value.mac, dst_mac, sizeof(sai_mac_t)); + attrs.push_back(attr); + + // Sampled-ERSPAN GRE protocol type per the test plan. + attr.id = SAI_MIRROR_SESSION_ATTR_GRE_PROTOCOL_TYPE; + attr.value.u16 = 0x8949; + attrs.push_back(attr); + + sai_object_id_t oid = SAI_NULL_OBJECT_ID; + EXPECT_NE(sai_mirror_api, nullptr); + if (sai_mirror_api == nullptr) return SAI_NULL_OBJECT_ID; + sai_status_t status = sai_mirror_api->create_mirror_session( + &oid, gSwitchId, (uint32_t)attrs.size(), attrs.data()); + EXPECT_EQ(status, SAI_STATUS_SUCCESS); + return oid; + } + + // Read back the emulated SAMPLEPACKET_ENABLE attr for a port (via the SAI + // wrap) so a test can assert which direction was actually programmed. + static sai_object_id_t readSamplePacketEnable(sai_object_id_t port_id, sai_attr_id_t attr_id) + { + sai_attribute_t attr; + attr.id = attr_id; + attr.value.oid = SAI_NULL_OBJECT_ID; + EXPECT_EQ(sai_port_api->get_port_attribute(port_id, 1, &attr), SAI_STATUS_SUCCESS); + return attr.value.oid; + } + + TEST_F(MirrorOrchPortTest, SampledMirrorPhyPortSetClear) + { + mirror_sample_port_wrap_ut::PortSampleSaiGuard saiPortSampleGuard; + // Covers setUnsetSampledMirrorOnPhyPort path for both set and clear + // (SAMPLEPACKET_ENABLE + SAMPLE_MIRROR_SESSION bind, then reverse-order unbind). + ASSERT_NE(gMirrorOrch, nullptr); + ASSERT_NE(gPortsOrch, nullptr); + + Port port; + ASSERT_TRUE(gPortsOrch->getPort("Ethernet0", port)); + + sai_object_id_t sessionOid = createErspanSessionOid(port.m_port_id); + ASSERT_NE(sessionOid, SAI_NULL_OBJECT_ID); + + MirrorEntry entry(""); + entry.sample_rate = 50000; + ASSERT_TRUE(gMirrorOrch->createSamplePacket("phy_set_clear", entry)); + ASSERT_NE(entry.samplepacketId, SAI_NULL_OBJECT_ID); + + // SET: SAMPLEPACKET_ENABLE first, then SAMPLE_MIRROR_SESSION. + ASSERT_TRUE(gMirrorOrch->setUnsetSampledMirrorOnPhyPort( + port.m_port_id, port.m_alias, /*set*/ true, MirrorBindDirection::Ingress, sessionOid, entry.samplepacketId)); + + // CLEAR: SAMPLE_MIRROR_SESSION first, then SAMPLEPACKET_ENABLE. + ASSERT_TRUE(gMirrorOrch->setUnsetSampledMirrorOnPhyPort( + port.m_port_id, port.m_alias, /*set*/ false, MirrorBindDirection::Ingress, sessionOid, entry.samplepacketId)); + + sai_samplepacket_api->remove_samplepacket(entry.samplepacketId); + sai_mirror_api->remove_mirror_session(sessionOid); + } + + TEST_F(MirrorOrchPortTest, SampledMirrorPhyPortConflict) + { + mirror_sample_port_wrap_ut::PortSampleSaiGuard saiPortSampleGuard; + // Covers the samplepacket-conflict guard: a port already bound to a + // different INGRESS_SAMPLEPACKET_ENABLE OID must reject the new bind. + ASSERT_NE(gMirrorOrch, nullptr); + ASSERT_NE(gPortsOrch, nullptr); + + Port port; + ASSERT_TRUE(gPortsOrch->getPort("Ethernet0", port)); + + sai_object_id_t sessionOid = createErspanSessionOid(port.m_port_id); + ASSERT_NE(sessionOid, SAI_NULL_OBJECT_ID); + + MirrorEntry existing(""); + existing.sample_rate = 50000; + ASSERT_TRUE(gMirrorOrch->createSamplePacket("conflict_existing", existing)); + MirrorEntry incoming(""); + incoming.sample_rate = 60000; + ASSERT_TRUE(gMirrorOrch->createSamplePacket("conflict_incoming", incoming)); + ASSERT_NE(existing.samplepacketId, incoming.samplepacketId); + + // Pre-bind a DIFFERENT samplepacket OID directly onto the port. + sai_attribute_t pre; + pre.id = SAI_PORT_ATTR_INGRESS_SAMPLEPACKET_ENABLE; + pre.value.oid = existing.samplepacketId; + ASSERT_EQ(sai_port_api->set_port_attribute(port.m_port_id, &pre), SAI_STATUS_SUCCESS); + + // Binding a different samplepacket must fail the conflict check. + ASSERT_FALSE(gMirrorOrch->setUnsetSampledMirrorOnPhyPort( + port.m_port_id, port.m_alias, /*set*/ true, MirrorBindDirection::Ingress, sessionOid, incoming.samplepacketId)); + + // Cleanup: clear binding and remove real objects. + pre.value.oid = SAI_NULL_OBJECT_ID; + sai_port_api->set_port_attribute(port.m_port_id, &pre); + sai_samplepacket_api->remove_samplepacket(existing.samplepacketId); + sai_samplepacket_api->remove_samplepacket(incoming.samplepacketId); + sai_mirror_api->remove_mirror_session(sessionOid); + } + + TEST_F(MirrorOrchPortTest, ConfigurePortMirrorSessionSampledPhy) + { + mirror_sample_port_wrap_ut::PortSampleSaiGuard saiPortSampleGuard; + // Covers configurePortMirrorSession RX dispatch into the sampled + // PHY-direct branch of setUnsetPortMirror. + ASSERT_NE(gMirrorOrch, nullptr); + ASSERT_NE(gPortsOrch, nullptr); + ASSERT_NE(gSwitchOrch, nullptr); + + gSwitchOrch->m_portIngressMirrorSupported = true; + gSwitchOrch->m_portIngressSampleMirrorSupported = true; + + Port port; + ASSERT_TRUE(gPortsOrch->getPort("Ethernet0", port)); + + sai_object_id_t sessionOid = createErspanSessionOid(port.m_port_id); + ASSERT_NE(sessionOid, SAI_NULL_OBJECT_ID); + + MirrorEntry entry(""); + entry.sample_rate = 50000; + ASSERT_TRUE(gMirrorOrch->createSamplePacket("cfg_phy", entry)); + ASSERT_NE(entry.samplepacketId, SAI_NULL_OBJECT_ID); + entry.sessionId = sessionOid; + entry.src_port = "Ethernet0"; + entry.direction = MIRROR_RX_DIRECTION; + + ASSERT_TRUE(gMirrorOrch->configurePortMirrorSession("cfg_phy", entry, /*set*/ true)); + ASSERT_TRUE(gMirrorOrch->configurePortMirrorSession("cfg_phy", entry, /*set*/ false)); + + sai_samplepacket_api->remove_samplepacket(entry.samplepacketId); + sai_mirror_api->remove_mirror_session(sessionOid); + } + + TEST_F(MirrorOrchPortTest, SampledMirrorEgressPhyPortSetClear) + { + mirror_sample_port_wrap_ut::PortSampleSaiGuard saiPortSampleGuard; + // Covers setUnsetSampledMirrorOnPhyPort with ingress=false: it must + // program the EGRESS_SAMPLEPACKET_ENABLE + EGRESS_SAMPLE_MIRROR_SESSION + // attributes (bind, then reverse-order unbind). + ASSERT_NE(gMirrorOrch, nullptr); + ASSERT_NE(gPortsOrch, nullptr); + + Port port; + ASSERT_TRUE(gPortsOrch->getPort("Ethernet0", port)); + + sai_object_id_t sessionOid = createErspanSessionOid(port.m_port_id); + ASSERT_NE(sessionOid, SAI_NULL_OBJECT_ID); + + MirrorEntry entry(""); + entry.sample_rate = 50000; + ASSERT_TRUE(gMirrorOrch->createSamplePacket("egress_set_clear", entry)); + ASSERT_NE(entry.samplepacketId, SAI_NULL_OBJECT_ID); + + ASSERT_TRUE(gMirrorOrch->setUnsetSampledMirrorOnPhyPort( + port.m_port_id, port.m_alias, /*set*/ true, MirrorBindDirection::Egress, sessionOid, entry.samplepacketId)); + + // The egress bind must program EGRESS_SAMPLEPACKET_ENABLE and must NOT + // touch the ingress attr (otherwise the direction enum was misrouted). + ASSERT_EQ(readSamplePacketEnable(port.m_port_id, SAI_PORT_ATTR_EGRESS_SAMPLEPACKET_ENABLE), entry.samplepacketId); + ASSERT_EQ(readSamplePacketEnable(port.m_port_id, SAI_PORT_ATTR_INGRESS_SAMPLEPACKET_ENABLE), SAI_NULL_OBJECT_ID); + + ASSERT_TRUE(gMirrorOrch->setUnsetSampledMirrorOnPhyPort( + port.m_port_id, port.m_alias, /*set*/ false, MirrorBindDirection::Egress, sessionOid, entry.samplepacketId)); + + // The clear must remove the egress binding. + ASSERT_EQ(readSamplePacketEnable(port.m_port_id, SAI_PORT_ATTR_EGRESS_SAMPLEPACKET_ENABLE), SAI_NULL_OBJECT_ID); + + sai_samplepacket_api->remove_samplepacket(entry.samplepacketId); + sai_mirror_api->remove_mirror_session(sessionOid); + } + + TEST_F(MirrorOrchPortTest, ConfigurePortMirrorSessionSampledEgressPhy) + { + mirror_sample_port_wrap_ut::PortSampleSaiGuard saiPortSampleGuard; + // Covers configurePortMirrorSession TX dispatch into the sampled + // egress PHY-direct branch of setUnsetPortMirror. + ASSERT_NE(gMirrorOrch, nullptr); + ASSERT_NE(gPortsOrch, nullptr); + ASSERT_NE(gSwitchOrch, nullptr); + + gSwitchOrch->m_portEgressMirrorSupported = true; + gSwitchOrch->m_portEgressSampleMirrorSupported = true; + + Port port; + ASSERT_TRUE(gPortsOrch->getPort("Ethernet0", port)); + + sai_object_id_t sessionOid = createErspanSessionOid(port.m_port_id); + ASSERT_NE(sessionOid, SAI_NULL_OBJECT_ID); + + MirrorEntry entry(""); + entry.sample_rate = 50000; + ASSERT_TRUE(gMirrorOrch->createSamplePacket("cfg_egress_phy", entry)); + ASSERT_NE(entry.samplepacketId, SAI_NULL_OBJECT_ID); + entry.sessionId = sessionOid; + entry.src_port = "Ethernet0"; + entry.direction = MIRROR_TX_DIRECTION; + + ASSERT_TRUE(gMirrorOrch->configurePortMirrorSession("cfg_egress_phy", entry, /*set*/ true)); + + // TX dispatch must bind only the egress attr. + ASSERT_EQ(readSamplePacketEnable(port.m_port_id, SAI_PORT_ATTR_EGRESS_SAMPLEPACKET_ENABLE), entry.samplepacketId); + ASSERT_EQ(readSamplePacketEnable(port.m_port_id, SAI_PORT_ATTR_INGRESS_SAMPLEPACKET_ENABLE), SAI_NULL_OBJECT_ID); + + ASSERT_TRUE(gMirrorOrch->configurePortMirrorSession("cfg_egress_phy", entry, /*set*/ false)); + + ASSERT_EQ(readSamplePacketEnable(port.m_port_id, SAI_PORT_ATTR_EGRESS_SAMPLEPACKET_ENABLE), SAI_NULL_OBJECT_ID); + + sai_samplepacket_api->remove_samplepacket(entry.samplepacketId); + sai_mirror_api->remove_mirror_session(sessionOid); + } + + TEST_F(MirrorOrchPortTest, ConfigurePortMirrorSessionSampledBothPhy) + { + mirror_sample_port_wrap_ut::PortSampleSaiGuard saiPortSampleGuard; + // Covers configurePortMirrorSession BOTH dispatch: the RX and TX + // branches are independent, so a BOTH session must program the + // ingress AND egress sampled-mirror attrs on the same port. + ASSERT_NE(gMirrorOrch, nullptr); + ASSERT_NE(gPortsOrch, nullptr); + ASSERT_NE(gSwitchOrch, nullptr); + + gSwitchOrch->m_portIngressMirrorSupported = true; + gSwitchOrch->m_portEgressMirrorSupported = true; + + Port port; + ASSERT_TRUE(gPortsOrch->getPort("Ethernet0", port)); + + sai_object_id_t sessionOid = createErspanSessionOid(port.m_port_id); + ASSERT_NE(sessionOid, SAI_NULL_OBJECT_ID); + + MirrorEntry entry(""); + entry.sample_rate = 50000; + ASSERT_TRUE(gMirrorOrch->createSamplePacket("cfg_both_phy", entry)); + ASSERT_NE(entry.samplepacketId, SAI_NULL_OBJECT_ID); + entry.sessionId = sessionOid; + entry.src_port = "Ethernet0"; + entry.direction = MIRROR_BOTH_DIRECTION; + + ASSERT_TRUE(gMirrorOrch->configurePortMirrorSession("cfg_both_phy", entry, /*set*/ true)); + + // BOTH must bind both directions to the same samplepacket OID. + ASSERT_EQ(readSamplePacketEnable(port.m_port_id, SAI_PORT_ATTR_INGRESS_SAMPLEPACKET_ENABLE), entry.samplepacketId); + ASSERT_EQ(readSamplePacketEnable(port.m_port_id, SAI_PORT_ATTR_EGRESS_SAMPLEPACKET_ENABLE), entry.samplepacketId); + + ASSERT_TRUE(gMirrorOrch->configurePortMirrorSession("cfg_both_phy", entry, /*set*/ false)); + + // Clear must remove both bindings. + ASSERT_EQ(readSamplePacketEnable(port.m_port_id, SAI_PORT_ATTR_INGRESS_SAMPLEPACKET_ENABLE), SAI_NULL_OBJECT_ID); + ASSERT_EQ(readSamplePacketEnable(port.m_port_id, SAI_PORT_ATTR_EGRESS_SAMPLEPACKET_ENABLE), SAI_NULL_OBJECT_ID); + + sai_samplepacket_api->remove_samplepacket(entry.samplepacketId); + sai_mirror_api->remove_mirror_session(sessionOid); + } + + TEST_F(MirrorOrchPortTest, SampledMirrorSetMirrorSessionFailsRollsBack) + { + mirror_sample_port_wrap_ut::PortSampleSaiGuard saiPortSampleGuard; + // Covers the set path when SAMPLE_MIRROR_SESSION fails after + // SAMPLEPACKET_ENABLE succeeded: the bind must report failure and roll + // back the already-programmed SAMPLEPACKET_ENABLE to NULL. + ASSERT_NE(gMirrorOrch, nullptr); + ASSERT_NE(gPortsOrch, nullptr); + + Port port; + ASSERT_TRUE(gPortsOrch->getPort("Ethernet0", port)); + + sai_object_id_t sessionOid = createErspanSessionOid(port.m_port_id); + ASSERT_NE(sessionOid, SAI_NULL_OBJECT_ID); + + MirrorEntry entry(""); + entry.sample_rate = 50000; + ASSERT_TRUE(gMirrorOrch->createSamplePacket("set_fail_rollback", entry)); + ASSERT_NE(entry.samplepacketId, SAI_NULL_OBJECT_ID); + + mirror_sample_port_wrap_ut::g_fail_mirror_session_set = true; + ASSERT_FALSE(gMirrorOrch->setUnsetSampledMirrorOnPhyPort( + port.m_port_id, port.m_alias, /*set*/ true, MirrorBindDirection::Ingress, sessionOid, entry.samplepacketId)); + + // Rollback must have cleared the SAMPLEPACKET_ENABLE binding. + ASSERT_EQ(readSamplePacketEnable(port.m_port_id, SAI_PORT_ATTR_INGRESS_SAMPLEPACKET_ENABLE), SAI_NULL_OBJECT_ID); + + sai_samplepacket_api->remove_samplepacket(entry.samplepacketId); + sai_mirror_api->remove_mirror_session(sessionOid); + } + + TEST_F(MirrorOrchPortTest, SampledMirrorClearMirrorSessionFails) + { + mirror_sample_port_wrap_ut::PortSampleSaiGuard saiPortSampleGuard; + // Covers the clear path when the SAMPLE_MIRROR_SESSION fails: + // the unbind must report failure. + ASSERT_NE(gMirrorOrch, nullptr); + ASSERT_NE(gPortsOrch, nullptr); + + Port port; + ASSERT_TRUE(gPortsOrch->getPort("Ethernet0", port)); + + sai_object_id_t sessionOid = createErspanSessionOid(port.m_port_id); + ASSERT_NE(sessionOid, SAI_NULL_OBJECT_ID); + + MirrorEntry entry(""); + entry.sample_rate = 50000; + ASSERT_TRUE(gMirrorOrch->createSamplePacket("clear_mirror_fail", entry)); + ASSERT_NE(entry.samplepacketId, SAI_NULL_OBJECT_ID); + + // Bind successfully first so the clear operates on real state. + ASSERT_TRUE(gMirrorOrch->setUnsetSampledMirrorOnPhyPort( + port.m_port_id, port.m_alias, /*set*/ true, MirrorBindDirection::Ingress, sessionOid, entry.samplepacketId)); + ASSERT_EQ(readSamplePacketEnable(port.m_port_id, SAI_PORT_ATTR_INGRESS_SAMPLEPACKET_ENABLE), entry.samplepacketId); + + // The clear fails at SAMPLE_MIRROR_SESSION, so it must + // report failure before touching SAMPLEPACKET_ENABLE (still bound). + mirror_sample_port_wrap_ut::g_fail_mirror_session_set = true; + ASSERT_FALSE(gMirrorOrch->setUnsetSampledMirrorOnPhyPort( + port.m_port_id, port.m_alias, /*set*/ false, MirrorBindDirection::Ingress, sessionOid, entry.samplepacketId)); + ASSERT_EQ(readSamplePacketEnable(port.m_port_id, SAI_PORT_ATTR_INGRESS_SAMPLEPACKET_ENABLE), entry.samplepacketId); + + sai_samplepacket_api->remove_samplepacket(entry.samplepacketId); + sai_mirror_api->remove_mirror_session(sessionOid); + } + + TEST_F(MirrorOrchPortTest, SampledMirrorClearSamplePacketEnableFails) + { + mirror_sample_port_wrap_ut::PortSampleSaiGuard saiPortSampleGuard; + // Covers the clear path when the SAMPLEPACKET_ENABLE fails after + // the SAMPLE_MIRROR_SESSION clear succeeded: the + // unbind must report failure. + ASSERT_NE(gMirrorOrch, nullptr); + ASSERT_NE(gPortsOrch, nullptr); + + Port port; + ASSERT_TRUE(gPortsOrch->getPort("Ethernet0", port)); + + sai_object_id_t sessionOid = createErspanSessionOid(port.m_port_id); + ASSERT_NE(sessionOid, SAI_NULL_OBJECT_ID); + + MirrorEntry entry(""); + entry.sample_rate = 50000; + ASSERT_TRUE(gMirrorOrch->createSamplePacket("clear_enable_fail", entry)); + ASSERT_NE(entry.samplepacketId, SAI_NULL_OBJECT_ID); + + // Bind successfully first so the clear operates on real state. + ASSERT_TRUE(gMirrorOrch->setUnsetSampledMirrorOnPhyPort( + port.m_port_id, port.m_alias, /*set*/ true, MirrorBindDirection::Ingress, sessionOid, entry.samplepacketId)); + ASSERT_EQ(readSamplePacketEnable(port.m_port_id, SAI_PORT_ATTR_INGRESS_SAMPLEPACKET_ENABLE), entry.samplepacketId); + + mirror_sample_port_wrap_ut::g_fail_samplepacket_enable_set = true; + ASSERT_FALSE(gMirrorOrch->setUnsetSampledMirrorOnPhyPort( + port.m_port_id, port.m_alias, /*set*/ false, MirrorBindDirection::Ingress, sessionOid, entry.samplepacketId)); + ASSERT_EQ(readSamplePacketEnable(port.m_port_id, SAI_PORT_ATTR_INGRESS_SAMPLEPACKET_ENABLE), entry.samplepacketId); + + sai_samplepacket_api->remove_samplepacket(entry.samplepacketId); + sai_mirror_api->remove_mirror_session(sessionOid); + } + + TEST_F(MirrorOrchPortTest, ActivateSessionPortBindFailRollsBack) + { + mirror_sample_port_wrap_ut::PortSampleSaiGuard saiPortSampleGuard; + // Covers activateSession's rollback when configurePortMirrorSession + // fails: the samplepacket created during activation must be removed, + // the mirror session torn down, and activation must report failure. + ASSERT_NE(gMirrorOrch, nullptr); + ASSERT_NE(gPortsOrch, nullptr); + ASSERT_NE(gSwitchOrch, nullptr); + + gSwitchOrch->m_portIngressSampleMirrorSupported = true; + gSwitchOrch->m_portEgressSampleMirrorSupported = false; + + Port port; + ASSERT_TRUE(gPortsOrch->getPort("Ethernet0", port)); + + // Build a sampled ERSPAN session bound to a PHY src port via createEntry. + std::vector data; + data.emplace_back("type", "ERSPAN"); + data.emplace_back("src_ip", "10.0.0.1"); + data.emplace_back("dst_ip", "10.0.0.2"); + data.emplace_back("gre_type", "0x8949"); + data.emplace_back("dscp", "8"); + data.emplace_back("ttl", "64"); + data.emplace_back("queue", "0"); + data.emplace_back("direction", "RX"); + data.emplace_back("sample_rate", "50000"); + data.emplace_back("src_port", "Ethernet0"); + ASSERT_EQ(gMirrorOrch->createEntry("activate_rollback", data), task_process_status::task_success); + ASSERT_TRUE(gMirrorOrch->sessionExists("activate_rollback")); + + auto& session = gMirrorOrch->m_syncdMirrors.find("activate_rollback")->second; + ASSERT_FALSE(session.status); + + // Resolve the destination so activateSession can build the ERSPAN attrs + // and create the underlying mirror session. + session.neighborInfo.portId = port.m_port_id; + session.neighborInfo.mac = MacAddress("00:11:22:33:44:55"); + session.neighborInfo.port.m_type = Port::PHY; + + // Fail the SAMPLE_MIRROR_SESSION bind so configurePortMirrorSession + // fails inside activateSession and the rollback path runs. + mirror_sample_port_wrap_ut::g_fail_mirror_session_set = true; + ASSERT_FALSE(gMirrorOrch->activateSession("activate_rollback", session)); + + // The mirror session must have been created (so the failure happened at + // the port-bind step, not an early create_mirror_session exit), and the + // rollback must have removed the samplepacket and left it inactive. + ASSERT_NE(session.sessionId, SAI_NULL_OBJECT_ID); + ASSERT_EQ(session.samplepacketId, SAI_NULL_OBJECT_ID); + ASSERT_FALSE(session.status); + + gMirrorOrch->m_syncdMirrors.erase("activate_rollback"); + } + + TEST_F(MirrorOrchPortTest, ActivateSessionSamplePacketCreateFails) + { + mirror_sample_port_wrap_ut::PortSampleSaiGuard saiPortSampleGuard; + // Covers activateSession's samplepacket-create failure handling: after + // create_mirror_session succeeds, createSamplePacket fails, so the + // mirror session must be torn down and activation must report failure. + ASSERT_NE(gMirrorOrch, nullptr); + ASSERT_NE(gPortsOrch, nullptr); + ASSERT_NE(gSwitchOrch, nullptr); + + gSwitchOrch->m_portIngressSampleMirrorSupported = true; + gSwitchOrch->m_portEgressSampleMirrorSupported = false; + + Port port; + ASSERT_TRUE(gPortsOrch->getPort("Ethernet0", port)); + + // Build a sampled ERSPAN session bound to a PHY src port via createEntry. + std::vector data; + data.emplace_back("type", "ERSPAN"); + data.emplace_back("src_ip", "10.0.0.1"); + data.emplace_back("dst_ip", "10.0.0.2"); + data.emplace_back("gre_type", "0x8949"); + data.emplace_back("dscp", "8"); + data.emplace_back("ttl", "64"); + data.emplace_back("queue", "0"); + data.emplace_back("direction", "RX"); + data.emplace_back("sample_rate", "50000"); + data.emplace_back("src_port", "Ethernet0"); + ASSERT_EQ(gMirrorOrch->createEntry("sp_create_fail", data), task_process_status::task_success); + ASSERT_TRUE(gMirrorOrch->sessionExists("sp_create_fail")); + + auto& session = gMirrorOrch->m_syncdMirrors.find("sp_create_fail")->second; + ASSERT_FALSE(session.status); + + // Resolve the destination so activateSession reaches createSamplePacket. + session.neighborInfo.portId = port.m_port_id; + session.neighborInfo.mac = MacAddress("00:11:22:33:44:55"); + session.neighborInfo.port.m_type = Port::PHY; + + // Fail the samplepacket creation so activateSession aborts and removes + // the mirror session it just created. + mirror_sample_port_wrap_ut::g_fail_samplepacket_create = true; + ASSERT_FALSE(gMirrorOrch->activateSession("sp_create_fail", session)); + + // createSamplePacket nulls the OID on failure, and the session is left + // inactive after the mirror session is torn down. + ASSERT_EQ(session.samplepacketId, SAI_NULL_OBJECT_ID); + ASSERT_FALSE(session.status); + + gMirrorOrch->m_syncdMirrors.erase("sp_create_fail"); + } + + TEST_F(MirrorOrchPortTest, RemoveSamplePacketFails) + { + mirror_sample_port_wrap_ut::PortSampleSaiGuard saiPortSampleGuard; + // Covers removeSamplePacket's SAI-failure branch: remove_samplepacket + // returns failure, so the function reports failure via + // handleSaiRemoveStatus and retains the (non-NULL) OID. + ASSERT_NE(gMirrorOrch, nullptr); + + MirrorEntry entry(""); + entry.type = "ERSPAN"; + entry.sample_rate = 50000; + entry.truncate_size = 0; + + // createSamplePacket yields a real samplepacket OID from the VS switch. + ASSERT_TRUE(gMirrorOrch->createSamplePacket("rm_sp_fail", entry)); + ASSERT_NE(entry.samplepacketId, SAI_NULL_OBJECT_ID); + sai_object_id_t created = entry.samplepacketId; + + // Fail only the removal so the SAI-failure branch runs. + mirror_sample_port_wrap_ut::g_fail_samplepacket_remove = true; + ASSERT_FALSE(gMirrorOrch->removeSamplePacket("rm_sp_fail", entry)); + // The failure returns before the OID is cleared, so it is retained. + ASSERT_EQ(entry.samplepacketId, created); + + // Clean up for real now that the fault is disabled. + mirror_sample_port_wrap_ut::g_fail_samplepacket_remove = false; + sai_samplepacket_api->remove_samplepacket(created); + } + +} diff --git a/tests/mock_tests/mock_orchagent_main.h b/tests/mock_tests/mock_orchagent_main.h index d18478d0..118c45d5 100644 --- a/tests/mock_tests/mock_orchagent_main.h +++ b/tests/mock_tests/mock_orchagent_main.h @@ -105,6 +105,7 @@ extern sai_udf_api_t* sai_udf_api; extern sai_mpls_api_t* sai_mpls_api; extern sai_counter_api_t* sai_counter_api; extern sai_samplepacket_api_t *sai_samplepacket_api; +extern sai_mirror_api_t *sai_mirror_api; extern sai_fdb_api_t* sai_fdb_api; extern sai_twamp_api_t* sai_twamp_api; extern sai_tam_api_t* sai_tam_api; diff --git a/tests/mock_tests/portal.h b/tests/mock_tests/portal.h index cfc78ed4..cc8ec615 100644 --- a/tests/mock_tests/portal.h +++ b/tests/mock_tests/portal.h @@ -110,6 +110,36 @@ struct Portal { return obj.m_sflowPortInfoMap; } + + static bool sflowAddPort(SflowOrch &obj, sai_object_id_t sample_id, + sai_object_id_t port_id, std::string direction) + { + return obj.sflowAddPort(sample_id, port_id, direction); + } + + static bool sflowUpdateSampleDirection(SflowOrch &obj, sai_object_id_t port_id, + std::string old_dir, std::string new_dir) + { + return obj.sflowUpdateSampleDirection(port_id, old_dir, new_dir); + } + + static void seedSamplePacketOid(SflowOrch &obj, uint32_t rate, sai_object_id_t sample_id) + { + SflowSession s; + s.m_sample_id = sample_id; + s.ref_count = 0; + obj.m_sflowRateSampleMap[rate] = s; + } + + static void seedPortInfo(SflowOrch &obj, sai_object_id_t port_id, + sai_object_id_t sample_id, std::string dir) + { + SflowPortInfo p; + p.admin_state = true; + p.m_sample_dir = dir; + p.m_sample_id = sample_id; + obj.m_sflowPortInfoMap[port_id] = p; + } }; struct TwampOrchInternal diff --git a/tests/mock_tests/sfloworh_ut.cpp b/tests/mock_tests/sfloworh_ut.cpp index d3d4d0de..a05c7874 100644 --- a/tests/mock_tests/sfloworh_ut.cpp +++ b/tests/mock_tests/sfloworh_ut.cpp @@ -6,6 +6,7 @@ #include "ut_helper.h" #include "mock_orchagent_main.h" +#include "mirrororch_sample_port_sai_wrap.h" using namespace swss; @@ -367,6 +368,236 @@ namespace sflow_test ASSERT_FALSE(Portal::SflowOrchInternal::getSflowStatusEnable(mock_orch.get())); } } -} + TEST_F(SflowOrchTest, SflowAddPortRejectsConflictingEgressBinding) + { + MockSflowOrch mock_orch; + Port port; + ASSERT_TRUE(gPortsOrch->getPort("Ethernet0", port)); + ASSERT_NE(port.m_port_id, SAI_NULL_OBJECT_ID); + + sai_object_id_t foreign_oid; + sai_attribute_t sp_attr; + sp_attr.id = SAI_SAMPLEPACKET_ATTR_SAMPLE_RATE; + sp_attr.value.u32 = 8888; + ASSERT_EQ(sai_samplepacket_api->create_samplepacket(&foreign_oid, gSwitchId, 1, &sp_attr), + SAI_STATUS_SUCCESS); + + sai_attribute_t attr; + attr.id = SAI_PORT_ATTR_EGRESS_SAMPLEPACKET_ENABLE; + attr.value.oid = foreign_oid; + ASSERT_EQ(sai_port_api->set_port_attribute(port.m_port_id, &attr), SAI_STATUS_SUCCESS); + + sai_object_id_t sflow_sample; + sai_attribute_t sp_attr2; + sp_attr2.id = SAI_SAMPLEPACKET_ATTR_SAMPLE_RATE; + sp_attr2.value.u32 = 5678; + ASSERT_EQ(sai_samplepacket_api->create_samplepacket(&sflow_sample, gSwitchId, 1, &sp_attr2), + SAI_STATUS_SUCCESS); + + ASSERT_FALSE(Portal::SflowOrchInternal::sflowAddPort( + mock_orch.get(), sflow_sample, port.m_port_id, "tx")); + + sai_attribute_t after; + after.id = SAI_PORT_ATTR_EGRESS_SAMPLEPACKET_ENABLE; + ASSERT_EQ(sai_port_api->get_port_attribute(port.m_port_id, 1, &after), SAI_STATUS_SUCCESS); + ASSERT_EQ(after.value.oid, foreign_oid); + + attr.value.oid = SAI_NULL_OBJECT_ID; + sai_port_api->set_port_attribute(port.m_port_id, &attr); + sai_samplepacket_api->remove_samplepacket(foreign_oid); + sai_samplepacket_api->remove_samplepacket(sflow_sample); + } + TEST_F(SflowOrchTest, SflowAddPortAllowsSflowOwnedOidBinding) + { + MockSflowOrch mock_orch; + Port port; + ASSERT_TRUE(gPortsOrch->getPort("Ethernet0", port)); + ASSERT_NE(port.m_port_id, SAI_NULL_OBJECT_ID); + + // Create two real samplepacket OIDs (SAI VS validates OIDs) and treat + // both as sflow-owned via seedSamplePacketOid. + sai_object_id_t prior_sflow_oid; + sai_attribute_t sp_attr; + sp_attr.id = SAI_SAMPLEPACKET_ATTR_SAMPLE_RATE; + sp_attr.value.u32 = 10000; + ASSERT_EQ(sai_samplepacket_api->create_samplepacket(&prior_sflow_oid, gSwitchId, 1, &sp_attr), + SAI_STATUS_SUCCESS); + + sai_object_id_t new_sflow_oid; + sai_attribute_t sp_attr2; + sp_attr2.id = SAI_SAMPLEPACKET_ATTR_SAMPLE_RATE; + sp_attr2.value.u32 = 20000; + ASSERT_EQ(sai_samplepacket_api->create_samplepacket(&new_sflow_oid, gSwitchId, 1, &sp_attr2), + SAI_STATUS_SUCCESS); + + Portal::SflowOrchInternal::seedSamplePacketOid(mock_orch.get(), 10000, prior_sflow_oid); + + // Pre-bind port egress to that sflow-owned OID + sai_attribute_t attr; + attr.id = SAI_PORT_ATTR_EGRESS_SAMPLEPACKET_ENABLE; + attr.value.oid = prior_sflow_oid; + ASSERT_EQ(sai_port_api->set_port_attribute(port.m_port_id, &attr), SAI_STATUS_SUCCESS); + + // Bind a different sflow sample_id on tx; pre-check sees prior_sflow_oid is + // sflow-owned and lets the bind through. + ASSERT_TRUE(Portal::SflowOrchInternal::sflowAddPort( + mock_orch.get(), new_sflow_oid, port.m_port_id, "tx")); + + // Egress attr is now the new sflow OID + sai_attribute_t after; + after.id = SAI_PORT_ATTR_EGRESS_SAMPLEPACKET_ENABLE; + ASSERT_EQ(sai_port_api->get_port_attribute(port.m_port_id, 1, &after), SAI_STATUS_SUCCESS); + ASSERT_EQ(after.value.oid, new_sflow_oid); + + // Cleanup + attr.value.oid = SAI_NULL_OBJECT_ID; + sai_port_api->set_port_attribute(port.m_port_id, &attr); + sai_samplepacket_api->remove_samplepacket(prior_sflow_oid); + sai_samplepacket_api->remove_samplepacket(new_sflow_oid); + } + + TEST_F(SflowOrchTest, SflowUpdateDirectionRejectsConflictingBinding) + { + MockSflowOrch mock_orch; + Port port; + ASSERT_TRUE(gPortsOrch->getPort("Ethernet0", port)); + ASSERT_NE(port.m_port_id, SAI_NULL_OBJECT_ID); + + // Seed port info so sflowUpdateSampleDirection can find the sample_id. + // Initial direction is "rx"; we are transitioning to "tx". + sai_object_id_t my_sflow_oid; + sai_attribute_t sp_attr; + sp_attr.id = SAI_SAMPLEPACKET_ATTR_SAMPLE_RATE; + sp_attr.value.u32 = 4242; + ASSERT_EQ(sai_samplepacket_api->create_samplepacket(&my_sflow_oid, gSwitchId, 1, &sp_attr), + SAI_STATUS_SUCCESS); + Portal::SflowOrchInternal::seedPortInfo( + mock_orch.get(), port.m_port_id, my_sflow_oid, "rx"); + + // Pre-bind port egress to a foreign (non-sflow) OID + sai_object_id_t foreign_oid; + sai_attribute_t sp_attr2; + sp_attr2.id = SAI_SAMPLEPACKET_ATTR_SAMPLE_RATE; + sp_attr2.value.u32 = 7777; + ASSERT_EQ(sai_samplepacket_api->create_samplepacket(&foreign_oid, gSwitchId, 1, &sp_attr2), + SAI_STATUS_SUCCESS); + + sai_attribute_t attr; + attr.id = SAI_PORT_ATTR_EGRESS_SAMPLEPACKET_ENABLE; + attr.value.oid = foreign_oid; + ASSERT_EQ(sai_port_api->set_port_attribute(port.m_port_id, &attr), SAI_STATUS_SUCCESS); + + // rx -> tx wants to assign egress; pre-check must detect the conflict + // and bail out before any SAI mutation happens. + ASSERT_FALSE(Portal::SflowOrchInternal::sflowUpdateSampleDirection( + mock_orch.get(), port.m_port_id, "rx", "tx")); + + // Egress binding must remain the foreign OID (no SAI mutation happened) + sai_attribute_t after; + after.id = SAI_PORT_ATTR_EGRESS_SAMPLEPACKET_ENABLE; + ASSERT_EQ(sai_port_api->get_port_attribute(port.m_port_id, 1, &after), SAI_STATUS_SUCCESS); + ASSERT_EQ(after.value.oid, foreign_oid); + + // Cleanup + attr.value.oid = SAI_NULL_OBJECT_ID; + sai_port_api->set_port_attribute(port.m_port_id, &attr); + sai_samplepacket_api->remove_samplepacket(my_sflow_oid); + sai_samplepacket_api->remove_samplepacket(foreign_oid); + } + TEST_F(SflowOrchTest, SflowAddPortRejectsConflictingIngressBinding) + { + mirror_sample_port_wrap_ut::PortSampleSaiGuard saiPortSampleGuard; + + MockSflowOrch mock_orch; + Port port; + ASSERT_TRUE(gPortsOrch->getPort("Ethernet0", port)); + ASSERT_NE(port.m_port_id, SAI_NULL_OBJECT_ID); + + // Pre-bind port ingress to a foreign (non-sflow) OID via the wrap. + sai_object_id_t foreign_oid; + sai_attribute_t sp_attr; + sp_attr.id = SAI_SAMPLEPACKET_ATTR_SAMPLE_RATE; + sp_attr.value.u32 = 8888; + ASSERT_EQ(sai_samplepacket_api->create_samplepacket(&foreign_oid, gSwitchId, 1, &sp_attr), + SAI_STATUS_SUCCESS); + + sai_attribute_t attr; + attr.id = SAI_PORT_ATTR_INGRESS_SAMPLEPACKET_ENABLE; + attr.value.oid = foreign_oid; + ASSERT_EQ(sai_port_api->set_port_attribute(port.m_port_id, &attr), SAI_STATUS_SUCCESS); + + sai_object_id_t sflow_sample; + sai_attribute_t sp_attr2; + sp_attr2.id = SAI_SAMPLEPACKET_ATTR_SAMPLE_RATE; + sp_attr2.value.u32 = 5678; + ASSERT_EQ(sai_samplepacket_api->create_samplepacket(&sflow_sample, gSwitchId, 1, &sp_attr2), + SAI_STATUS_SUCCESS); + + // rx bind must be rejected because ingress is held by a foreign OID. + ASSERT_FALSE(Portal::SflowOrchInternal::sflowAddPort( + mock_orch.get(), sflow_sample, port.m_port_id, "rx")); + + // Ingress binding must remain the foreign OID (pre-check bails before mutation). + sai_attribute_t after; + after.id = SAI_PORT_ATTR_INGRESS_SAMPLEPACKET_ENABLE; + ASSERT_EQ(sai_port_api->get_port_attribute(port.m_port_id, 1, &after), SAI_STATUS_SUCCESS); + ASSERT_EQ(after.value.oid, foreign_oid); + + attr.value.oid = SAI_NULL_OBJECT_ID; + sai_port_api->set_port_attribute(port.m_port_id, &attr); + sai_samplepacket_api->remove_samplepacket(foreign_oid); + sai_samplepacket_api->remove_samplepacket(sflow_sample); + } + + TEST_F(SflowOrchTest, SflowUpdateDirectionRejectsConflictingIngressBinding) + { + mirror_sample_port_wrap_ut::PortSampleSaiGuard saiPortSampleGuard; + + MockSflowOrch mock_orch; + Port port; + ASSERT_TRUE(gPortsOrch->getPort("Ethernet0", port)); + ASSERT_NE(port.m_port_id, SAI_NULL_OBJECT_ID); + + // Seed port info with initial direction "tx"; transition tx -> rx assigns + // ingress, so the ingress conflict pre-check runs. + sai_object_id_t my_sflow_oid; + sai_attribute_t sp_attr; + sp_attr.id = SAI_SAMPLEPACKET_ATTR_SAMPLE_RATE; + sp_attr.value.u32 = 4242; + ASSERT_EQ(sai_samplepacket_api->create_samplepacket(&my_sflow_oid, gSwitchId, 1, &sp_attr), + SAI_STATUS_SUCCESS); + Portal::SflowOrchInternal::seedPortInfo( + mock_orch.get(), port.m_port_id, my_sflow_oid, "tx"); + + // Pre-bind port ingress to a foreign (non-sflow) OID via the wrap. + sai_object_id_t foreign_oid; + sai_attribute_t sp_attr2; + sp_attr2.id = SAI_SAMPLEPACKET_ATTR_SAMPLE_RATE; + sp_attr2.value.u32 = 7777; + ASSERT_EQ(sai_samplepacket_api->create_samplepacket(&foreign_oid, gSwitchId, 1, &sp_attr2), + SAI_STATUS_SUCCESS); + + sai_attribute_t attr; + attr.id = SAI_PORT_ATTR_INGRESS_SAMPLEPACKET_ENABLE; + attr.value.oid = foreign_oid; + ASSERT_EQ(sai_port_api->set_port_attribute(port.m_port_id, &attr), SAI_STATUS_SUCCESS); + + // tx -> rx wants to assign ingress; pre-check must detect the conflict + // and bail out before any SAI mutation happens. + ASSERT_FALSE(Portal::SflowOrchInternal::sflowUpdateSampleDirection( + mock_orch.get(), port.m_port_id, "tx", "rx")); + + // Ingress binding must remain the foreign OID (no SAI mutation happened). + sai_attribute_t after; + after.id = SAI_PORT_ATTR_INGRESS_SAMPLEPACKET_ENABLE; + ASSERT_EQ(sai_port_api->get_port_attribute(port.m_port_id, 1, &after), SAI_STATUS_SUCCESS); + ASSERT_EQ(after.value.oid, foreign_oid); + + attr.value.oid = SAI_NULL_OBJECT_ID; + sai_port_api->set_port_attribute(port.m_port_id, &attr); + sai_samplepacket_api->remove_samplepacket(my_sflow_oid); + sai_samplepacket_api->remove_samplepacket(foreign_oid); + } +} diff --git a/tests/mock_tests/switchorch_ut.cpp b/tests/mock_tests/switchorch_ut.cpp index 0f0ce5d7..a2c14335 100644 --- a/tests/mock_tests/switchorch_ut.cpp +++ b/tests/mock_tests/switchorch_ut.cpp @@ -328,6 +328,14 @@ namespace switchorch_test ASSERT_TRUE(ingress_exists); bool egress_exists = gSwitchOrch->m_switchTable.hget("switch", SWITCH_CAPABILITY_TABLE_PORT_EGRESS_MIRROR_CAPABLE, value); ASSERT_TRUE(egress_exists); + + // Test that sampled mirror capabilities are queried and stored + bool ingress_sample_exists = gSwitchOrch->m_switchTable.hget("switch", SWITCH_CAPABILITY_TABLE_PORT_INGRESS_SAMPLE_MIRROR_CAPABLE, value); + ASSERT_TRUE(ingress_sample_exists); + bool egress_sample_exists = gSwitchOrch->m_switchTable.hget("switch", SWITCH_CAPABILITY_TABLE_PORT_EGRESS_SAMPLE_MIRROR_CAPABLE, value); + ASSERT_TRUE(egress_sample_exists); + bool truncation_exists = gSwitchOrch->m_switchTable.hget("switch", SWITCH_CAPABILITY_TABLE_SAMPLEPACKET_TRUNCATION_CAPABLE, value); + ASSERT_TRUE(truncation_exists); } TEST_F(SwitchOrchTest, SwitchOrchTestCheckCapabilityUnsupported) @@ -355,6 +363,14 @@ namespace switchorch_test bool egress_exists = gSwitchOrch->m_switchTable.hget("switch", SWITCH_CAPABILITY_TABLE_PORT_EGRESS_MIRROR_CAPABLE, value); ASSERT_TRUE(egress_exists); + // Test that sampled mirror capabilities are queried and stored + bool ingress_sample_exists = gSwitchOrch->m_switchTable.hget("switch", SWITCH_CAPABILITY_TABLE_PORT_INGRESS_SAMPLE_MIRROR_CAPABLE, value); + ASSERT_TRUE(ingress_sample_exists); + bool egress_sample_exists = gSwitchOrch->m_switchTable.hget("switch", SWITCH_CAPABILITY_TABLE_PORT_EGRESS_SAMPLE_MIRROR_CAPABLE, value); + ASSERT_TRUE(egress_sample_exists); + bool truncation_exists = gSwitchOrch->m_switchTable.hget("switch", SWITCH_CAPABILITY_TABLE_SAMPLEPACKET_TRUNCATION_CAPABLE, value); + ASSERT_TRUE(truncation_exists); + // case: unsupported severity. To satisfy coverage. vector ts; std::deque entries; diff --git a/tests/mock_tests/ut_saihelper.cpp b/tests/mock_tests/ut_saihelper.cpp index 467a26a5..d1ad244d 100644 --- a/tests/mock_tests/ut_saihelper.cpp +++ b/tests/mock_tests/ut_saihelper.cpp @@ -70,6 +70,7 @@ namespace ut_helper sai_api_query(SAI_API_BRIDGE, (void **)&sai_bridge_api); sai_api_query(SAI_API_VIRTUAL_ROUTER, (void **)&sai_virtual_router_api); sai_api_query(SAI_API_SAMPLEPACKET, (void **)&sai_samplepacket_api); + sai_api_query(SAI_API_MIRROR, (void **)&sai_mirror_api); sai_api_query(SAI_API_PORT, (void **)&sai_port_api); sai_api_query(SAI_API_LAG, (void **)&sai_lag_api); sai_api_query(SAI_API_VLAN, (void **)&sai_vlan_api); @@ -126,6 +127,7 @@ namespace ut_helper sai_bridge_api = nullptr; sai_virtual_router_api = nullptr; sai_port_api = nullptr; + sai_mirror_api = nullptr; sai_lag_api = nullptr; sai_vlan_api = nullptr; sai_router_intfs_api = nullptr; diff --git a/tests/test_mirror_sampled_erspan.py b/tests/test_mirror_sampled_erspan.py new file mode 100644 index 00000000..1982a8f9 --- /dev/null +++ b/tests/test_mirror_sampled_erspan.py @@ -0,0 +1,593 @@ +# This test suite covers the functionality of sampled port mirroring with truncation on ERSPAN sessions +import pytest +import time + +@pytest.mark.usefixtures("testlog") +@pytest.mark.usefixtures('dvs_vlan_manager') +@pytest.mark.usefixtures('dvs_lag_manager') +@pytest.mark.usefixtures('dvs_mirror_manager') +class TestSampledMirror(object): + + @pytest.fixture(autouse=True) + def _isolate(self, dvs): + """Force a clean baseline before and after every test. + """ + self._force_clean(dvs) + yield + self._force_clean(dvs) + + def _wait_route_absent(self, dvs, prefix, timeout=10): + """Poll until the kernel route for prefix is gone.""" + for _ in range(timeout * 10): + if prefix not in dvs.runcmd("ip route show " + prefix)[1]: + return + time.sleep(0.1) + assert prefix not in dvs.runcmd("ip route show " + prefix)[1], \ + f"route {prefix} still present after cleanup" + + def _force_clean(self, dvs): + """Return the shared ERSPAN route / mirror / sFlow state to a baseline.""" + dvs.setup_db() + + # Remove any mirror sessions left behind by a failed test. + mirror_keys = dvs.config_db.get_keys("MIRROR_SESSION") + for key in mirror_keys: + self.dvs_mirror.remove_mirror_session(key) + + # Remove any leftover sFlow config (from the sFlow-conflict test). + for key in dvs.config_db.get_keys("SFLOW_SESSION"): + dvs.config_db.delete_entry("SFLOW_SESSION", key) + dvs.config_db.delete_entry("SFLOW", "global") + + # Tear down the shared route/neighbor/ip and bring the monitor port down. + self._tear_down_route(dvs) + + # Wait for the removed sessions and the kernel route to actually drain + # before the next test runs, then confirm no samplepacket survives. + for key in mirror_keys: + dvs.state_db.wait_for_deleted_entry("MIRROR_SESSION_TABLE", key) + self._wait_route_absent(dvs, "2.2.2.2") + dvs.asic_db.wait_for_n_keys("ASIC_STATE:SAI_OBJECT_TYPE_SAMPLEPACKET", 0) + + def _bring_up_route(self, dvs): + """Bring up the monitor port and install the route to the dst_ip""" + dvs.set_interface_status("Ethernet16", "up") + dvs.add_ip_address("Ethernet16", "10.0.0.0/30") + dvs.add_neighbor("Ethernet16", "10.0.0.1", "02:04:06:08:10:12") + dvs.add_route("2.2.2.2", "10.0.0.1") + + def _tear_down_route(self, dvs): + """Remove the route and bring down the monitor port""" + dvs.remove_route("2.2.2.2") + dvs.remove_neighbor("Ethernet16", "10.0.0.1") + dvs.remove_ip_address("Ethernet16", "10.0.0.0/30") + dvs.set_interface_status("Ethernet16", "down") + + def _setup_mirror_session(self, dvs, session, src_ports, sample_rate=None, + truncate_size=None, direction="RX"): + """Helper to set up network and create a sampled ERSPAN mirror session""" + dvs.setup_db() + + # create sampled mirror session + self.dvs_mirror.create_erspan_session_sampled( + session, "1.1.1.1", "2.2.2.2", "0x8949", "8", "64", "0", + src_ports=src_ports, direction=direction, + sample_rate=sample_rate, truncate_size=truncate_size) + + # Bring up port and create route to dst_ip + self._bring_up_route(dvs) + dvs.state_db.wait_for_field_match("MIRROR_SESSION_TABLE", session, {"status": "active"}) + + def _teardown_mirror_session(self, dvs, session): + """Helper to tear down network and remove mirror session""" + self._tear_down_route(dvs) + self.dvs_mirror.remove_mirror_session(session) + dvs.state_db.wait_for_deleted_entry("MIRROR_SESSION_TABLE", session) + + def _assert_sample_mirror_capable(self, dvs, ingress=True, egress=True, truncation=True): + """ + Assert the sampled-mirror capability values published to STATE_DB. + """ + cap = dvs.state_db.wait_for_entry("SWITCH_CAPABILITY", "switch") + assert cap.get("PORT_INGRESS_SAMPLE_MIRROR_CAPABLE") == ("true" if ingress else "false") + assert cap.get("PORT_EGRESS_SAMPLE_MIRROR_CAPABLE") == ("true" if egress else "false") + assert cap.get("SAMPLEPACKET_TRUNCATION_CAPABLE") == ("true" if truncation else "false") + + def test_SampledMirrorCreateRemove(self, dvs, testlog): + """ + Test sampled ERSPAN mirror session create and remove. + Verify SAMPLEPACKET object is created in ASIC_DB with correct attributes. + """ + session = "SAMPLED_SESSION" + self._setup_mirror_session(dvs, session, "Ethernet12", sample_rate="50000") + + # Verify SAMPLEPACKET object exists in ASIC_DB + samplepacket_keys = dvs.asic_db.wait_for_n_keys("ASIC_STATE:SAI_OBJECT_TYPE_SAMPLEPACKET", 1) + samplepacket_entry = dvs.asic_db.wait_for_entry("ASIC_STATE:SAI_OBJECT_TYPE_SAMPLEPACKET", samplepacket_keys[0]) + + # Verify correct attributes + assert samplepacket_entry["SAI_SAMPLEPACKET_ATTR_SAMPLE_RATE"] == "50000" + assert samplepacket_entry["SAI_SAMPLEPACKET_ATTR_TYPE"] == "SAI_SAMPLEPACKET_TYPE_MIRROR_SESSION" + + # Verify port attributes + fvs = dvs.counters_db.get_entry("COUNTERS_PORT_NAME_MAP", "") + fvs = dict(fvs) + port_oid = fvs.get("Ethernet12") + port_entry = dvs.asic_db.wait_for_entry("ASIC_STATE:SAI_OBJECT_TYPE_PORT", port_oid) + assert "SAI_PORT_ATTR_INGRESS_SAMPLEPACKET_ENABLE" in port_entry + assert "SAI_PORT_ATTR_INGRESS_SAMPLE_MIRROR_SESSION" in port_entry + + # Remove and verify cleanup + self._teardown_mirror_session(dvs, session) + dvs.asic_db.wait_for_n_keys("ASIC_STATE:SAI_OBJECT_TYPE_SAMPLEPACKET", 0) + + def test_SampledMirrorWithTruncation(self, dvs, testlog): + """ + Test sampled ERSPAN mirror session with truncation. + Verify truncation attributes are set on SAMPLEPACKET object. + """ + session = "SAMPLED_TRUNCATE_SESSION" + self._setup_mirror_session(dvs, session, "Ethernet12", + sample_rate="50000", truncate_size="128") + + # Verify SAMPLEPACKET with truncation + samplepacket_keys = dvs.asic_db.wait_for_n_keys("ASIC_STATE:SAI_OBJECT_TYPE_SAMPLEPACKET", 1) + samplepacket_entry = dvs.asic_db.wait_for_entry("ASIC_STATE:SAI_OBJECT_TYPE_SAMPLEPACKET", samplepacket_keys[0]) + + assert samplepacket_entry["SAI_SAMPLEPACKET_ATTR_SAMPLE_RATE"] == "50000" + assert samplepacket_entry["SAI_SAMPLEPACKET_ATTR_TYPE"] == "SAI_SAMPLEPACKET_TYPE_MIRROR_SESSION" + assert samplepacket_entry["SAI_SAMPLEPACKET_ATTR_TRUNCATE_ENABLE"] == "true" + assert samplepacket_entry["SAI_SAMPLEPACKET_ATTR_TRUNCATE_SIZE"] == "128" + + self._teardown_mirror_session(dvs, session) + + def test_NoSamplePacketWhenSampleRateAbsent(self, dvs, testlog): + """ + Test backward compatibility: no SAMPLEPACKET created when sample_rate is not configured. + """ + session = "FULL_MIRROR_SESSION" + + dvs.setup_db() + # Create regular ERSPAN session without sample_rate + self.dvs_mirror.create_erspan_session( + session, "1.1.1.1", "2.2.2.2", "0x8949", "8", "64", "0", + src_ports="Ethernet12", direction="RX") + + dvs.set_interface_status("Ethernet16", "up") + dvs.add_ip_address("Ethernet16", "10.0.0.0/30") + dvs.add_neighbor("Ethernet16", "10.0.0.1", "02:04:06:08:10:12") + dvs.add_route("2.2.2.2", "10.0.0.1") + dvs.state_db.wait_for_field_match("MIRROR_SESSION_TABLE", session, {"status": "active"}) + + # Verify NO SAMPLEPACKET object in ASIC_DB + dvs.asic_db.wait_for_n_keys("ASIC_STATE:SAI_OBJECT_TYPE_SAMPLEPACKET", 0) + + # Verify full mirror path (INGRESS_MIRROR_SESSION, not SAMPLE_MIRROR_SESSION) + fvs = dvs.counters_db.get_entry("COUNTERS_PORT_NAME_MAP", "") + fvs = dict(fvs) + port_oid = fvs.get("Ethernet12") + port_entry = dvs.asic_db.wait_for_entry("ASIC_STATE:SAI_OBJECT_TYPE_PORT", port_oid) + assert "SAI_PORT_ATTR_INGRESS_MIRROR_SESSION" in port_entry + + self._teardown_mirror_session(dvs, session) + + def test_SampledMirrorStateDB(self, dvs, testlog): + """ + Test that sample_rate and truncate_size are written to STATE_DB. + """ + session = "SAMPLED_STATE_SESSION" + self._setup_mirror_session(dvs, session, "Ethernet12", + sample_rate="50000", truncate_size="128") + + # Verify STATE_DB has sample_rate and truncate_size + state_entry = dvs.state_db.wait_for_entry("MIRROR_SESSION_TABLE", session) + assert state_entry.get("sample_rate") == "50000" + assert state_entry.get("truncate_size") == "128" + + self._teardown_mirror_session(dvs, session) + + def test_SampledMirrorCapabilityInStateDB(self, dvs, testlog): + """ + Test that sampled mirror capability values are published to STATE_DB. + In the virtual switch all three are expected to be "true"; this also + guards the precondition for the TX/BOTH sampled mirror VS tests. + """ + dvs.setup_db() + self._assert_sample_mirror_capable(dvs, ingress=True, egress=True, truncation=True) + + def test_SampledMirrorTxDirection(self, dvs, testlog): + """ + Test sampled ERSPAN mirror session with TX (egress) direction. + Verify the SAMPLEPACKET is created and the source port is programmed + with egress sampled mirror attributes only (no ingress attrs). + """ + session = "SAMPLED_TX_SESSION" + dvs.setup_db() + # Egress sampled mirroring must be reported capable for this VS test + self._assert_sample_mirror_capable(dvs, egress=True) + self._setup_mirror_session(dvs, session, "Ethernet12", + sample_rate="50000", direction="TX") + + # SAMPLEPACKET created + dvs.asic_db.wait_for_n_keys("ASIC_STATE:SAI_OBJECT_TYPE_SAMPLEPACKET", 1) + + # Port programmed with egress sampled mirror attrs + fvs = dict(dvs.counters_db.get_entry("COUNTERS_PORT_NAME_MAP", "")) + port_oid = fvs.get("Ethernet12") + port_entry = dvs.asic_db.wait_for_entry("ASIC_STATE:SAI_OBJECT_TYPE_PORT", port_oid) + assert "SAI_PORT_ATTR_EGRESS_SAMPLEPACKET_ENABLE" in port_entry + assert "SAI_PORT_ATTR_EGRESS_SAMPLE_MIRROR_SESSION" in port_entry + # Ingress must not be actively bound + if "SAI_PORT_ATTR_INGRESS_SAMPLEPACKET_ENABLE" in port_entry: + assert port_entry["SAI_PORT_ATTR_INGRESS_SAMPLEPACKET_ENABLE"] == "oid:0x0" + if "SAI_PORT_ATTR_INGRESS_SAMPLE_MIRROR_SESSION" in port_entry: + assert port_entry["SAI_PORT_ATTR_INGRESS_SAMPLE_MIRROR_SESSION"] == "0:null" + + self._teardown_mirror_session(dvs, session) + dvs.asic_db.wait_for_n_keys("ASIC_STATE:SAI_OBJECT_TYPE_SAMPLEPACKET", 0) + + def test_SampledMirrorTruncateWithoutRateDefaultsToRate1(self, dvs, testlog): + """ + Test that truncate_size without an explicit sample_rate defaults the + sample rate to 1 (sample every packet) so a SAMPLEPACKET object is + still created to carry the truncation. + """ + session = "SAMPLED_TRUNC_NO_RATE_SESSION" + self._setup_mirror_session(dvs, session, "Ethernet12", + truncate_size="128") + + # SAMPLEPACKET is created with sample_rate defaulted to 1 and truncation applied + samplepacket_keys = dvs.asic_db.wait_for_n_keys("ASIC_STATE:SAI_OBJECT_TYPE_SAMPLEPACKET", 1) + samplepacket_entry = dvs.asic_db.wait_for_entry("ASIC_STATE:SAI_OBJECT_TYPE_SAMPLEPACKET", samplepacket_keys[0]) + assert samplepacket_entry["SAI_SAMPLEPACKET_ATTR_SAMPLE_RATE"] == "1" + assert samplepacket_entry["SAI_SAMPLEPACKET_ATTR_TRUNCATE_ENABLE"] == "true" + assert samplepacket_entry["SAI_SAMPLEPACKET_ATTR_TRUNCATE_SIZE"] == "128" + + self._teardown_mirror_session(dvs, session) + dvs.asic_db.wait_for_n_keys("ASIC_STATE:SAI_OBJECT_TYPE_SAMPLEPACKET", 0) + + def test_SampledMirrorPortCleanup(self, dvs, testlog): + """ + Test that port attributes are properly cleaned up after removing a sampled mirror session. + Verify SAMPLEPACKET_ENABLE and SAMPLE_MIRROR_SESSION are cleared from port. + """ + session = "SAMPLED_CLEANUP_SESSION" + self._setup_mirror_session(dvs, session, "Ethernet12", sample_rate="50000") + + # Verify port has sampled mirror attributes set + fvs = dvs.counters_db.get_entry("COUNTERS_PORT_NAME_MAP", "") + fvs = dict(fvs) + port_oid = fvs.get("Ethernet12") + port_entry = dvs.asic_db.wait_for_entry("ASIC_STATE:SAI_OBJECT_TYPE_PORT", port_oid) + assert "SAI_PORT_ATTR_INGRESS_SAMPLEPACKET_ENABLE" in port_entry + assert "SAI_PORT_ATTR_INGRESS_SAMPLE_MIRROR_SESSION" in port_entry + + # Remove mirror session + self._teardown_mirror_session(dvs, session) + + # Verify SAMPLEPACKET object removed + dvs.asic_db.wait_for_n_keys("ASIC_STATE:SAI_OBJECT_TYPE_SAMPLEPACKET", 0) + + # Verify port attributes cleaned up + port_entry = dvs.asic_db.wait_for_entry("ASIC_STATE:SAI_OBJECT_TYPE_PORT", port_oid) + # After cleanup, SAMPLEPACKET_ENABLE should be null or absent + if "SAI_PORT_ATTR_INGRESS_SAMPLEPACKET_ENABLE" in port_entry: + assert port_entry["SAI_PORT_ATTR_INGRESS_SAMPLEPACKET_ENABLE"] == "oid:0x0" + if "SAI_PORT_ATTR_INGRESS_SAMPLE_MIRROR_SESSION" in port_entry: + assert port_entry["SAI_PORT_ATTR_INGRESS_SAMPLE_MIRROR_SESSION"] == "0:null" + + def test_SampledMirrorMultipleSrcPorts(self, dvs, testlog): + """ + Test sampled ERSPAN mirror session with multiple source ports. + Verify both ports have SAMPLEPACKET_ENABLE and SAMPLE_MIRROR_SESSION set. + """ + session = "SAMPLED_MULTI_PORT_SESSION" + self._setup_mirror_session(dvs, session, "Ethernet0,Ethernet4", sample_rate="50000") + + # Verify SAMPLEPACKET object exists + dvs.asic_db.wait_for_n_keys("ASIC_STATE:SAI_OBJECT_TYPE_SAMPLEPACKET", 1) + + # Verify both ports have sampled mirror attributes + fvs = dvs.counters_db.get_entry("COUNTERS_PORT_NAME_MAP", "") + fvs = dict(fvs) + for port_name in ["Ethernet0", "Ethernet4"]: + port_oid = fvs.get(port_name) + port_entry = dvs.asic_db.wait_for_entry("ASIC_STATE:SAI_OBJECT_TYPE_PORT", port_oid) + assert "SAI_PORT_ATTR_INGRESS_SAMPLEPACKET_ENABLE" in port_entry, \ + f"SAMPLEPACKET_ENABLE not set on {port_name}" + assert "SAI_PORT_ATTR_INGRESS_SAMPLE_MIRROR_SESSION" in port_entry, \ + f"SAMPLE_MIRROR_SESSION not set on {port_name}" + + self._teardown_mirror_session(dvs, session) + + def test_SampledMirrorBothDirection(self, dvs, testlog): + """ + Test sampled ERSPAN mirror session with BOTH direction. + Verify a single SAMPLEPACKET is created and the source port is + programmed with both ingress and egress sampled mirror attributes. + """ + session = "SAMPLED_BOTH_SESSION" + dvs.setup_db() + # Both ingress and egress sampled mirroring must be reported capable + self._assert_sample_mirror_capable(dvs, ingress=True, egress=True) + self._setup_mirror_session(dvs, session, "Ethernet12", + sample_rate="50000", direction="BOTH") + + # A single SAMPLEPACKET serves both directions + dvs.asic_db.wait_for_n_keys("ASIC_STATE:SAI_OBJECT_TYPE_SAMPLEPACKET", 1) + + # Port programmed with both ingress and egress sampled mirror attrs + fvs = dict(dvs.counters_db.get_entry("COUNTERS_PORT_NAME_MAP", "")) + port_oid = fvs.get("Ethernet12") + port_entry = dvs.asic_db.wait_for_entry("ASIC_STATE:SAI_OBJECT_TYPE_PORT", port_oid) + assert "SAI_PORT_ATTR_INGRESS_SAMPLEPACKET_ENABLE" in port_entry + assert "SAI_PORT_ATTR_INGRESS_SAMPLE_MIRROR_SESSION" in port_entry + assert "SAI_PORT_ATTR_EGRESS_SAMPLEPACKET_ENABLE" in port_entry + assert "SAI_PORT_ATTR_EGRESS_SAMPLE_MIRROR_SESSION" in port_entry + + self._teardown_mirror_session(dvs, session) + dvs.asic_db.wait_for_n_keys("ASIC_STATE:SAI_OBJECT_TYPE_SAMPLEPACKET", 0) + + def test_SampledMirrorDeleteRecreate(self, dvs, testlog): + """ + Verify recreating a session under the same key applies the new sample_rate and truncation. + """ + session = "SAMPLED_RECREATE_SESSION" + dvs.setup_db() + + # Create initial sampled session (rate only) + self.dvs_mirror.create_erspan_session_sampled( + session, "1.1.1.1", "2.2.2.2", "0x8949", "8", "64", "0", + src_ports="Ethernet12", direction="RX", sample_rate="50000") + self._bring_up_route(dvs) + dvs.state_db.wait_for_field_match("MIRROR_SESSION_TABLE", session, {"status": "active"}) + + samplepacket_keys = dvs.asic_db.wait_for_n_keys("ASIC_STATE:SAI_OBJECT_TYPE_SAMPLEPACKET", 1) + entry = dvs.asic_db.wait_for_entry("ASIC_STATE:SAI_OBJECT_TYPE_SAMPLEPACKET", samplepacket_keys[0]) + assert entry["SAI_SAMPLEPACKET_ATTR_SAMPLE_RATE"] == "50000" + + # Delete the session; the SAMPLEPACKET must be cleaned up + self.dvs_mirror.remove_mirror_session(session) + dvs.state_db.wait_for_deleted_entry("MIRROR_SESSION_TABLE", session) + dvs.asic_db.wait_for_n_keys("ASIC_STATE:SAI_OBJECT_TYPE_SAMPLEPACKET", 0) + + # Recreate under the same key with a new rate + truncation + self.dvs_mirror.create_erspan_session_sampled( + session, "1.1.1.1", "2.2.2.2", "0x8949", "8", "64", "0", + src_ports="Ethernet12", direction="RX", + sample_rate="100000", truncate_size="128") + dvs.state_db.wait_for_field_match("MIRROR_SESSION_TABLE", session, {"status": "active"}) + + new_keys = dvs.asic_db.wait_for_n_keys("ASIC_STATE:SAI_OBJECT_TYPE_SAMPLEPACKET", 1) + new_entry = dvs.asic_db.wait_for_entry("ASIC_STATE:SAI_OBJECT_TYPE_SAMPLEPACKET", new_keys[0]) + assert new_entry["SAI_SAMPLEPACKET_ATTR_SAMPLE_RATE"] == "100000" + assert new_entry["SAI_SAMPLEPACKET_ATTR_TRUNCATE_ENABLE"] == "true" + assert new_entry["SAI_SAMPLEPACKET_ATTR_TRUNCATE_SIZE"] == "128" + + # STATE_DB reflects the recreated values + state_entry = dvs.state_db.wait_for_entry("MIRROR_SESSION_TABLE", session) + assert state_entry.get("sample_rate") == "100000" + assert state_entry.get("truncate_size") == "128" + + self._teardown_mirror_session(dvs, session) + dvs.asic_db.wait_for_n_keys("ASIC_STATE:SAI_OBJECT_TYPE_SAMPLEPACKET", 0) + + def test_SampledMirrorDuplicateSetIgnored(self, dvs, testlog): + """ + Verify the existing SAMPLEPACKET and STATE_DB are not mutated. + """ + session = "SAMPLED_DUP_SET_SESSION" + self._setup_mirror_session(dvs, session, "Ethernet12", sample_rate="50000") + + samplepacket_keys = dvs.asic_db.wait_for_n_keys("ASIC_STATE:SAI_OBJECT_TYPE_SAMPLEPACKET", 1) + entry = dvs.asic_db.wait_for_entry("ASIC_STATE:SAI_OBJECT_TYPE_SAMPLEPACKET", samplepacket_keys[0]) + assert entry["SAI_SAMPLEPACKET_ATTR_SAMPLE_RATE"] == "50000" + + # Partial SET on the existing key - should be ignored as a duplicate + self.dvs_mirror.config_db.set_field( + "MIRROR_SESSION", session, "sample_rate", "100000") + time.sleep(1) + + # Still exactly one SAMPLEPACKET, rate unchanged + dvs.asic_db.wait_for_n_keys("ASIC_STATE:SAI_OBJECT_TYPE_SAMPLEPACKET", 1) + entry = dvs.asic_db.wait_for_entry("ASIC_STATE:SAI_OBJECT_TYPE_SAMPLEPACKET", samplepacket_keys[0]) + assert entry["SAI_SAMPLEPACKET_ATTR_SAMPLE_RATE"] == "50000" + + # STATE_DB unchanged + state_entry = dvs.state_db.wait_for_entry("MIRROR_SESSION_TABLE", session) + assert state_entry.get("sample_rate") == "50000" + + self._teardown_mirror_session(dvs, session) + + def test_SampledMirrorRejectsWhenSflowBound(self, dvs, testlog): + """ + Test that sampled mirror activation is rejected when sFlow + already has a samplepacket bound on the same source port. + """ + dvs.setup_db() + + # Step 1: Enable sFlow and configure on Ethernet12 + dvs.config_db.create_entry("SFLOW", "global", {"admin_state": "up"}) + dvs.config_db.create_entry("SFLOW_SESSION", "Ethernet12", + {"sample_rate": "1000", "admin_state": "up"}) + + # Wait for sFlow samplepacket to appear in ASIC_DB + dvs.asic_db.wait_for_n_keys( + "ASIC_STATE:SAI_OBJECT_TYPE_SAMPLEPACKET", 1, wait_at_least_n_keys=True) + + # Step 2: Create sampled mirror on same port + self.dvs_mirror.create_erspan_session_sampled( + "CONFLICT_SESSION", "1.1.1.1", "2.2.2.2", "0x8949", "8", "64", "0", + src_ports="Ethernet12", direction="RX", sample_rate="50000") + + # Setup route for ERSPAN destination + dvs.set_interface_status("Ethernet16", "up") + dvs.add_ip_address("Ethernet16", "10.0.0.0/30") + dvs.add_neighbor("Ethernet16", "10.0.0.1", "02:04:06:08:10:12") + dvs.add_route("2.2.2.2", "10.0.0.1") + + # Wait for activation attempt + import time + time.sleep(3) + + # Step 3: Verify mirror session stays inactive (conflict rejected it) + dvs.state_db.wait_for_field_match("MIRROR_SESSION_TABLE", + "CONFLICT_SESSION", + {"status": "inactive"}) + + # Step 4: Verify sFlow samplepacket still exists (not overwritten) + sp_keys = dvs.asic_db.wait_for_n_keys( + "ASIC_STATE:SAI_OBJECT_TYPE_SAMPLEPACKET", 1, wait_at_least_n_keys=True) + sp_entry = dvs.asic_db.wait_for_entry("ASIC_STATE:SAI_OBJECT_TYPE_SAMPLEPACKET", sp_keys[0]) + assert sp_entry["SAI_SAMPLEPACKET_ATTR_SAMPLE_RATE"] == "1000" + + # Cleanup + dvs.remove_route("2.2.2.2") + dvs.remove_neighbor("Ethernet16", "10.0.0.1") + dvs.remove_ip_address("Ethernet16", "10.0.0.0/30") + dvs.set_interface_status("Ethernet16", "down") + self.dvs_mirror.remove_mirror_session("CONFLICT_SESSION") + dvs.config_db.delete_entry("SFLOW_SESSION", "Ethernet12") + dvs.config_db.delete_entry("SFLOW", "global") + time.sleep(2) + dvs.asic_db.wait_for_n_keys("ASIC_STATE:SAI_OBJECT_TYPE_SAMPLEPACKET", 0) + + def test_SampledMirrorOnLag(self, dvs, testlog): + """ + Sampled ERSPAN mirror session with a LAG as src_port. + Verifies that LAG members each get SAMPLEPACKET/SAMPLE_MIRROR_SESSION + bindings (LAG OID itself is not a valid SAI target for these attrs). + Covers MirrorOrch::setUnsetPortMirror LAG dispatch branch. + """ + dvs.setup_db() + + session = "SAMPLED_LAG_SESSION" + po = "001" + po_name = "PortChannel" + po + member1 = "Ethernet0" + member2 = "Ethernet4" + + # create LAG and add two members + self.dvs_lag.create_port_channel(po) + self.dvs_lag.create_port_channel_member(po, member1) + self.dvs_lag.create_port_channel_member(po, member2) + dvs.set_interface_status(po_name, "up") + dvs.set_interface_status(member1, "up") + dvs.set_interface_status(member2, "up") + + # create sampled mirror session with LAG as src_port + self.dvs_mirror.create_erspan_session_sampled( + session, "1.1.1.1", "2.2.2.2", "0x8949", "8", "64", "0", + src_ports=po_name, direction="RX", + sample_rate="50000", truncate_size=None) + + # set up neighbor on a separate PHY port to activate session + dvs.set_interface_status("Ethernet16", "up") + dvs.add_ip_address("Ethernet16", "10.0.0.0/30") + dvs.add_neighbor("Ethernet16", "10.0.0.1", "02:04:06:08:10:12") + dvs.add_route("2.2.2.2", "10.0.0.1") + dvs.state_db.wait_for_field_match("MIRROR_SESSION_TABLE", session, {"status": "active"}) + + # SAMPLEPACKET object should be created + dvs.asic_db.wait_for_n_keys("ASIC_STATE:SAI_OBJECT_TYPE_SAMPLEPACKET", 1) + + # Both LAG members should have INGRESS_SAMPLEPACKET_ENABLE + + # INGRESS_SAMPLE_MIRROR_SESSION attributes set on their port objects. + fvs = dict(dvs.counters_db.get_entry("COUNTERS_PORT_NAME_MAP", "")) + for member in (member1, member2): + port_oid = fvs.get(member) + assert port_oid is not None + entry = dvs.asic_db.wait_for_entry("ASIC_STATE:SAI_OBJECT_TYPE_PORT", port_oid) + assert "SAI_PORT_ATTR_INGRESS_SAMPLEPACKET_ENABLE" in entry + assert "SAI_PORT_ATTR_INGRESS_SAMPLE_MIRROR_SESSION" in entry + + # Cleanup + dvs.remove_route("2.2.2.2") + dvs.remove_neighbor("Ethernet16", "10.0.0.1") + dvs.remove_ip_address("Ethernet16", "10.0.0.0/30") + dvs.set_interface_status("Ethernet16", "down") + self.dvs_mirror.remove_mirror_session(session) + dvs.state_db.wait_for_deleted_entry("MIRROR_SESSION_TABLE", session) + dvs.asic_db.wait_for_n_keys("ASIC_STATE:SAI_OBJECT_TYPE_SAMPLEPACKET", 0) + self.dvs_lag.remove_port_channel_member(po, member1) + self.dvs_lag.remove_port_channel_member(po, member2) + self.dvs_lag.remove_port_channel(po) + + def test_SampledMirrorInactiveNotProgrammed(self, dvs, testlog): + """ + A sampled session must not program the ASIC until its ERSPAN dst is + routable. Verify that while the session is inactive (no route) there + is no SAMPLEPACKET and the source port carries no sampled mirror + bindings, and that installing the route activates and programs it. + """ + session = "SAMPLED_INACTIVE_SESSION" + dvs.setup_db() + + # Create the sampled session but do NOT install the route yet + self.dvs_mirror.create_erspan_session_sampled( + session, "1.1.1.1", "2.2.2.2", "0x8949", "8", "64", "0", + src_ports="Ethernet12", direction="RX", + sample_rate="50000", truncate_size="128") + + # Without a route to the ERSPAN dst the session stays inactive + dvs.state_db.wait_for_field_match("MIRROR_SESSION_TABLE", session, {"status": "inactive"}) + + # Nothing is programmed into the ASIC while inactive + dvs.asic_db.wait_for_n_keys("ASIC_STATE:SAI_OBJECT_TYPE_SAMPLEPACKET", 0) + fvs = dict(dvs.counters_db.get_entry("COUNTERS_PORT_NAME_MAP", "")) + port_oid = fvs.get("Ethernet12") + port_entry = dvs.asic_db.wait_for_entry("ASIC_STATE:SAI_OBJECT_TYPE_PORT", port_oid) + if "SAI_PORT_ATTR_INGRESS_SAMPLEPACKET_ENABLE" in port_entry: + assert port_entry["SAI_PORT_ATTR_INGRESS_SAMPLEPACKET_ENABLE"] == "oid:0x0" + if "SAI_PORT_ATTR_INGRESS_SAMPLE_MIRROR_SESSION" in port_entry: + assert port_entry["SAI_PORT_ATTR_INGRESS_SAMPLE_MIRROR_SESSION"] == "0:null" + + # Installing the route activates the session and programs the ASIC + self._bring_up_route(dvs) + dvs.state_db.wait_for_field_match("MIRROR_SESSION_TABLE", session, {"status": "active"}) + dvs.asic_db.wait_for_n_keys("ASIC_STATE:SAI_OBJECT_TYPE_SAMPLEPACKET", 1) + port_entry = dvs.asic_db.wait_for_entry("ASIC_STATE:SAI_OBJECT_TYPE_PORT", port_oid) + assert "SAI_PORT_ATTR_INGRESS_SAMPLEPACKET_ENABLE" in port_entry + assert "SAI_PORT_ATTR_INGRESS_SAMPLE_MIRROR_SESSION" in port_entry + + self._teardown_mirror_session(dvs, session) + dvs.asic_db.wait_for_n_keys("ASIC_STATE:SAI_OBJECT_TYPE_SAMPLEPACKET", 0) + + def test_SampledMirrorRouteWithdrawReactivate(self, dvs, testlog): + """ + Verify the activation lifecycle when the route to the ERSPAN dst is + withdrawn and later restored while the session still exists. The + SAMPLEPACKET and port bindings must be torn down on route loss and + recreated when the route returns. + """ + session = "SAMPLED_ROUTE_FLAP_SESSION" + self._setup_mirror_session(dvs, session, "Ethernet12", sample_rate="50000") + + # Active: SAMPLEPACKET present and the source port is bound + dvs.asic_db.wait_for_n_keys("ASIC_STATE:SAI_OBJECT_TYPE_SAMPLEPACKET", 1) + fvs = dict(dvs.counters_db.get_entry("COUNTERS_PORT_NAME_MAP", "")) + port_oid = fvs.get("Ethernet12") + port_entry = dvs.asic_db.wait_for_entry("ASIC_STATE:SAI_OBJECT_TYPE_PORT", port_oid) + assert "SAI_PORT_ATTR_INGRESS_SAMPLEPACKET_ENABLE" in port_entry + assert "SAI_PORT_ATTR_INGRESS_SAMPLE_MIRROR_SESSION" in port_entry + + # Withdraw only the route; the session must deactivate and unprogram + self._tear_down_route(dvs) + dvs.state_db.wait_for_field_match("MIRROR_SESSION_TABLE", session, {"status": "inactive"}) + dvs.asic_db.wait_for_n_keys("ASIC_STATE:SAI_OBJECT_TYPE_SAMPLEPACKET", 0) + port_entry = dvs.asic_db.wait_for_entry("ASIC_STATE:SAI_OBJECT_TYPE_PORT", port_oid) + if "SAI_PORT_ATTR_INGRESS_SAMPLEPACKET_ENABLE" in port_entry: + assert port_entry["SAI_PORT_ATTR_INGRESS_SAMPLEPACKET_ENABLE"] == "oid:0x0" + if "SAI_PORT_ATTR_INGRESS_SAMPLE_MIRROR_SESSION" in port_entry: + assert port_entry["SAI_PORT_ATTR_INGRESS_SAMPLE_MIRROR_SESSION"] == "0:null" + + # Restore the route; the session reactivates and reprograms the ASIC + self._bring_up_route(dvs) + dvs.state_db.wait_for_field_match("MIRROR_SESSION_TABLE", session, {"status": "active"}) + dvs.asic_db.wait_for_n_keys("ASIC_STATE:SAI_OBJECT_TYPE_SAMPLEPACKET", 1) + port_entry = dvs.asic_db.wait_for_entry("ASIC_STATE:SAI_OBJECT_TYPE_PORT", port_oid) + assert "SAI_PORT_ATTR_INGRESS_SAMPLEPACKET_ENABLE" in port_entry + assert "SAI_PORT_ATTR_INGRESS_SAMPLE_MIRROR_SESSION" in port_entry + + self._teardown_mirror_session(dvs, session) + dvs.asic_db.wait_for_n_keys("ASIC_STATE:SAI_OBJECT_TYPE_SAMPLEPACKET", 0)