From 6603d9bb9c19bc7d120841a0b8dcbc49327a742e Mon Sep 17 00:00:00 2001 From: Shauli Taragin Date: Wed, 15 Jul 2026 16:47:44 +0300 Subject: [PATCH] add config sed wipe-ssd cli Signed-off-by: Shauli Taragin --- config/sed.py | 44 +++++++++++++++++++ doc/Command-Reference.md | 32 ++++++++++++++ tests/sed_test.py | 94 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 170 insertions(+) diff --git a/config/sed.py b/config/sed.py index ddc7df074e1..e8181fbc229 100644 --- a/config/sed.py +++ b/config/sed.py @@ -50,3 +50,47 @@ def reset_password(): click.echo("Error: SED password reset failed") except Exception as e: click.echo(f"Error resetting SED password: {str(e)}") + + +WIPE_SSD_START_BANNER = ( + "=========================================================================\n" + " SSD ERASE STARTED\n" + " * Do NOT power off the switch or interrupt this session.\n" + " * The wipe runs from a RAM-disk and will keep going even if SSH drops.\n" + " * Follow progress in syslog: journalctl -f -t ssd_erase.sh\n" + " * When it finishes, reboot with: sudo /sbin/reboot\n" + "=========================================================================" +) + + +@sed.command('wipe-ssd') +@click.option('-y', '--yes', is_flag=True, default=False, + help='Skip the interactive confirmation prompt.') +def wipe_ssd(yes): + """Securely wipe the boot SSD (SED PSID revert + NVMe sanitize). + + IRREVERSIBLE: after wipe the switch cannot boot until re-imaged. + """ + try: + from sonic_platform import platform + chassis = platform.Platform().get_chassis() + sed_mgmt = chassis.get_sed_mgmt() + if sed_mgmt is None: + click.echo("Error: SED management not supported on this platform") + return + if not yes: + click.confirm( + 'This will PERMANENTLY erase the SSD. Continue?', + default=False, + abort=True, + ) + click.echo(WIPE_SSD_START_BANNER) + success = sed_mgmt.wipe_ssd() + if success: + click.echo("SSD wipe completed successfully. Reboot now with `sudo /sbin/reboot`.") + else: + click.echo("Error: SSD wipe failed") + except click.exceptions.Abort: + raise + except Exception as e: + click.echo(f"Error wiping SSD: {str(e)}") diff --git a/doc/Command-Reference.md b/doc/Command-Reference.md index 38dcec36609..ac593a26f04 100644 --- a/doc/Command-Reference.md +++ b/doc/Command-Reference.md @@ -13102,6 +13102,38 @@ This command resets the SED password to the default value. SED password reset process completed successfully ``` +**config sed wipe-ssd** + +This command performs a graceful SSD wipe: SED PSID revert (crypto erase) followed by NVMe sanitize (block erase). The operation is **irreversible** - after wipe the switch cannot boot until re-imaged (ONIE / rescue image). + +The wipe runs from a RAM-disk after the OS root is pivoted, so it survives an SSH disconnect. Follow progress in syslog via `journalctl -f -t ssd_erase.sh`. + +- Usage: + + ```console + config sed wipe-ssd [-y | --yes] + ``` + +- Options: + - `-y, --yes` : skip the interactive confirmation prompt (for automation). + +- Example (interactive): + + ```console + admin@sonic:~$ config sed wipe-ssd + This will PERMANENTLY erase the SSD. Continue? [y/N]: y + ========================================================================= + SSD ERASE STARTED + * Do NOT power off the switch or interrupt this session. + * The wipe runs from a RAM-disk and will keep going even if SSH drops. + * Follow progress in syslog: journalctl -f -t ssd_erase.sh + * When it finishes, reboot with: sudo /sbin/reboot + ========================================================================= + SSD wipe completed successfully. Reboot now with `sudo /sbin/reboot`. + ``` + +After the wipe completes, reboot with `sudo /sbin/reboot` (or via BMC / power cycle). The full SONiC `/usr/local/bin/reboot` script is not supported after wipe because it depends on `/host` and Redis, which no longer exist. + Go Back To [Beginning of the document](#) or [Beginning of this section](#sed) ## SNMP diff --git a/tests/sed_test.py b/tests/sed_test.py index 6402f29a369..4ac00dc7f5b 100644 --- a/tests/sed_test.py +++ b/tests/sed_test.py @@ -130,3 +130,97 @@ def test_reset_password_exception(self, mock_platform): [] ) assert "Error resetting SED password: Reset test error" in result.output + + @patch('sonic_platform.platform.Platform') + def test_wipe_ssd_success_confirm_yes(self, mock_platform): + """wipe-ssd success path with interactive 'y' confirmation.""" + runner = CliRunner() + mock_chassis = MagicMock() + mock_sed_mgmt = MagicMock() + mock_sed_mgmt.wipe_ssd.return_value = True + mock_chassis.get_sed_mgmt.return_value = mock_sed_mgmt + mock_platform.return_value.get_chassis.return_value = mock_chassis + result = runner.invoke( + config.config.commands["sed"].commands["wipe-ssd"], + [], + input='y\n', + ) + assert "SSD ERASE STARTED" in result.output + assert "SSD wipe completed successfully" in result.output + mock_sed_mgmt.wipe_ssd.assert_called_once() + + @patch('sonic_platform.platform.Platform') + def test_wipe_ssd_confirm_abort(self, mock_platform): + """wipe-ssd aborts and does NOT call wipe_ssd when user answers 'n'.""" + runner = CliRunner() + mock_chassis = MagicMock() + mock_sed_mgmt = MagicMock() + mock_chassis.get_sed_mgmt.return_value = mock_sed_mgmt + mock_platform.return_value.get_chassis.return_value = mock_chassis + result = runner.invoke( + config.config.commands["sed"].commands["wipe-ssd"], + [], + input='n\n', + ) + mock_sed_mgmt.wipe_ssd.assert_not_called() + # click.confirm(abort=True) exits non-zero on abort. + assert result.exit_code != 0 + + @patch('sonic_platform.platform.Platform') + def test_wipe_ssd_yes_flag_skips_prompt(self, mock_platform): + """wipe-ssd with -y skips the confirmation prompt.""" + runner = CliRunner() + mock_chassis = MagicMock() + mock_sed_mgmt = MagicMock() + mock_sed_mgmt.wipe_ssd.return_value = True + mock_chassis.get_sed_mgmt.return_value = mock_sed_mgmt + mock_platform.return_value.get_chassis.return_value = mock_chassis + result = runner.invoke( + config.config.commands["sed"].commands["wipe-ssd"], + ['--yes'], + ) + assert "SSD wipe completed successfully" in result.output + mock_sed_mgmt.wipe_ssd.assert_called_once() + + @patch('sonic_platform.platform.Platform') + def test_wipe_ssd_failure(self, mock_platform): + """wipe-ssd reports failure when SedMgmt.wipe_ssd returns False.""" + runner = CliRunner() + mock_chassis = MagicMock() + mock_sed_mgmt = MagicMock() + mock_sed_mgmt.wipe_ssd.return_value = False + mock_chassis.get_sed_mgmt.return_value = mock_sed_mgmt + mock_platform.return_value.get_chassis.return_value = mock_chassis + result = runner.invoke( + config.config.commands["sed"].commands["wipe-ssd"], + ['--yes'], + ) + assert "Error: SSD wipe failed" in result.output + + @patch('sonic_platform.platform.Platform') + def test_wipe_ssd_not_supported(self, mock_platform): + """wipe-ssd exits cleanly when get_sed_mgmt() returns None.""" + runner = CliRunner() + mock_chassis = MagicMock() + mock_chassis.get_sed_mgmt.return_value = None + mock_platform.return_value.get_chassis.return_value = mock_chassis + result = runner.invoke( + config.config.commands["sed"].commands["wipe-ssd"], + ['--yes'], + ) + assert "Error: SED management not supported on this platform" in result.output + + @patch('sonic_platform.platform.Platform') + def test_wipe_ssd_exception(self, mock_platform): + """wipe-ssd surfaces unexpected exceptions.""" + runner = CliRunner() + mock_chassis = MagicMock() + mock_sed_mgmt = MagicMock() + mock_sed_mgmt.wipe_ssd.side_effect = Exception("boom") + mock_chassis.get_sed_mgmt.return_value = mock_sed_mgmt + mock_platform.return_value.get_chassis.return_value = mock_chassis + result = runner.invoke( + config.config.commands["sed"].commands["wipe-ssd"], + ['--yes'], + ) + assert "Error wiping SSD: boom" in result.output