Skip to content

Commit 51d6bf5

Browse files
authored
Fix Aboot breakage in sonic package manager in sonic-installer (sonic-net#1625)
> Failure cause The `get_rootfs_path` contextmanager was repurposed to implement `get_file_in_image` and later used as a function by leveraging some python complexity to bypass the restrictions coming with the contextmanager which were added purposefuly. It was then called multiple times to compute paths though a simple path join using `new_image_dir` could have been performed. The `get_rootfs_path` implementation for Aboot behaved differently when a rootfs was extracted or kept within the SWI image. It also behaved differently on secureboot systems. The updated method was then called on non-existing files for which the method was never meant to process. > Context around the failure Over time, the installation and boot process has slightly diverged from the ONIE one. There are 3 scenarios to consider. 1) Regular boot similar to ONIE This one should just work the same as the filesystem layout is unchanged. 2) docker_inram where dockerfs.tar.gz is extracted in tmpfs at boot In this scenario the boot is similar to the regular one beside that dockerfs.tar.gz is preserved intact on the flash and not extracted. By design this does not fit the sonic-package-manager requierements and the migration should be skipped which is what I did in this review. In the coming month this boot mode will look closer to 3) below. 3) Secureboot on Arista devices In this scenario the SWI image is kept intact and nothing extracted from it. By ensuring the integrity of the SWI we can guarantee that no code/data has been tampered with. This mode also relies on `docker_inram` at the moment. It could be enhanced when sonic-package-manager can guarantee the integrity of code and data that is both installed and migrated. > Solution provided The method `get_file_in_image` was reverted to its original meaning `get_rootfs_path` as there is no point in keeping the new one. It doesn't have the necessary logic to handle more than the rootfs and the logic can be easily be achieved by the following line. `os.path.join(bootloader.get_image_path(binary_image_name), 'something')` A new Bootloader method called `support_package_migration` is introduced to allow the bootloader to skip the package migration based on the image (docker_inram) or its own configuration (secureboot). By default all bootloaders enable the package migration. That change leads to 1) above running package migration while 2) and 3) skip it.
1 parent 18bed46 commit 51d6bf5

File tree

3 files changed

+73
-33
lines changed

3 files changed

+73
-33
lines changed

sonic_installer/bootloader/aboot.py

+49-11
Original file line numberDiff line numberDiff line change
@@ -19,25 +19,44 @@
1919
HOST_PATH,
2020
IMAGE_DIR_PREFIX,
2121
IMAGE_PREFIX,
22+
ROOTFS_NAME,
2223
run_command,
2324
run_command_or_raise,
2425
)
2526
from .bootloader import Bootloader
2627

2728
_secureboot = None
2829
DEFAULT_SWI_IMAGE = 'sonic.swi'
30+
KERNEL_CMDLINE_NAME = 'kernel-cmdline'
2931

3032
# For the signature format, see: https://github.com/aristanetworks/swi-tools/tree/master/switools
3133
SWI_SIG_FILE_NAME = 'swi-signature'
3234
SWIX_SIG_FILE_NAME = 'swix-signature'
3335
ISSUERCERT = 'IssuerCert'
3436

35-
def isSecureboot():
37+
def parse_cmdline(cmdline=None):
38+
if cmdline is None:
39+
with open('/proc/cmdline') as f:
40+
cmdline = f.read()
41+
42+
data = {}
43+
for entry in cmdline.split():
44+
idx = entry.find('=')
45+
if idx == -1:
46+
data[entry] = None
47+
else:
48+
data[entry[:idx]] = entry[idx+1:]
49+
return data
50+
51+
def docker_inram(cmdline=None):
52+
cmdline = parse_cmdline(cmdline)
53+
return cmdline.get('docker_inram') == 'on'
54+
55+
def is_secureboot():
3656
global _secureboot
3757
if _secureboot is None:
38-
with open('/proc/cmdline') as f:
39-
m = re.search(r"secure_boot_enable=[y1]", f.read())
40-
_secureboot = bool(m)
58+
cmdline = parse_cmdline()
59+
_secureboot = cmdline.get('secure_boot_enable') in ['y', '1']
4160
return _secureboot
4261

