Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support to enable fips for the command sonic_installer #2154

Merged
merged 6 commits into from
Jul 8, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions sonic_installer/bootloader/aboot.py
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,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
Expand Down Expand Up @@ -205,6 +210,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)
Expand Down
8 changes: 8 additions & 0 deletions sonic_installer/bootloader/bootloader.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
39 changes: 39 additions & 0 deletions sonic_installer/bootloader/grub.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
16 changes: 16 additions & 0 deletions sonic_installer/bootloader/uboot.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import platform
import subprocess
import os
import re

import click

Expand Down Expand Up @@ -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()
Expand Down
32 changes: 32 additions & 0 deletions sonic_installer/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -637,6 +637,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')
Expand Down
23 changes: 23 additions & 0 deletions tests/installer_bootloader_aboot_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

# 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'
Expand Down Expand Up @@ -50,3 +52,24 @@ def test_get_next_image(re_search_patch):
# Test convertion image dir to image name
re_search_patch().group = Mock(return_value=image_dir)
assert bootloader.get_next_image() == exp_image

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)
29 changes: 29 additions & 0 deletions tests/installer_bootloader_grub_test.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import os
import shutil
from unittest.mock import Mock, patch

# Import test module
Expand All @@ -24,3 +25,31 @@ def test_remove_image(open_patch, run_command_patch, re_search_patch):

args, _ = args_list[0]
assert exp_image_path in args[0]

@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)
51 changes: 51 additions & 0 deletions tests/installer_bootloader_input/host/grub/grub.cfg
Original file line number Diff line number Diff line change
@@ -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
}
36 changes: 36 additions & 0 deletions tests/installer_bootloader_uboot_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,13 @@
# 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.call", Mock())
@patch("sonic_installer.bootloader.uboot.run_command")
Expand All @@ -27,3 +34,32 @@ def test_remove_image(run_command_patch):

args, _ = args_list[0]
assert exp_image_path in args[0]

@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)
30 changes: 30 additions & 0 deletions tests/test_sonic_installer.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,3 +81,33 @@ def rootfs_path_mock(path):
call(["rm", "-rf", mounted_image_folder], raise_exception=False),
]
assert run_command_or_raise.call_args_list == expected_call_list

@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