Skip to content

Commit

Permalink
Support to enable fips for the command sonic_installer (#2154) (#2303)
Browse files Browse the repository at this point in the history
What I did
Cherry-pick #2154
Support to enable fips for the command sonic_installer
See sonic-net/SONiC#997

How I did it
sonic-installer set-fips  [--enable-fips|--disable-fips]
sonic-installer get-fips
  • Loading branch information
xumia authored Aug 10, 2022
1 parent 8cbbe4f commit c088ec4
Show file tree
Hide file tree
Showing 10 changed files with 305 additions and 0 deletions.
16 changes: 16 additions & 0 deletions sonic_installer/bootloader/aboot.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
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 @@ -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')
Expand Down
32 changes: 32 additions & 0 deletions tests/installer_bootloader_aboot_test.py
Original file line number Diff line number Diff line change
@@ -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)
34 changes: 34 additions & 0 deletions tests/installer_bootloader_grub_test.py
Original file line number Diff line number Diff line change
@@ -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)
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
}
42 changes: 42 additions & 0 deletions tests/installer_bootloader_uboot_test.py
Original file line number Diff line number Diff line change
@@ -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)
35 changes: 35 additions & 0 deletions tests/test_sonic_installer.py
Original file line number Diff line number Diff line change
@@ -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

0 comments on commit c088ec4

Please sign in to comment.