Skip to content

Commit 484538a

Browse files
[macsec]: Extend MACsec show command to support FIPS POST status option. (#24067)
Why I did it Extended the MACsec show command to display FIPS POST (Power-On Self-Test) status, enabling network operators to verify cryptographic module compliance and operational readiness in FIPS enabled deployments. Reference HLD: sonic-net/SONiC#2034. How I did it Added --post-status option to existing show macsec command Read and display information from FIPS_MACSEC_POST_TABLE|<module> keys Added unit tests to verify the changes. Updated macsec show command structure show macsec [--profile/--dump-file/--post-status] [interface_name]
1 parent c260bbf commit 484538a

File tree

2 files changed

+321
-4
lines changed

2 files changed

+321
-4
lines changed

dockers/docker-macsec/cli-plugin-tests/test_show_macsec.py

Lines changed: 249 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import sys
2-
from unittest import mock
2+
from unittest.mock import MagicMock, patch
33

44
from click.testing import CliRunner
55

@@ -9,7 +9,7 @@
99

1010
class TestShowMACsec(object):
1111
def test_plugin_registration(self):
12-
cli = mock.MagicMock()
12+
cli = MagicMock()
1313
show_macsec.register(cli)
1414
cli.add_command.assert_called_once_with(show_macsec.macsec)
1515

@@ -27,3 +27,250 @@ def test_show_profile(self):
2727
runner = CliRunner()
2828
result = runner.invoke(show_macsec.macsec,["--profile"])
2929
assert result.exit_code == 0, "exit code: {}, Exception: {}, Traceback: {}".format(result.exit_code, result.exception, result.exc_info)
30+
31+
@patch('show_macsec.SonicV2Connector')
32+
def test_post_status_success(self, mock_connector):
33+
"""Test --post-status command with successful data"""
34+
with patch('show_macsec.multi_asic_util.MultiAsic') as mock_multi_asic_class:
35+
# Setup MultiAsic mock
36+
mock_multi_asic_instance = MagicMock()
37+
mock_multi_asic_instance.is_multi_asic.return_value = False
38+
mock_multi_asic_instance.get_ns_list_based_on_options.return_value = ['']
39+
mock_multi_asic_class.return_value = mock_multi_asic_instance
40+
41+
# Setup database mock
42+
def mock_connector_side_effect(use_unix_socket_path=True, namespace=None):
43+
mock_db = MagicMock()
44+
45+
if not namespace or namespace == '':
46+
mock_db.keys.return_value = ['FIPS_MACSEC_POST_TABLE|crypto', 'FIPS_MACSEC_POST_TABLE|sai']
47+
# Mock get_all for different modules
48+
def mock_get_all(db_name, key):
49+
if key == "FIPS_MACSEC_POST_TABLE|crypto":
50+
return {
51+
'status': 'pass',
52+
'timestamp': '2025-09-15 10:30:00 UTC',
53+
}
54+
elif key == "FIPS_MACSEC_POST_TABLE|sai":
55+
return {
56+
'status': 'fail',
57+
'timestamp': '2025-09-15 10:31:30 UTC',
58+
}
59+
return {}
60+
mock_db.get_all.side_effect = mock_get_all
61+
else:
62+
mock_db.keys.return_value = []
63+
mock_db.get_all.return_value = {}
64+
65+
return mock_db
66+
mock_connector.side_effect = mock_connector_side_effect
67+
68+
# Test the CLI
69+
runner = CliRunner()
70+
result = runner.invoke(show_macsec.macsec, ["--post-status"])
71+
72+
# Assertions
73+
assert result.exit_code == 0
74+
assert "Module : crypto" in result.output
75+
assert "Module : sai" in result.output
76+
assert "Status : pass" in result.output
77+
assert "Status : fail" in result.output
78+
79+
@patch('show_macsec.SonicV2Connector')
80+
def test_post_status_no_entries(self, mock_connector):
81+
"""Test --post-status command when no POST entries exist"""
82+
with patch('show_macsec.multi_asic_util.MultiAsic') as mock_multi_asic_class:
83+
# Mock the database connection
84+
# Setup MultiAsic mock
85+
mock_multi_asic_instance = MagicMock()
86+
mock_multi_asic_instance.is_multi_asic.return_value = False
87+
mock_multi_asic_instance.get_ns_list_based_on_options.return_value = ['']
88+
mock_multi_asic_class.return_value = mock_multi_asic_instance
89+
90+
# Create separate mock instances for different database connections
91+
def mock_connector_side_effect(use_unix_socket_path=True, namespace=None):
92+
mock_db = MagicMock()
93+
mock_db.keys.return_value = []
94+
mock_connector.return_value = mock_db
95+
# Return empty dict for any get_all call (no POST data)
96+
mock_db.get_all.return_value = {}
97+
return mock_db
98+
99+
mock_connector.side_effect = mock_connector_side_effect
100+
101+
runner = CliRunner()
102+
result = runner.invoke(show_macsec.macsec, ["--post-status"])
103+
104+
assert result.exit_code == 0
105+
assert "No entries found" in result.output
106+
107+
def test_post_status_mutual_exclusivity(self):
108+
"""Test that --post-status and other option/argument are mutually exclusive"""
109+
runner = CliRunner()
110+
result = runner.invoke(show_macsec.macsec, ["Ethernet0", "--post-status"])
111+
112+
assert result.exit_code == 0
113+
assert "POST status is not valid with other options/arguments" in result.output
114+
115+
result = runner.invoke(show_macsec.macsec, ["--profile", "--post-status"])
116+
117+
assert result.exit_code == 0
118+
assert "POST status is not valid with other options/arguments" in result.output
119+
120+
result = runner.invoke(show_macsec.macsec, ["--dump-file", "--post-status"])
121+
122+
assert result.exit_code == 0
123+
assert "POST status is not valid with other options/arguments" in result.output
124+
125+
result = runner.invoke(show_macsec.macsec, ["--profile", "--post-status"])
126+
127+
assert result.exit_code == 0
128+
assert "POST status is not valid with other options/arguments" in result.output
129+
130+
result = runner.invoke(show_macsec.macsec, ["Ethernet0", "--profile", "--post-status"])
131+
132+
assert result.exit_code == 0
133+
assert "POST status is not valid with other options/arguments" in result.output
134+
135+
@patch('show_macsec.SonicV2Connector')
136+
def test_post_status_multi_asic_success(self, mock_connector):
137+
"""Test --post-status command in multi-ASIC environment with successful data"""
138+
with patch('show_macsec.multi_asic_util.MultiAsic') as mock_multi_asic_class:
139+
mock_multi_asic_instance = MagicMock()
140+
mock_multi_asic_instance.is_multi_asic.return_value = True
141+
mock_multi_asic_instance.get_ns_list_based_on_options.return_value = ['', 'asic0', 'asic1']
142+
mock_multi_asic_class.return_value = mock_multi_asic_instance
143+
144+
# Setup database mock
145+
def mock_connector_side_effect(use_unix_socket_path=True, namespace=None):
146+
mock_db = MagicMock()
147+
148+
if namespace == 'asic0':
149+
mock_db.keys.return_value = ['FIPS_MACSEC_POST_TABLE|sai']
150+
mock_db.get_all.return_value = {'status': 'fail', 'timestamp': '2025-09-15 10:31:00 UTC'}
151+
elif namespace == 'asic1':
152+
mock_db.keys.return_value = ['FIPS_MACSEC_POST_TABLE|sai']
153+
mock_db.get_all.return_value = {'status': 'inprogress', 'timestamp': '2025-09-15 10:31:30 UTC'}
154+
elif not namespace or namespace == '':
155+
mock_db.keys.return_value = ['FIPS_MACSEC_POST_TABLE|crypto', 'FIPS_MACSEC_POST_TABLE|sai']
156+
# Mock get_all for different modules
157+
def mock_get_all(db_name, key):
158+
if key == "FIPS_MACSEC_POST_TABLE|crypto":
159+
return {
160+
'status': 'pass',
161+
'timestamp': '2025-09-15 10:30:00 UTC',
162+
}
163+
elif key == "FIPS_MACSEC_POST_TABLE|sai":
164+
return {
165+
'status': 'pass',
166+
'timestamp': '2025-09-15 10:31:30 UTC',
167+
}
168+
return {}
169+
mock_db.get_all.side_effect = mock_get_all
170+
else:
171+
mock_db.keys.return_value = []
172+
mock_db.get_all.return_value = {}
173+
174+
return mock_db
175+
176+
mock_connector.side_effect = mock_connector_side_effect
177+
178+
# Test the CLI
179+
runner = CliRunner()
180+
result = runner.invoke(show_macsec.macsec, ["--post-status"])
181+
182+
# Assertions
183+
assert result.exit_code == 0
184+
assert "Module : crypto" in result.output
185+
assert "Module : sai" in result.output
186+
assert "Namespace (asic0)" in result.output
187+
assert "Namespace (asic1)" in result.output
188+
assert "Status : pass" in result.output
189+
assert "Status : fail" in result.output
190+
assert "Status : inprogress" in result.output
191+
192+
@patch('show_macsec.SonicV2Connector')
193+
def test_post_status_multi_asic_specific_namespace(self, mock_connector):
194+
"""Test --post-status command with specific namespace filter"""
195+
with patch('show_macsec.multi_asic_util.MultiAsic') as mock_multi_asic_class:
196+
mock_multi_asic_instance = MagicMock()
197+
mock_multi_asic_instance.is_multi_asic.return_value = True
198+
mock_multi_asic_instance.get_ns_list_based_on_options.return_value = ['asic1']
199+
mock_multi_asic_class.return_value = mock_multi_asic_instance
200+
201+
# Mock database to return keys and data only for asic1
202+
def mock_connector_side_effect(use_unix_socket_path=True, namespace=None):
203+
mock_db = MagicMock()
204+
205+
if namespace == 'asic1':
206+
# Only asic1 should have data when filtered
207+
mock_db.keys.return_value = ['FIPS_MACSEC_POST_TABLE|sai']
208+
# Mock get_all for different modules
209+
def mock_get_all(db_name, key):
210+
if key == "FIPS_MACSEC_POST_TABLE|sai":
211+
return {
212+
'status': 'pass',
213+
'timestamp': '2025-09-15 10:31:00 UTC',
214+
}
215+
return {}
216+
mock_db.get_all.side_effect = mock_get_all
217+
else:
218+
# No keys for other namespaces (they shouldn't be called with filtering)
219+
mock_db.keys.return_value = []
220+
mock_db.get_all.return_value = {}
221+
222+
return mock_db
223+
224+
mock_connector.side_effect = mock_connector_side_effect
225+
226+
# Test the new approach with namespace filtering
227+
runner = CliRunner()
228+
result = runner.invoke(show_macsec.macsec, ["--post-status"])
229+
230+
assert result.exit_code == 0
231+
# Check that modules are displayed for asic1 only
232+
assert "Module : sai" in result.output
233+
assert "Status : pass" in result.output
234+
assert "Namespace (asic1)" in result.output
235+
236+
@patch('show_macsec.SonicV2Connector')
237+
def test_post_status_multi_asic_partial_entries(self, mock_connector):
238+
"""Test --post-status command when no POST data is available"""
239+
# Setup MultiAsic mock
240+
with patch('show_macsec.multi_asic_util.MultiAsic') as mock_multi_asic_class:
241+
mock_multi_asic_instance = MagicMock()
242+
mock_multi_asic_instance.is_multi_asic.return_value = True
243+
mock_multi_asic_instance.get_ns_list_based_on_options.return_value = ['', 'asic0', 'asic1']
244+
mock_multi_asic_class.return_value = mock_multi_asic_instance
245+
def mock_connector_side_effect(use_unix_socket_path=True, namespace=None):
246+
mock_db = MagicMock()
247+
if namespace == 'asic0':
248+
# Only asic1 should have data when filtered
249+
mock_db.keys.return_value = ['FIPS_MACSEC_POST_TABLE|sai']
250+
# Mock get_all for different modules
251+
def mock_get_all(db_name, key):
252+
if key == "FIPS_MACSEC_POST_TABLE|sai":
253+
return {
254+
'status': 'pass',
255+
'timestamp': '2025-09-15 10:31:00 UTC',
256+
}
257+
return {}
258+
mock_db.get_all.side_effect = mock_get_all
259+
else:
260+
# No keys for other namespaces (they shouldn't be called with filtering)
261+
mock_db.keys.return_value = []
262+
mock_db.get_all.return_value = {}
263+
264+
return mock_db
265+
266+
mock_connector.side_effect = mock_connector_side_effect
267+
268+
# Test the CLI command
269+
runner = CliRunner()
270+
result = runner.invoke(show_macsec.macsec, ["--post-status"])
271+
272+
assert result.exit_code == 0
273+
assert "No entries found" in result.output
274+
assert "Module : sai" in result.output
275+
assert "Status : pass" in result.output
276+
assert "Namespace (asic1)" in result.output

dockers/docker-macsec/cli/show/plugins/show_macsec.py

Lines changed: 72 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
from tabulate import tabulate
1010

1111
import utilities_common.multi_asic as multi_asic_util
12-
from swsscommon.swsscommon import CounterTable, MacsecCounter
12+
from swsscommon.swsscommon import CounterTable, MacsecCounter, SonicV2Connector
1313
from utilities_common.cli import UserCache
1414

1515
CACHE_MANAGER = UserCache(app_name="macsec")
@@ -266,8 +266,16 @@ def cache_find(cache: dict, target: MACsecAppMeta) -> MACsecAppMeta:
266266
@click.argument('interface_name', required=False)
267267
@click.option('--profile', is_flag=True, required=False, default=False, help="show all macsec profiles")
268268
@click.option('--dump-file', is_flag=True, required=False, default=False, help="store show output to a file")
269+
@click.option('--post-status', is_flag=True, required=False, default=False, help="show macsec FIPS POST(Pre-Operational Self-Test) status")
269270
@multi_asic_util.multi_asic_click_options
270-
def macsec(interface_name, dump_file, namespace, display, profile):
271+
def macsec(interface_name, dump_file, namespace, display, profile, post_status):
272+
if post_status:
273+
if interface_name is not None or profile or dump_file:
274+
click.echo('POST status is not valid with other options/arguments')
275+
return
276+
MacsecContext(namespace, display).show_post_status()
277+
return
278+
271279
if interface_name is not None and profile:
272280
click.echo('Interface name is not valid with profile option')
273281
return
@@ -329,6 +337,68 @@ def show(self, interface_name, dump_file, profile):
329337
pickle.dump(dump_obj, dump_file)
330338
dump_file.flush()
331339

340+
@multi_asic_util.run_on_multi_asic
341+
def show_post_status(self):
342+
"""Show POST (Pre-Operational Self-Test) status"""
343+
# Define the table name
344+
table_name = "FIPS_MACSEC_POST_TABLE"
345+
346+
def format_module_status(module, namespace):
347+
# Get all fields from the table
348+
post_data = state_db.get_all("STATE_DB", table_name+"|"+module)
349+
350+
# Format according to SONiC CLI guidelines
351+
output = []
352+
indent = " " if namespace else ""
353+
output.append(f"{indent}{'Module'.ljust(11)} : {module}")
354+
355+
for field in post_data:
356+
value = post_data[field]
357+
# Format field name for consistent alignment (capitalize and pad to 11 chars)
358+
display_name = field.capitalize().ljust(11)
359+
output.append(f"{indent}{display_name} : {value}")
360+
361+
return "\n".join(output)
362+
363+
namespace = self.multi_asic.current_namespace
364+
# Connect to STATE_DB
365+
state_db = SonicV2Connector(use_unix_socket_path=True, namespace=namespace)
366+
state_db.connect(state_db.STATE_DB)
367+
368+
# Get all keys in the FIPS_MACSEC_POST_TABLE
369+
all_keys = state_db.keys(state_db.STATE_DB, table_name + "|*")
370+
if not len(all_keys):
371+
click.echo("") # Add blank line for separation
372+
if namespace:
373+
click.echo(f"Namespace ({namespace})")
374+
click.echo(" No entries found")
375+
else:
376+
click.echo("No entries found")
377+
return
378+
379+
# Extract module names from the keys and sort them
380+
modules = []
381+
for key in all_keys:
382+
# Key format is "FIPS_MACSEC_POST_TABLE|module_name"
383+
if "|" in key:
384+
module = key.split("|", 1)[1]
385+
modules.append(module)
386+
387+
# Sort modules for consistent output
388+
modules.sort()
389+
390+
display_output = [""]
391+
if namespace:
392+
display_output.append(f"Namespace ({namespace})")
393+
for i, module in enumerate(modules):
394+
if i > 0:
395+
display_output.append("") # Add separator between modules
396+
module_output = format_module_status(module, namespace)
397+
display_output.append(module_output)
398+
399+
if display_output:
400+
click.echo("\n".join(display_output))
401+
332402
def register(cli):
333403
cli.add_command(macsec)
334404

0 commit comments

Comments
 (0)