From 723468136c3716d4d5b5f801af7a5916fb3ee9af Mon Sep 17 00:00:00 2001 From: xumia <59720581+xumia@users.noreply.github.com> Date: Fri, 8 Jul 2022 16:29:09 +0800 Subject: [PATCH] Support to enable fips for the command sonic_installer (#2154) --- sonic_installer/bootloader/aboot.py | 16 ++++++ sonic_installer/bootloader/bootloader.py | 8 +++ sonic_installer/bootloader/grub.py | 39 ++++++++++++++ sonic_installer/bootloader/uboot.py | 16 ++++++ sonic_installer/main.py | 32 ++++++++++++ tests/installer_bootloader_aboot_test.py | 32 ++++++++++++ tests/installer_bootloader_grub_test.py | 34 +++++++++++++ .../host/grub/grub.cfg | 51 +++++++++++++++++++ tests/installer_bootloader_uboot_test.py | 42 +++++++++++++++ tests/test_sonic_installer.py | 35 +++++++++++++ 10 files changed, 305 insertions(+) create mode 100644 tests/installer_bootloader_aboot_test.py create mode 100644 tests/installer_bootloader_grub_test.py create mode 100644 tests/installer_bootloader_input/host/grub/grub.cfg create mode 100644 tests/installer_bootloader_uboot_test.py create mode 100644 tests/test_sonic_installer.py diff --git a/sonic_installer/bootloader/aboot.py b/sonic_installer/bootloader/aboot.py index ab4c0ff38c..8884ab5434 100644 --- a/sonic_installer/bootloader/aboot.py +++ b/sonic_installer/bootloader/aboot.py @@ -146,6 +146,11 @@ def _get_image_cmdline(self, image): with open(os.path.join(image_path, KERNEL_CMDLINE_NAME)) as f: return f.read() + def _set_image_cmdline(self, image, cmdline): + image_path = self.get_image_path(image) + with open(os.path.join(image_path, KERNEL_CMDLINE_NAME), 'w') as f: + return f.write(cmdline) + def supports_package_migration(self, image): if is_secureboot(): # NOTE: unsafe until migration can guarantee migration safety @@ -204,6 +209,17 @@ def verify_next_image(self): image_path = os.path.join(self.get_image_path(image), DEFAULT_SWI_IMAGE) return self._verify_secureboot_image(image_path) + def set_fips(self, image, enable): + fips = "1" if enable else "0" + cmdline = self._get_image_cmdline(image) + cmdline = re.sub(r' sonic_fips=[^\s]', '', cmdline) + " sonic_fips=" + fips + self._set_image_cmdline(image, cmdline) + click.echo('Done') + + def get_fips(self, image): + cmdline = self._get_image_cmdline(image) + return 'sonic_fips=1' in cmdline + def _verify_secureboot_image(self, image_path): if is_secureboot(): cert = self.getCert(image_path) diff --git a/sonic_installer/bootloader/bootloader.py b/sonic_installer/bootloader/bootloader.py index aaeddeba2f..d0919cdd50 100644 --- a/sonic_installer/bootloader/bootloader.py +++ b/sonic_installer/bootloader/bootloader.py @@ -57,6 +57,14 @@ def verify_secureboot_image(self, image_path): """verify that the image is secure running image""" raise NotImplementedError + def set_fips(self, image, enable): + """set fips""" + raise NotImplementedError + + def get_fips(self, image): + """returns true if fips set""" + raise NotImplementedError + def verify_next_image(self): """verify the next image for reboot""" image = self.get_next_image() diff --git a/sonic_installer/bootloader/grub.py b/sonic_installer/bootloader/grub.py index 11ee3de1f4..9a00800be5 100644 --- a/sonic_installer/bootloader/grub.py +++ b/sonic_installer/bootloader/grub.py @@ -85,6 +85,45 @@ def remove_image(self, image): run_command('grub-set-default --boot-directory=' + HOST_PATH + ' 0') click.echo('Image removed') + def get_linux_cmdline(self, image): + cmdline = None + config = open(HOST_PATH + '/grub/grub.cfg', 'r') + old_config = config.read() + menuentry = re.search("menuentry '" + image + "[^}]*}", old_config).group() + config.close() + for line in menuentry.split('\n'): + line = line.strip() + if line.startswith('linux '): + cmdline = line[6:].strip() + break + return cmdline + + def set_linux_cmdline(self, image, cmdline): + config = open(HOST_PATH + '/grub/grub.cfg', 'r') + old_config = config.read() + old_menuentry = re.search("menuentry '" + image + "[^}]*}", old_config).group() + config.close() + new_menuentry = old_menuentry + for line in old_menuentry.split('\n'): + line = line.strip() + if line.startswith('linux '): + new_menuentry = old_menuentry.replace(line, "linux " + cmdline) + break + config = open(HOST_PATH + '/grub/grub.cfg', 'w') + config.write(old_config.replace(old_menuentry, new_menuentry)) + config.close() + + def set_fips(self, image, enable): + fips = "1" if enable else "0" + cmdline = self.get_linux_cmdline(image) + cmdline = re.sub(r' sonic_fips=[^\s]', '', cmdline) + " sonic_fips=" + fips + self.set_linux_cmdline(image, cmdline) + click.echo('Done') + + def get_fips(self, image): + cmdline = self.get_linux_cmdline(image) + return 'sonic_fips=1' in cmdline + def platform_in_platforms_asic(self, platform, image_path): """ For those images that don't have devices list builtin, 'tar' will have non-zero returncode. diff --git a/sonic_installer/bootloader/uboot.py b/sonic_installer/bootloader/uboot.py index bc4b98daeb..ef4491bd5d 100644 --- a/sonic_installer/bootloader/uboot.py +++ b/sonic_installer/bootloader/uboot.py @@ -5,6 +5,7 @@ import platform import subprocess import os +import re import click @@ -81,6 +82,21 @@ def remove_image(self, image): def verify_image_platform(self, image_path): return os.path.isfile(image_path) + def set_fips(self, image, enable): + fips = "1" if enable else "0" + proc = subprocess.Popen("/usr/bin/fw_printenv linuxargs", shell=True, text=True, stdout=subprocess.PIPE) + (out, _) = proc.communicate() + cmdline = out.strip() + cmdline = re.sub('^linuxargs=', '', cmdline) + cmdline = re.sub(r' sonic_fips=[^\s]', '', cmdline) + " sonic_fips=" + fips + run_command('/usr/bin/fw_setenv linuxargs ' + cmdline) + click.echo('Done') + + def get_fips(self, image): + proc = subprocess.Popen("/usr/bin/fw_printenv linuxargs", shell=True, text=True, stdout=subprocess.PIPE) + (out, _) = proc.communicate() + return 'sonic_fips=1' in out + @classmethod def detect(cls): arch = platform.machine() diff --git a/sonic_installer/main.py b/sonic_installer/main.py index 1aaec8054e..f9b919abf9 100644 --- a/sonic_installer/main.py +++ b/sonic_installer/main.py @@ -616,6 +616,38 @@ def set_next_boot(image): sys.exit(1) bootloader.set_next_image(image) +# Set fips for image +@sonic_installer.command('set-fips') +@click.argument('image', required=False) +@click.option('--enable-fips/--disable-fips', is_flag=True, default=True, + help="Enable or disable FIPS, the default value is to enable FIPS") +def set_fips(image, enable_fips): + """ Set fips for the image """ + bootloader = get_bootloader() + if not image: + image = bootloader.get_next_image() + if image not in bootloader.get_installed_images(): + echo_and_log('Error: Image does not exist', LOG_ERR) + sys.exit(1) + bootloader.set_fips(image, enable=enable_fips) + click.echo('Set FIPS for the image successfully') + +# Get fips for image +@sonic_installer.command('get-fips') +@click.argument('image', required=False) +def get_fips(image): + """ Get the fips enabled or disabled status for the image """ + bootloader = get_bootloader() + if not image: + image = bootloader.get_next_image() + if image not in bootloader.get_installed_images(): + echo_and_log('Error: Image does not exist', LOG_ERR) + sys.exit(1) + enable = bootloader.get_fips(image) + if enable: + click.echo("FIPS is enabled") + else: + click.echo("FIPS is disabled") # Uninstall image @sonic_installer.command('remove') diff --git a/tests/installer_bootloader_aboot_test.py b/tests/installer_bootloader_aboot_test.py new file mode 100644 index 0000000000..15d2dc1121 --- /dev/null +++ b/tests/installer_bootloader_aboot_test.py @@ -0,0 +1,32 @@ +from unittest.mock import Mock, patch + +# Import test module +import sonic_installer.bootloader.aboot as aboot +import tempfile +import shutil + +# Constants +image_dir = f'{aboot.IMAGE_DIR_PREFIX}expeliarmus-{aboot.IMAGE_DIR_PREFIX}abcde' +exp_image = f'{aboot.IMAGE_PREFIX}expeliarmus-{aboot.IMAGE_DIR_PREFIX}abcde' +image_dirs = [image_dir] + +def test_set_fips_aboot(): + image = 'test-image' + dirpath = tempfile.mkdtemp() + bootloader = aboot.AbootBootloader() + bootloader.get_image_path = Mock(return_value=dirpath) + + # The the default setting + bootloader._set_image_cmdline(image, 'test=1') + assert not bootloader.get_fips(image) + + # Test fips enabled + bootloader.set_fips(image, True) + assert bootloader.get_fips(image) + + # Test fips disabled + bootloader.set_fips(image, False) + assert not bootloader.get_fips(image) + + # Cleanup + shutil.rmtree(dirpath) diff --git a/tests/installer_bootloader_grub_test.py b/tests/installer_bootloader_grub_test.py new file mode 100644 index 0000000000..9450aa1d47 --- /dev/null +++ b/tests/installer_bootloader_grub_test.py @@ -0,0 +1,34 @@ +import os +import shutil +from unittest.mock import Mock, patch + +# Import test module +import sonic_installer.bootloader.grub as grub + +@patch("sonic_installer.bootloader.grub.HOST_PATH", os.path.join(os.path.dirname(os.path.abspath(__file__)), 'installer_bootloader_input/_tmp_host')) +def test_set_fips_grub(): + # Prepare the grub.cfg in the _tmp_host folder + current_path = os.path.dirname(os.path.abspath(__file__)) + grub_config = os.path.join(current_path, 'installer_bootloader_input/host/grub/grub.cfg') + tmp_host_path = os.path.join(current_path, 'installer_bootloader_input/_tmp_host') + tmp_grub_path = os.path.join(tmp_host_path, 'grub') + tmp_grub_config = os.path.join(tmp_grub_path, 'grub.cfg') + os.makedirs(tmp_grub_path, exist_ok=True) + shutil.copy(grub_config, tmp_grub_path) + + image = 'SONiC-OS-internal-202205.57377412-84a9a7f11b' + bootloader = grub.GrubBootloader() + + # The the default setting + assert not bootloader.get_fips(image) + + # Test fips enabled + bootloader.set_fips(image, True) + assert bootloader.get_fips(image) + + # Test fips disabled + bootloader.set_fips(image, False) + assert not bootloader.get_fips(image) + + # Cleanup the _tmp_host folder + shutil.rmtree(tmp_host_path) diff --git a/tests/installer_bootloader_input/host/grub/grub.cfg b/tests/installer_bootloader_input/host/grub/grub.cfg new file mode 100644 index 0000000000..5b44bae12b --- /dev/null +++ b/tests/installer_bootloader_input/host/grub/grub.cfg @@ -0,0 +1,51 @@ +serial --port=0x3f8 --speed=9600 --word=8 --parity=no --stop=1 +terminal_input console serial +terminal_output console serial + +set timeout=5 + +if [ -s $prefix/grubenv ]; then + load_env +fi +if [ "${saved_entry}" ]; then + set default="${saved_entry}" +fi +if [ "${next_entry}" ]; then + set default="${next_entry}" + unset next_entry + save_env next_entry +fi +if [ "${onie_entry}" ]; then + set next_entry="${default}" + set default="${onie_entry}" + unset onie_entry + save_env onie_entry next_entry +fi + +menuentry 'SONiC-OS-internal-202205.57377412-84a9a7f11b' { + search --no-floppy --label --set=root SONiC-OS + echo 'Loading SONiC-OS OS kernel ...' + insmod gzio + if [ x = xxen ]; then insmod xzio; insmod lzopio; fi + insmod part_msdos + insmod ext2 + linux /image-internal-202205.57377412-84a9a7f11b/boot/vmlinuz-5.10.0-12-2-amd64 root=UUID=df89970c-bf6d-40cf-80fc-a977c89054dd rw console=tty0 console=ttyS0,9600n8 quiet intel_idle.max_cstate=0 net.ifnames=0 biosdevname=0 loop=image-internal-202205.57377412-84a9a7f11b/fs.squashfs loopfstype=squashfs systemd.unified_cgroup_hierarchy=0 apparmor=1 security=apparmor varlog_size=4096 usbcore.autosuspend=-1 acpi_enforce_resources=lax acpi=noirq + echo 'Loading SONiC-OS OS initial ramdisk ...' + initrd /image-internal-202205.57377412-84a9a7f11b/boot/initrd.img-5.10.0-12-2-amd64 +} +menuentry 'SONiC-OS-master-11298.116581-1a4f95389' { + search --no-floppy --label --set=root SONiC-OS + echo 'Loading SONiC-OS OS kernel ...' + insmod gzio + if [ x = xxen ]; then insmod xzio; insmod lzopio; fi + insmod part_msdos + insmod ext2 + linux /image-master-11298.116581-1a4f95389/boot/vmlinuz-5.10.0-12-2-amd64 root=UUID=df89970c-bf6d-40cf-80fc-a977c89054dd rw console=tty0 console=ttyS0,9600n8 quiet intel_idle.max_cstate=0 sonic_fips=1 net.ifnames=0 biosdevname=0 loop=image-master-11298.116581-1a4f95389/fs.squashfs loopfstype=squashfs systemd.unified_cgroup_hierarchy=0 apparmor=1 security=apparmor varlog_size=4096 usbcore.autosuspend=-1 acpi_enforce_resources=lax acpi=noirq + echo 'Loading SONiC-OS OS initial ramdisk ...' + initrd /image-master-11298.116581-1a4f95389/boot/initrd.img-5.10.0-12-2-amd64 +} +menuentry ONIE { + search --no-floppy --label --set=root ONIE-BOOT + echo 'Loading ONIE ...' + chainloader +1 +} diff --git a/tests/installer_bootloader_uboot_test.py b/tests/installer_bootloader_uboot_test.py new file mode 100644 index 0000000000..fc2e13b8e0 --- /dev/null +++ b/tests/installer_bootloader_uboot_test.py @@ -0,0 +1,42 @@ +import os +from unittest.mock import Mock, patch + +# Import test module +import sonic_installer.bootloader.uboot as uboot + +class MockProc(): + commandline = "linuxargs=" + def communicate(): + return commandline, None + +def mock_run_command(cmd): + MockProc.commandline = cmd + +@patch("sonic_installer.bootloader.uboot.subprocess.Popen") +@patch("sonic_installer.bootloader.uboot.run_command") +def test_set_fips_uboot(run_command_patch, popen_patch): + class MockProc(): + commandline = "linuxargs" + def communicate(self): + return MockProc.commandline, None + + def mock_run_command(cmd): + # Remove leading string "/usr/bin/fw_setenv linuxargs " -- the 29 characters + MockProc.commandline = 'linuxargs=' + cmd[29:] + + run_command_patch.side_effect = mock_run_command + popen_patch.return_value = MockProc() + + image = 'test-image' + bootloader = uboot.UbootBootloader() + + # The the default setting + assert not bootloader.get_fips(image) + + # Test fips enabled + bootloader.set_fips(image, True) + assert bootloader.get_fips(image) + + # Test fips disabled + bootloader.set_fips(image, False) + assert not bootloader.get_fips(image) diff --git a/tests/test_sonic_installer.py b/tests/test_sonic_installer.py new file mode 100644 index 0000000000..5d19291a91 --- /dev/null +++ b/tests/test_sonic_installer.py @@ -0,0 +1,35 @@ +import os +from contextlib import contextmanager +from sonic_installer.main import sonic_installer +from click.testing import CliRunner +from unittest.mock import patch, Mock, call + +@patch("sonic_installer.main.get_bootloader") +def test_set_fips(get_bootloader): + """ This test covers the execution of "sonic-installer set-fips/get-fips" command. """ + + image = "image_1" + next_image = "image_2" + + # Setup bootloader mock + mock_bootloader = Mock() + mock_bootloader.get_next_image = Mock(return_value=next_image) + mock_bootloader.get_installed_images = Mock(return_value=[image, next_image]) + mock_bootloader.set_fips = Mock() + mock_bootloader.get_fips = Mock(return_value=False) + get_bootloader.return_value=mock_bootloader + + runner = CliRunner() + + # Test set-fips command options: --enable-fips/--disable-fips + result = runner.invoke(sonic_installer.commands["set-fips"], [next_image, '--enable-fips']) + assert 'Set FIPS' in result.output + result = runner.invoke(sonic_installer.commands["set-fips"], ['--disable-fips']) + assert 'Set FIPS' in result.output + + # Test command get-fips options + result = runner.invoke(sonic_installer.commands["get-fips"]) + assert "FIPS is disabled" in result.output + mock_bootloader.get_fips = Mock(return_value=True) + result = runner.invoke(sonic_installer.commands["get-fips"], [next_image]) + assert "FIPS is enabled" in result.output