Skip to content

Commit 37352b8

Browse files
yutongzhang-microsoftJavier-Tan
authored andcommitted
Eliminate cross-feature dependency from macsec module (sonic-net#15617)
What is the motivation for this PR? Previously, the common script tests/conftest.py relied on importing a module from the feature-specific macsec folder, creating a cross-feature dependency. To eliminate this dependency and improve code organization, we created a Python package named macsec under the common path tests/common. The shared scripts were refactored and relocated into this new package, ensuring a cleaner and more modular structure. How did you do it? To eliminate this dependency and improve code organization, we created a Python package named macsec under the common path tests/common. The shared scripts were refactored and relocated into this new package, ensuring a cleaner and more modular structure. How did you verify/test it?
1 parent 4f33b0d commit 37352b8

16 files changed

+328
-295
lines changed

tests/common/devices/ptf.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import tempfile
44

55
from tests.common.devices.base import AnsibleHostBase
6-
from tests.macsec.macsec_helper import load_macsec_info
6+
from tests.common.macsec.macsec_helper import load_macsec_info
77

88
logger = logging.getLogger(__name__)
99

tests/common/macsec/__init__.py

Lines changed: 266 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,266 @@
1+
import collections
2+
import json
3+
import logging
4+
import os
5+
import sys
6+
from ipaddress import ip_address, IPv4Address
7+
8+
import natsort
9+
import pytest
10+
11+
if sys.version_info.major > 2:
12+
from pathlib import Path
13+
sys.path.insert(0, str(Path(__file__).parent))
14+
15+
from .macsec_config_helper import enable_macsec_feature
16+
from .macsec_config_helper import disable_macsec_feature
17+
from .macsec_config_helper import setup_macsec_configuration
18+
from .macsec_config_helper import cleanup_macsec_configuration
19+
# flake8: noqa: F401
20+
from tests.common.plugins.sanity_check import sanity_check
21+
22+
logger = logging.getLogger(__name__)
23+
24+
25+
class MacsecPlugin(object):
26+
"""
27+
Pytest macsec plugin
28+
"""
29+
30+
def __init__(self):
31+
with open(os.path.dirname(__file__) + '/profile.json') as f:
32+
self.macsec_profiles = json.load(f)
33+
for k, v in list(self.macsec_profiles.items()):
34+
self.macsec_profiles[k]["name"] = k
35+
# Set default value
36+
if "rekey_period" not in v:
37+
self.macsec_profiles[k]["rekey_period"] = 0
38+
39+
def _generate_macsec_profile(self, metafunc):
40+
value = metafunc.config.getoption("macsec_profile")
41+
if value == 'all':
42+
return natsort.natsorted(list(self.macsec_profiles.keys()))
43+
return [x for x in value.split(',') if x in self.macsec_profiles]
44+
45+
def pytest_generate_tests(self, metafunc):
46+
if 'macsec_profile' in metafunc.fixturenames:
47+
profiles = self._generate_macsec_profile(metafunc)
48+
assert profiles, "Specify valid macsec profile!"
49+
metafunc.parametrize('macsec_profile',
50+
[self.macsec_profiles[x] for x in profiles],
51+
ids=profiles,
52+
scope="module")
53+
54+
def get_ctrl_nbr_names(self, macsec_duthost, nbrhosts, tbinfo):
55+
return NotImplementedError()
56+
57+
def downstream_neighbor(self,tbinfo, neighbor):
58+
return NotImplementedError()
59+
60+
def upstream_neighbor(self,tbinfo, neighbor):
61+
return NotImplementedError()
62+
63+
@pytest.fixture(scope="module")
64+
def start_macsec_service(self, macsec_duthost, macsec_nbrhosts):
65+
def __start_macsec_service():
66+
enable_macsec_feature(macsec_duthost, macsec_nbrhosts)
67+
return __start_macsec_service
68+
69+
@pytest.fixture(scope="module")
70+
def stop_macsec_service(self, macsec_duthost, macsec_nbrhosts):
71+
def __stop_macsec_service():
72+
disable_macsec_feature(macsec_duthost, macsec_nbrhosts)
73+
return __stop_macsec_service
74+
75+
@pytest.fixture(scope="module")
76+
def macsec_feature(self, start_macsec_service, stop_macsec_service):
77+
start_macsec_service()
78+
yield
79+
stop_macsec_service()
80+
81+
@pytest.fixture(scope="module")
82+
def startup_macsec(self, request, macsec_duthost, ctrl_links, macsec_profile, tbinfo):
83+
topo_name = tbinfo['topo']['name']
84+
def __startup_macsec():
85+
profile = macsec_profile
86+
if request.config.getoption("neighbor_type") == "eos":
87+
if macsec_duthost.facts["asic_type"] == "vs" and profile['send_sci'] == "false":
88+
# On EOS, portchannel mac is not same as the member port mac (being as SCI),
89+
# then src mac is not equal to SCI in its sending packet. The receiver of vSONIC
90+
# will drop it for macsec kernel module does not correctly handle it.
91+
pytest.skip(
92+
"macsec on dut vsonic, neighbor eos, send_sci false")
93+
if 't2' not in topo_name:
94+
cleanup_macsec_configuration(macsec_duthost, ctrl_links, profile['name'])
95+
setup_macsec_configuration(macsec_duthost, ctrl_links,
96+
profile['name'], profile['priority'], profile['cipher_suite'],
97+
profile['primary_cak'], profile['primary_ckn'], profile['policy'],
98+
profile['send_sci'], profile['rekey_period'])
99+
logger.info(
100+
"Setup MACsec configuration with arguments:\n{}".format(locals()))
101+
return __startup_macsec
102+
103+
@pytest.fixture(scope="module")
104+
def shutdown_macsec(self, macsec_duthost, ctrl_links, macsec_profile):
105+
def __shutdown_macsec():
106+
profile = macsec_profile
107+
cleanup_macsec_configuration(macsec_duthost, ctrl_links, profile['name'])
108+
return __shutdown_macsec
109+
110+
@pytest.fixture(scope="module", autouse=True)
111+
def macsec_setup(self, startup_macsec, shutdown_macsec, macsec_feature):
112+
'''
113+
setup macsec links
114+
'''
115+
startup_macsec()
116+
yield
117+
shutdown_macsec()
118+
119+
@pytest.fixture(scope="module")
120+
def macsec_nbrhosts(self, ctrl_links):
121+
return {nbr["name"]: nbr for nbr in list(ctrl_links.values())}
122+
123+
@pytest.fixture(scope="module")
124+
def ctrl_links(self, macsec_duthost, tbinfo, nbrhosts):
125+
126+
if not nbrhosts:
127+
topo_name = tbinfo['topo']['name']
128+
pytest.skip("None of neighbors on topology {}".format(topo_name))
129+
130+
ctrl_nbr_names = self.get_ctrl_nbr_names(macsec_duthost, nbrhosts, tbinfo)
131+
logger.info("Controlled links {}".format(ctrl_nbr_names))
132+
nbrhosts = {name: nbrhosts[name] for name in ctrl_nbr_names}
133+
return self.find_links_from_nbr(macsec_duthost, tbinfo, nbrhosts)
134+
135+
@pytest.fixture(scope="module")
136+
def unctrl_links(self, macsec_duthost, tbinfo, nbrhosts, ctrl_links):
137+
unctrl_nbr_names = set(nbrhosts.keys())
138+
for _, nbr in ctrl_links.items():
139+
if nbr["name"] in unctrl_nbr_names:
140+
unctrl_nbr_names.remove(nbr["name"])
141+
142+
logger.info("Uncontrolled links {}".format(unctrl_nbr_names))
143+
nbrhosts = {name: nbrhosts[name] for name in unctrl_nbr_names}
144+
return self.find_links_from_nbr(macsec_duthost, tbinfo, nbrhosts)
145+
146+
@pytest.fixture(scope="module")
147+
def downstream_links(self, macsec_duthost, tbinfo, nbrhosts):
148+
links = collections.defaultdict(dict)
149+
150+
def filter(interface, neighbor, mg_facts, tbinfo):
151+
if self.downstream_neighbor(tbinfo, neighbor):
152+
port = mg_facts["minigraph_neighbors"][interface]["port"]
153+
if interface not in mg_facts["minigraph_ptf_indices"]:
154+
logger.info("Interface {} not in minigraph_ptf_indices".format(interface))
155+
return
156+
links[interface] = {
157+
"name": neighbor["name"],
158+
"ptf_port_id": mg_facts["minigraph_ptf_indices"][interface],
159+
"port": port
160+
}
161+
self.find_links(macsec_duthost, tbinfo, filter)
162+
return links
163+
164+
@pytest.fixture(scope="module")
165+
def upstream_links(self, macsec_duthost, tbinfo, nbrhosts):
166+
links = collections.defaultdict(dict)
167+
168+
def filter(interface, neighbor, mg_facts, tbinfo):
169+
if self.upstream_neighbor(tbinfo, neighbor):
170+
for item in mg_facts["minigraph_bgp"]:
171+
if item["name"] == neighbor["name"]:
172+
if isinstance(ip_address(item["addr"]), IPv4Address):
173+
# The address of neighbor device
174+
local_ipv4_addr = item["addr"]
175+
# The address of DUT
176+
peer_ipv4_addr = item["peer_addr"]
177+
break
178+
if interface not in mg_facts["minigraph_ptf_indices"]:
179+
logger.info("Interface {} not in minigraph_ptf_indices".format(interface))
180+
return
181+
port = mg_facts["minigraph_neighbors"][interface]["port"]
182+
links[interface] = {
183+
"name": neighbor["name"],
184+
"ptf_port_id": mg_facts["minigraph_ptf_indices"][interface],
185+
"local_ipv4_addr": local_ipv4_addr,
186+
"peer_ipv4_addr": peer_ipv4_addr,
187+
"port": port,
188+
"host": nbrhosts[neighbor["name"]]["host"]
189+
}
190+
self.find_links(macsec_duthost, tbinfo, filter)
191+
return links
192+
193+
def find_links(self, duthost, tbinfo, filter):
194+
195+
mg_facts = duthost.get_extended_minigraph_facts(tbinfo)
196+
for interface, neighbor in mg_facts["minigraph_neighbors"].items():
197+
filter(interface, neighbor, mg_facts, tbinfo)
198+
199+
def is_interface_portchannel_member(self, pc, interface):
200+
for pc_name, elements in list(pc.items()):
201+
if interface in elements['members']:
202+
return True
203+
return False
204+
205+
def find_links_from_nbr(self, duthost, tbinfo, nbrhosts):
206+
links = collections.defaultdict(dict)
207+
def filter(interface, neighbor, mg_facts, tbinfo):
208+
if neighbor["name"] not in list(nbrhosts.keys()):
209+
return
210+
port = mg_facts["minigraph_neighbors"][interface]["port"]
211+
212+
links[interface] = {
213+
"name": neighbor["name"],
214+
"host": nbrhosts[neighbor["name"]]["host"],
215+
"port": port,
216+
"dut_name": duthost.hostname
217+
}
218+
self.find_links(duthost, tbinfo, filter)
219+
return links
220+
221+
class MacsecPluginT0(MacsecPlugin):
222+
"""
223+
Pytest macsec plugin
224+
"""
225+
226+
227+
def __init__(self):
228+
super(MacsecPluginT0, self).__init__()
229+
230+
def get_ctrl_nbr_names(self, macsec_duthost, nbrhosts, tbinfo):
231+
ctrl_nbr_names = natsort.natsorted(nbrhosts.keys())[:2]
232+
return ctrl_nbr_names
233+
234+
def downstream_neighbor(self,tbinfo, neighbor):
235+
if (tbinfo["topo"]["type"] == "t0" and "Server" in neighbor["name"]):
236+
return True
237+
return False
238+
239+
def upstream_neighbor(self,tbinfo, neighbor):
240+
if (tbinfo["topo"]["type"] == "t0" and "T1" in neighbor["name"]):
241+
return True
242+
return False
243+
244+
class MacsecPluginT2(MacsecPlugin):
245+
"""
246+
Pytest macsec plugin
247+
"""
248+
249+
250+
def __init__(self):
251+
super(MacsecPluginT2, self).__init__()
252+
253+
def get_ctrl_nbr_names(self, macsec_duthost, nbrhosts, tbinfo):
254+
mg_facts = macsec_duthost.get_extended_minigraph_facts(tbinfo)
255+
ctrl_nbr_names = mg_facts['macsec_neighbors']
256+
return ctrl_nbr_names
257+
258+
def downstream_neighbor(self,tbinfo, neighbor):
259+
if ("t2" in tbinfo["topo"]["type"] and "T1" in neighbor["name"]):
260+
return True
261+
return False
262+
263+
def upstream_neighbor(self,tbinfo, neighbor):
264+
if ("t2" in tbinfo["topo"]["type"] and "T3" in neighbor["name"]):
265+
return True
266+
return False

tests/macsec/macsec_config_helper.py renamed to tests/common/macsec/macsec_config_helper.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import logging
22
import time
33

4-
from .macsec_helper import get_mka_session, getns_prefix, wait_all_complete, submit_async_task
5-
from .macsec_platform_helper import global_cmd, find_portchannel_from_member, get_portchannel
4+
from tests.common.macsec.macsec_helper import get_mka_session, getns_prefix, wait_all_complete, submit_async_task
5+
from tests.common.macsec.macsec_platform_helper import global_cmd, find_portchannel_from_member, get_portchannel
66
from tests.common.devices.eos import EosHost
77
from tests.common.utilities import wait_until
88

Lines changed: 19 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
import scapy.all as scapy
1616
import scapy.contrib.macsec as scapy_macsec
1717

18-
from .macsec_platform_helper import sonic_db_cli
18+
from tests.common.macsec.macsec_platform_helper import sonic_db_cli
1919
from tests.common.devices.eos import EosHost
2020

2121
__all__ = [
@@ -192,15 +192,17 @@ def get_mka_session(host):
192192
'''
193193
Here is an output example of `ip macsec show`
194194
admin@vlab-01:~$ ip macsec show
195-
130: macsec_eth29: protect on validate strict sc off sa off encrypt on send_sci on end_station off scb off replay off
195+
130: macsec_eth29: protect on validate strict sc off sa off encrypt
196+
on send_sci on end_station off scb off replay off
196197
cipher suite: GCM-AES-128, using ICV length 16
197198
TXSC: 52540041303f0001 on SA 0
198199
0: PN 1041, state on, SSCI 16777216, key 0ecddfe0f462491c13400dbf7433465d
199200
3: PN 2044, state off, SSCI 16777216, key 0ecddfe0f462491c13400dbf7433465d
200201
RXSC: 525400b5be690001, state on
201202
0: PN 1041, state on, SSCI 16777216, key 0ecddfe0f462491c13400dbf7433465d
202203
3: PN 0, state on, SSCI 16777216, key 0ecddfe0f462491c13400dbf7433465d
203-
131: macsec_eth30: protect on validate strict sc off sa off encrypt on send_sci on end_station off scb off replay off
204+
131: macsec_eth30: protect on validate strict sc off sa off encrypt
205+
on send_sci on end_station off scb off replay off
204206
cipher suite: GCM-AES-128, using ICV length 16
205207
TXSC: 52540041303f0001 on SA 0
206208
0: PN 1041, state on, key daa8169cde2fe1e238aaa83672e40279
@@ -438,14 +440,16 @@ def macsec_dp_poll(test, device_number=0, port_number=None, timeout=None, exp_pk
438440
ret = __origin_dp_poll(
439441
test, device_number=device_number, port_number=port_number, timeout=timeout, exp_pkt=None)
440442
timeout -= time.time() - start_time
441-
# Since we call __origin_dp_poll with exp_pkt=None, it should only ever fail if no packets are received at all. In this case, continue normally
443+
# Since we call __origin_dp_poll with exp_pkt=None, it should only ever fail if no packets are received at all.
444+
# In this case, continue normally
442445
# until we exceed the timeout value provided to macsec_dp_poll.
443446
if isinstance(ret, test.dataplane.PollFailure):
444447
if timeout <= 0:
445448
break
446449
else:
447450
continue
448-
# The device number of PTF host is 0, if the target port isn't a injected port(belong to ptf host), Don't need to do MACsec further.
451+
# The device number of PTF host is 0, if the target port isn't a injected port(belong to ptf host),
452+
# Don't need to do MACsec further.
449453
if ret.device != 0 or exp_pkt is None:
450454
return ret
451455
pkt = scapy.Ether(ret.packet)
@@ -454,17 +458,22 @@ def macsec_dp_poll(test, device_number=0, port_number=None, timeout=None, exp_pk
454458
if ptf.dataplane.match_exp_pkt(exp_pkt, pkt):
455459
return ret
456460
else:
457-
macsec_info = load_macsec_info(test.duthost, find_portname_from_ptf_id(test.mg_facts, ret.port), force_reload[ret.port])
461+
macsec_info = load_macsec_info(test.duthost, find_portname_from_ptf_id(test.mg_facts, ret.port),
462+
force_reload[ret.port])
458463
if macsec_info:
459464
encrypt, send_sci, xpn_en, sci, an, sak, ssci, salt = macsec_info
460465
force_reload[ret.port] = False
461466
pkt, decap_success = decap_macsec_pkt(pkt, sci, an, sak, encrypt, send_sci, 0, xpn_en, ssci, salt)
462467
if decap_success and ptf.dataplane.match_exp_pkt(exp_pkt, pkt):
463468
return ret
464-
# Normally, if __origin_dp_poll returns a PollFailure, the PollFailure object will contain a list of recently received packets
465-
# to help with debugging. However, since we call __origin_dp_poll multiple times, only the packets from the most recent call is retained.
466-
# If we don't find a matching packet (either with or without MACsec decoding), we need to manually store the packet we received.
467-
# Later if we return a PollFailure, we can provide the received packets to emulate the behavior of __origin_dp_poll.
469+
# Normally, if __origin_dp_poll returns a PollFailure,
470+
# the PollFailure object will contain a list of recently received packets
471+
# to help with debugging. However, since we call __origin_dp_poll multiple times,
472+
# only the packets from the most recent call is retained.
473+
# If we don't find a matching packet (either with or without MACsec decoding),
474+
# we need to manually store the packet we received.
475+
# Later if we return a PollFailure,
476+
# we can provide the received packets to emulate the behavior of __origin_dp_poll.
468477
recent_packets.append(pkt)
469478
packet_count += 1
470479
if timeout <= 0:
File renamed without changes.

tests/conftest.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@
6565
from tests.common.helpers.multi_thread_utils import SafeThreadPoolExecutor
6666

6767
try:
68-
from tests.macsec import MacsecPluginT2, MacsecPluginT0
68+
from tests.common.macsec import MacsecPluginT2, MacsecPluginT0
6969
except ImportError as e:
7070
logging.error(e)
7171

0 commit comments

Comments
 (0)