4362
class AbootBootloader(Bootloader):
@@ -70,7 +89,7 @@ def _boot_config_set(self, **kwargs):
7089

7190
def _swi_image_path(self, image):
7291
image_dir = image.replace(IMAGE_PREFIX, IMAGE_DIR_PREFIX)
73-
if isSecureboot():
92+
if is_secureboot():
7493
return 'flash:%s/sonic.swi' % image_dir
7594
return 'flash:%s/.sonic-boot.swi' % image_dir
7695

@@ -118,6 +137,25 @@ def remove_image(self, image):
118137
subprocess.call(['rm','-rf', image_path])
119138
click.echo('Image removed')
120139

140+
def _get_image_cmdline(self, image):
141+
image_path = self.get_image_path(image)
142+
with open(os.path.join(image_path, KERNEL_CMDLINE_NAME)) as f:
143+
return f.read()
144+
145+
def supports_package_migration(self, image):
146+
if is_secureboot():
147+
# NOTE: unsafe until migration can guarantee migration safety
148+
# packages need to be signed and verified at boot time.
149+
return False
150+
cmdline = self._get_image_cmdline(image)
151+
if docker_inram(cmdline):
152+
# NOTE: the docker_inram feature extracts builtin containers at boot
153+
# time in memory. the use of package manager under these
154+
# circumpstances is not possible without a boot time package
155+
# installation mechanism.
156+
return False
157+
return True
158+
121159
def get_binary_image_version(self, image_path):
122160
try:
123161
version = subprocess.check_output(['/usr/bin/unzip', '-qop', image_path, '.imagehash'], text=True)
@@ -140,7 +178,7 @@ def verify_next_image(self):
140178
return self._verify_secureboot_image(image_path)
141179

142180
def _verify_secureboot_image(self, image_path):
143-
if isSecureboot():
181+
if is_secureboot():
144182
cert = self.getCert(image_path)
145183
return cert is not None
146184
return True
@@ -188,14 +226,14 @@ def _get_swi_file_offset(self, swipath, filename):
188226
return f._fileobj.tell() # pylint: disable=protected-access
189227

190228
@contextmanager
191-
def get_path_in_image(self, image_path, path):
192-
path_in_image = os.path.join(image_path, path)
193-
if os.path.exists(path_in_image) and not isSecureboot():
194-
yield path_in_image
229+
def get_rootfs_path(self, image_path):
230+
path = os.path.join(image_path, ROOTFS_NAME)
231+
if os.path.exists(path) and not is_secureboot():
232+
yield path
195233
return
196234

197235
swipath = os.path.join(image_path, DEFAULT_SWI_IMAGE)
198-
offset = self._get_swi_file_offset(swipath, path)
236+
offset = self._get_swi_file_offset(swipath, ROOTFS_NAME)
199237
loopdev = subprocess.check_output(['losetup', '-f']).decode('utf8').rstrip()
200238

201239
try:

sonic_installer/bootloader/bootloader.py

+7-2
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
HOST_PATH,
1010
IMAGE_DIR_PREFIX,
1111
IMAGE_PREFIX,
12+
ROOTFS_NAME,
1213
)
1314

1415
class Bootloader(object):
@@ -58,6 +59,10 @@ def verify_next_image(self):
5859
image_path = self.get_image_path(image)
5960
return path.exists(image_path)
6061

62+
def supports_package_migration(self, image):
63+
"""tells if the image supports package migration"""
64+
return True
65+
6166
@classmethod
6267
def detect(cls):
6368
"""returns True if the bootloader is in use"""
@@ -70,6 +75,6 @@ def get_image_path(cls, image):
7075
return image.replace(IMAGE_PREFIX, prefix)
7176

7277
@contextmanager
73-
def get_path_in_image(self, image_path, path_in_image):
78+
def get_rootfs_path(self, image_path):
7479
"""returns the path to the squashfs"""
75-
yield path.join(image_path, path_in_image)
80+
yield path.join(image_path, ROOTFS_NAME)

sonic_installer/main.py

+17-20
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import configparser
2-
import contextlib
32
import os
43
import re
54
import subprocess
@@ -12,11 +11,9 @@
1211
from swsscommon.swsscommon import SonicV2Connector
1312

1413
from .bootloader import get_bootloader
15-
from .bootloader.aboot import AbootBootloader
1614
from .common import (
1715
run_command, run_command_or_raise,
1816
IMAGE_PREFIX,
19-
ROOTFS_NAME,
2017
UPPERDIR_NAME,
2118
WORKDIR_NAME,
2219
DOCKERDIR_NAME,
@@ -279,7 +276,7 @@ def update_sonic_environment(bootloader, binary_image_version):
279276
env_dir = os.path.join(new_image_dir, "sonic-config")
280277
env_file = os.path.join(env_dir, "sonic-environment")
281278

282-
with bootloader.get_path_in_image(new_image_dir, ROOTFS_NAME) as new_image_squashfs_path:
279+
with bootloader.get_rootfs_path(new_image_dir) as new_image_squashfs_path:
283280
try:
284281
mount_squash_fs(new_image_squashfs_path, new_image_mount)
285282

@@ -321,21 +318,21 @@ def migrate_sonic_packages(bootloader, binary_image_version):
321318
packages_path = os.path.join(PACKAGE_MANAGER_DIR, packages_file)
322319
sonic_version = re.sub(IMAGE_PREFIX, '', binary_image_version)
323320
new_image_dir = bootloader.get_image_path(binary_image_version)
321+
new_image_upper_dir = os.path.join(new_image_dir, UPPERDIR_NAME)
322+
new_image_work_dir = os.path.join(new_image_dir, WORKDIR_NAME)
323+
new_image_docker_dir = os.path.join(new_image_dir, DOCKERDIR_NAME)
324+
new_image_mount = os.path.join("/", tmp_dir, "image-{0}-fs".format(sonic_version))
325+
new_image_docker_mount = os.path.join(new_image_mount, "var", "lib", "docker")
326+
327+
if not os.path.isdir(new_image_docker_dir):
328+
# NOTE: This codepath can be reached if the installation process did not
329+
# extract the default dockerfs. This can happen with docker_inram
330+
# though the bootloader class should have disabled the package
331+
# migration which is why this message is a non fatal error message.
332+
echo_and_log("Error: SONiC package migration cannot proceed due to missing docker folder", LOG_ERR, fg="red")
333+
return
324334

325-
with contextlib.ExitStack() as stack:
326-
def get_path(path):
327-
""" Closure to get path by entering
328-
a context manager of bootloader.get_path_in_image """
329-
330-
return stack.enter_context(bootloader.get_path_in_image(new_image_dir, path))
331-
332-
new_image_squashfs_path = get_path(ROOTFS_NAME)
333-
new_image_upper_dir = get_path(UPPERDIR_NAME)
334-
new_image_work_dir = get_path(WORKDIR_NAME)
335-
new_image_docker_dir = get_path(DOCKERDIR_NAME)
336-
new_image_mount = os.path.join("/", tmp_dir, "image-{0}-fs".format(sonic_version))
337-
new_image_docker_mount = os.path.join(new_image_mount, "var", "lib", "docker")
338-
335+
with bootloader.get_rootfs_path(new_image_dir) as new_image_squashfs_path:
339336
try:
340337
mount_squash_fs(new_image_squashfs_path, new_image_mount)
341338
# make sure upper dir and work dir exist
@@ -434,8 +431,8 @@ def install(url, force, skip_migration=False, skip_package_migration=False):
434431

435432
update_sonic_environment(bootloader, binary_image_version)
436433

437-
if isinstance(bootloader, AbootBootloader) and not skip_package_migration:
438-
echo_and_log("Warning: SONiC package migration is not supported currenty on aboot platform due to https://github.com/Azure/sonic-buildimage/issues/7566.", LOG_ERR, fg="red")
434+
if not bootloader.supports_package_migration(binary_image_version) and not skip_package_migration:
435+
echo_and_log("Warning: SONiC package migration is not supported for this bootloader/image", fg="yellow")
439436
skip_package_migration = True
440437

441438
if not skip_package_migration:

0 commit comments

Comments
 (0)