From a4e64d16e66293777621ad39c0281b62445e3ab3 Mon Sep 17 00:00:00 2001 From: Samuel Angebault Date: Fri, 26 Jun 2020 09:56:11 -0700 Subject: [PATCH] [sonic_installer] Refactor sonic_installer code (#953) Add a new Bootloader abstraction. This makes it easier to add bootloader specific behavior while keeping the main logic identical. It is also a step that will ease the introduction of secureboot which relies on bootloader specific behaviors. Shuffle code around to get rid of the hacky if/else all over the place. There are now 3 bootloader classes - AbootBootloader - GrubBootloader - UbootBootloader There was almost no logic change in any of the implementations. Only the AbootBootloader saw some small improvements. More will follow in subsequent changes. --- setup.py | 1 + sonic_installer/bootloader/__init__.py | 16 ++ sonic_installer/bootloader/aboot.py | 125 ++++++++ sonic_installer/bootloader/bootloader.py | 50 ++++ sonic_installer/bootloader/grub.py | 86 ++++++ sonic_installer/bootloader/onie.py | 48 ++++ sonic_installer/bootloader/uboot.py | 83 ++++++ sonic_installer/common.py | 25 ++ sonic_installer/main.py | 352 ++++------------------- 9 files changed, 487 insertions(+), 299 deletions(-) create mode 100644 sonic_installer/bootloader/__init__.py create mode 100644 sonic_installer/bootloader/aboot.py create mode 100644 sonic_installer/bootloader/bootloader.py create mode 100644 sonic_installer/bootloader/grub.py create mode 100644 sonic_installer/bootloader/onie.py create mode 100644 sonic_installer/bootloader/uboot.py create mode 100644 sonic_installer/common.py diff --git a/setup.py b/setup.py index edffa77cd06a..adbcc2c9927a 100644 --- a/setup.py +++ b/setup.py @@ -49,6 +49,7 @@ 'pddf_ledutil', 'show', 'sonic_installer', + 'sonic_installer.bootloader', 'sonic-utilities-tests', 'undebug', 'utilities_common', diff --git a/sonic_installer/bootloader/__init__.py b/sonic_installer/bootloader/__init__.py new file mode 100644 index 000000000000..d2872eb7d06f --- /dev/null +++ b/sonic_installer/bootloader/__init__.py @@ -0,0 +1,16 @@ + +from .aboot import AbootBootloader +from .grub import GrubBootloader +from .uboot import UbootBootloader + +BOOTLOADERS = [ + AbootBootloader, + GrubBootloader, + UbootBootloader, +] + +def get_bootloader(): + for bootloaderCls in BOOTLOADERS: + if bootloaderCls.detect(): + return bootloaderCls() + raise RuntimeError('Bootloader could not be detected') diff --git a/sonic_installer/bootloader/aboot.py b/sonic_installer/bootloader/aboot.py new file mode 100644 index 000000000000..b7c1a061aec2 --- /dev/null +++ b/sonic_installer/bootloader/aboot.py @@ -0,0 +1,125 @@ +""" +Bootloader implementation for Aboot used on Arista devices +""" + +import collections +import os +import re +import subprocess + +import click + +from ..common import ( + HOST_PATH, + IMAGE_DIR_PREFIX, + IMAGE_PREFIX, + run_command, +) +from .bootloader import Bootloader + +_secureboot = None +def isSecureboot(): + global _secureboot + if _secureboot is None: + with open('/proc/cmdline') as f: + m = re.search(r"secure_boot_enable=[y1]", f.read()) + _secureboot = bool(m) + return _secureboot + +class AbootBootloader(Bootloader): + + NAME = 'aboot' + BOOT_CONFIG_PATH = os.path.join(HOST_PATH, 'boot-config') + DEFAULT_IMAGE_PATH = '/tmp/sonic_image.swi' + + def _boot_config_read(self, path=BOOT_CONFIG_PATH): + config = collections.OrderedDict() + with open(path) as f: + for line in f.readlines(): + line = line.strip() + if not line or line.startswith('#') or '=' not in line: + continue + key, value = line.split('=', 1) + config[key] = value + return config + + def _boot_config_write(self, config, path=BOOT_CONFIG_PATH): + with open(path, 'w') as f: + f.write(''.join('%s=%s\n' % (k, v) for k, v in config.items())) + + def _boot_config_set(self, **kwargs): + path = kwargs.pop('path', self.BOOT_CONFIG_PATH) + config = self._boot_config_read(path=path) + for key, value in kwargs.items(): + config[key] = value + self._boot_config_write(config, path=path) + + def _swi_image_path(self, image): + image_dir = image.replace(IMAGE_PREFIX, IMAGE_DIR_PREFIX) + if isSecureboot(): + return 'flash:%s/sonic.swi' % image_dir + return 'flash:%s/.sonic-boot.swi' % image_dir + + def get_current_image(self): + with open('/proc/cmdline') as f: + current = re.search(r"loop=/*(\S+)/", f.read()).group(1) + return current.replace(IMAGE_DIR_PREFIX, IMAGE_PREFIX) + + def get_installed_images(self): + images = [] + for filename in os.listdir(HOST_PATH): + if filename.startswith(IMAGE_DIR_PREFIX): + images.append(filename.replace(IMAGE_DIR_PREFIX, IMAGE_PREFIX)) + return images + + def get_next_image(self): + config = self._boot_config_read() + match = re.search(r"flash:/*(\S+)/", config['SWI']) + return match.group(1).replace(IMAGE_DIR_PREFIX, IMAGE_PREFIX) + + def set_default_image(self, image): + image_path = self._swi_image_path(image) + self._boot_config_set(SWI=image_path, SWI_DEFAULT=image_path) + return True + + def set_next_image(self, image): + image_path = self._swi_image_path(image) + self._boot_config_set(SWI=image_path) + return True + + def install_image(self, image_path): + run_command("/usr/bin/unzip -od /tmp %s boot0" % image_path) + run_command("swipath=%s target_path=/host sonic_upgrade=1 . /tmp/boot0" % image_path) + + def remove_image(self, image): + nextimage = self.get_next_image() + current = self.get_current_image() + if image == nextimage: + image_path = self._swi_image_path(current) + self._boot_config_set(SWI=image_path, SWI_DEFAULT=image_path) + click.echo("Set next and default boot to current image %s" % current) + + image_dir = image.replace(IMAGE_PREFIX, IMAGE_DIR_PREFIX) + click.echo('Removing image root filesystem...') + subprocess.call(['rm','-rf', os.path.join(HOST_PATH, image_dir)]) + click.echo('Image removed') + + def get_binary_image_version(self, image_path): + try: + version = subprocess.check_output(['/usr/bin/unzip', '-qop', image_path, '.imagehash']) + except subprocess.CalledProcessError: + return None + return IMAGE_PREFIX + version.strip() + + def verify_binary_image(self, image_path): + try: + subprocess.check_call(['/usr/bin/unzip', '-tq', image_path]) + # TODO: secureboot check signature + except subprocess.CalledProcessError: + return False + return True + + @classmethod + def detect(cls): + with open('/proc/cmdline') as f: + return 'Aboot=' in f.read() diff --git a/sonic_installer/bootloader/bootloader.py b/sonic_installer/bootloader/bootloader.py new file mode 100644 index 000000000000..78bd05c61cb7 --- /dev/null +++ b/sonic_installer/bootloader/bootloader.py @@ -0,0 +1,50 @@ +""" +Abstract Bootloader class +""" + +class Bootloader(object): + + NAME = None + DEFAULT_IMAGE_PATH = None + + def get_current_image(self): + """returns name of the current image""" + raise NotImplementedError + + def get_next_image(self): + """returns name of the next image""" + raise NotImplementedError + + def get_installed_images(self): + """returns list of installed images""" + raise NotImplementedError + + def set_default_image(self, image): + """set default image to boot from""" + raise NotImplementedError + + def set_next_image(self, image): + """set next image to boot from""" + raise NotImplementedError + + def install_image(self, image_path): + """install new image""" + raise NotImplementedError + + def remove_image(self, image): + """remove existing image""" + raise NotImplementedError + + def get_binary_image_version(self, image_path): + """returns the version of the image""" + raise NotImplementedError + + def verify_binary_image(self, image_path): + """verify that the image is supported by the bootloader""" + raise NotImplementedError + + @classmethod + def detect(cls): + """returns True if the bootloader is in use""" + return False + diff --git a/sonic_installer/bootloader/grub.py b/sonic_installer/bootloader/grub.py new file mode 100644 index 000000000000..1d111f41919c --- /dev/null +++ b/sonic_installer/bootloader/grub.py @@ -0,0 +1,86 @@ +""" +Bootloader implementation for grub based platforms +""" + +import os +import re +import subprocess + +import click + +from ..common import ( + HOST_PATH, + IMAGE_DIR_PREFIX, + IMAGE_PREFIX, + run_command, +) +from .onie import OnieInstallerBootloader + +class GrubBootloader(OnieInstallerBootloader): + + NAME = 'grub' + + def get_installed_images(self): + images = [] + config = open(HOST_PATH + '/grub/grub.cfg', 'r') + for line in config: + if line.startswith('menuentry'): + image = line.split()[1].strip("'") + if IMAGE_PREFIX in image: + images.append(image) + config.close() + return images + + def get_next_image(self): + images = self.get_installed_images() + grubenv = subprocess.check_output(["/usr/bin/grub-editenv", HOST_PATH + "/grub/grubenv", "list"]) + m = re.search(r"next_entry=(\d+)", grubenv) + if m: + next_image_index = int(m.group(1)) + else: + m = re.search(r"saved_entry=(\d+)", grubenv) + if m: + next_image_index = int(m.group(1)) + else: + next_image_index = 0 + return images[next_image_index] + + def set_default_image(self, image): + images = self.get_installed_images() + command = 'grub-set-default --boot-directory=' + HOST_PATH + ' ' + str(images.index(image)) + run_command(command) + return True + + def set_next_image(self, image): + images = self.get_installed_images() + command = 'grub-reboot --boot-directory=' + HOST_PATH + ' ' + str(images.index(image)) + run_command(command) + return True + + def install_image(self, image_path): + run_command("bash " + image_path) + run_command('grub-set-default --boot-directory=' + HOST_PATH + ' 0') + + def remove_image(self, image): + click.echo('Updating GRUB...') + config = open(HOST_PATH + '/grub/grub.cfg', 'r') + old_config = config.read() + menuentry = re.search("menuentry '" + image + "[^}]*}", old_config).group() + config.close() + config = open(HOST_PATH + '/grub/grub.cfg', 'w') + # remove menuentry of the image in grub.cfg + config.write(old_config.replace(menuentry, "")) + config.close() + click.echo('Done') + + image_dir = image.replace(IMAGE_PREFIX, IMAGE_DIR_PREFIX) + click.echo('Removing image root filesystem...') + subprocess.call(['rm','-rf', HOST_PATH + '/' + image_dir]) + click.echo('Done') + + run_command('grub-set-default --boot-directory=' + HOST_PATH + ' 0') + click.echo('Image removed') + + @classmethod + def detect(cls): + return os.path.isfile(os.path.join(HOST_PATH, 'grub/grub.cfg')) diff --git a/sonic_installer/bootloader/onie.py b/sonic_installer/bootloader/onie.py new file mode 100644 index 000000000000..ca16172efa2b --- /dev/null +++ b/sonic_installer/bootloader/onie.py @@ -0,0 +1,48 @@ +""" +Common logic for bootloaders using an ONIE installer image +""" + +import os +import re +import signal +import subprocess + +from ..common import ( + IMAGE_DIR_PREFIX, + IMAGE_PREFIX, +) +from .bootloader import Bootloader + +# Needed to prevent "broken pipe" error messages when piping +# output of multiple commands using subprocess.Popen() +def default_sigpipe(): + signal.signal(signal.SIGPIPE, signal.SIG_DFL) + +class OnieInstallerBootloader(Bootloader): # pylint: disable=abstract-method + + DEFAULT_IMAGE_PATH = '/tmp/sonic_image' + + def get_current_image(self): + cmdline = open('/proc/cmdline', 'r') + current = re.search(r"loop=(\S+)/fs.squashfs", cmdline.read()).group(1) + cmdline.close() + return current.replace(IMAGE_DIR_PREFIX, IMAGE_PREFIX) + + def get_binary_image_version(self, image_path): + """returns the version of the image""" + p1 = subprocess.Popen(["cat", "-v", image_path], stdout=subprocess.PIPE, preexec_fn=default_sigpipe) + p2 = subprocess.Popen(["grep", "-m 1", "^image_version"], stdin=p1.stdout, stdout=subprocess.PIPE, preexec_fn=default_sigpipe) + p3 = subprocess.Popen(["sed", "-n", r"s/^image_version=\"\(.*\)\"$/\1/p"], stdin=p2.stdout, stdout=subprocess.PIPE, preexec_fn=default_sigpipe) + + stdout = p3.communicate()[0] + p3.wait() + version_num = stdout.rstrip('\n') + + # If we didn't read a version number, this doesn't appear to be a valid SONiC image file + if not version_num: + return None + + return IMAGE_PREFIX + version_num + + def verify_binary_image(self, image_path): + return os.path.isfile(image_path) diff --git a/sonic_installer/bootloader/uboot.py b/sonic_installer/bootloader/uboot.py new file mode 100644 index 000000000000..47252dd6af7b --- /dev/null +++ b/sonic_installer/bootloader/uboot.py @@ -0,0 +1,83 @@ +""" +Bootloader implementation for uboot based platforms +""" + +import platform +import subprocess + +import click + +from ..common import ( + HOST_PATH, + IMAGE_DIR_PREFIX, + IMAGE_PREFIX, + run_command, +) +from .onie import OnieInstallerBootloader + +class UbootBootloader(OnieInstallerBootloader): + + NAME = 'uboot' + + def get_installed_images(self): + images = [] + proc = subprocess.Popen("/usr/bin/fw_printenv -n sonic_version_1", shell=True, stdout=subprocess.PIPE) + (out, _) = proc.communicate() + image = out.rstrip() + if IMAGE_PREFIX in image: + images.append(image) + proc = subprocess.Popen("/usr/bin/fw_printenv -n sonic_version_2", shell=True, stdout=subprocess.PIPE) + (out, _) = proc.communicate() + image = out.rstrip() + if IMAGE_PREFIX in image: + images.append(image) + return images + + def get_next_image(self): + images = self.get_installed_images() + proc = subprocess.Popen("/usr/bin/fw_printenv -n boot_next", shell=True, stdout=subprocess.PIPE) + (out, _) = proc.communicate() + image = out.rstrip() + if "sonic_image_2" in image: + next_image_index = 1 + else: + next_image_index = 0 + return images[next_image_index] + + def set_default_image(self, image): + images = self.get_installed_images() + if image in images[0]: + run_command('/usr/bin/fw_setenv boot_next "run sonic_image_1"') + elif image in images[1]: + run_command('/usr/bin/fw_setenv boot_next "run sonic_image_2"') + return True + + def set_next_image(self, image): + images = self.get_installed_images() + if image in images[0]: + run_command('/usr/bin/fw_setenv boot_once "run sonic_image_1"') + elif image in images[1]: + run_command('/usr/bin/fw_setenv boot_once "run sonic_image_2"') + return True + + def install_image(self, image_path): + run_command("bash " + image_path) + + def remove_image(self, image): + click.echo('Updating next boot ...') + images = self.get_installed_images() + if image in images[0]: + run_command('/usr/bin/fw_setenv boot_next "run sonic_image_2"') + run_command('/usr/bin/fw_setenv sonic_version_1 "NONE"') + elif image in images[1]: + run_command('/usr/bin/fw_setenv boot_next "run sonic_image_1"') + run_command('/usr/bin/fw_setenv sonic_version_2 "NONE"') + image_dir = image.replace(IMAGE_PREFIX, IMAGE_DIR_PREFIX) + click.echo('Removing image root filesystem...') + subprocess.call(['rm','-rf', HOST_PATH + '/' + image_dir]) + click.echo('Done') + + @classmethod + def detect(cls): + arch = platform.machine() + return ("arm" in arch) or ("aarch64" in arch) diff --git a/sonic_installer/common.py b/sonic_installer/common.py new file mode 100644 index 000000000000..f12454042a9c --- /dev/null +++ b/sonic_installer/common.py @@ -0,0 +1,25 @@ +""" +Module holding common functions and constants used by sonic_installer and its +subpackages. +""" + +import subprocess +import sys + +import click + +HOST_PATH = '/host' +IMAGE_PREFIX = 'SONiC-OS-' +IMAGE_DIR_PREFIX = 'image-' + +# Run bash command and print output to stdout +def run_command(command): + click.echo(click.style("Command: ", fg='cyan') + click.style(command, fg='green')) + + proc = subprocess.Popen(command, shell=True, stdout=subprocess.PIPE) + (out, _) = proc.communicate() + + click.echo(out) + + if proc.returncode != 0: + sys.exit(proc.returncode) diff --git a/sonic_installer/main.py b/sonic_installer/main.py index fb8179c9c65f..3c68bcd84398 100644 --- a/sonic_installer/main.py +++ b/sonic_installer/main.py @@ -1,8 +1,6 @@ #! /usr/bin/python -u import os -import re -import signal import sys import time import click @@ -10,45 +8,30 @@ import syslog import subprocess from swsssdk import SonicV2Connector -import collections -import platform - -HOST_PATH = '/host' -IMAGE_PREFIX = 'SONiC-OS-' -IMAGE_DIR_PREFIX = 'image-' -ONIE_DEFAULT_IMAGE_PATH = '/tmp/sonic_image' -ABOOT_DEFAULT_IMAGE_PATH = '/tmp/sonic_image.swi' -IMAGE_TYPE_ABOOT = 'aboot' -IMAGE_TYPE_ONIE = 'onie' -ABOOT_BOOT_CONFIG = '/boot-config' -BOOTLOADER_TYPE_GRUB = 'grub' -BOOTLOADER_TYPE_UBOOT = 'uboot' -ARCH = platform.machine() -BOOTLOADER = BOOTLOADER_TYPE_UBOOT if ("arm" in ARCH) or ("aarch64" in ARCH) else BOOTLOADER_TYPE_GRUB + +from .bootloader import get_bootloader +from .common import run_command # # Helper functions # -# Needed to prevent "broken pipe" error messages when piping -# output of multiple commands using subprocess.Popen() -def default_sigpipe(): - signal.signal(signal.SIGPIPE, signal.SIG_DFL) - +_start_time = None +_last_time = None def reporthook(count, block_size, total_size): - global start_time, last_time + global _start_time, _last_time cur_time = int(time.time()) if count == 0: - start_time = cur_time - last_time = cur_time + _start_time = cur_time + _last_time = cur_time return - if cur_time == last_time: + if cur_time == _last_time: return - last_time = cur_time + _last_time = cur_time - duration = cur_time - start_time + duration = cur_time - _start_time progress_size = int(count * block_size) speed = int(progress_size / (1024 * duration)) percent = int(count * block_size * 100 / total_size) @@ -57,226 +40,13 @@ def reporthook(count, block_size, total_size): (percent, progress_size / (1024 * 1024), speed, time_left)) sys.stdout.flush() -def get_running_image_type(): - """ Attempt to determine whether we are running an ONIE or Aboot image """ - cmdline = open('/proc/cmdline', 'r') - if "Aboot=" in cmdline.read(): - return IMAGE_TYPE_ABOOT - return IMAGE_TYPE_ONIE - -# Returns None if image doesn't exist or isn't a regular file -def get_binary_image_type(binary_image_path): - """ Attempt to determine whether this is an ONIE or Aboot image file """ - if not os.path.isfile(binary_image_path): - return None - - with open(binary_image_path) as f: - # Aboot file is a zip archive; check the start of the file for the zip magic number - if f.read(4) == "\x50\x4b\x03\x04": - return IMAGE_TYPE_ABOOT - return IMAGE_TYPE_ONIE - -# Returns None if image doesn't exist or doesn't appear to be a valid SONiC image file -def get_binary_image_version(binary_image_path): - binary_type = get_binary_image_type(binary_image_path) - if not binary_type: - return None - elif binary_type == IMAGE_TYPE_ABOOT: - p1 = subprocess.Popen(["unzip", "-p", binary_image_path, "boot0"], stdout=subprocess.PIPE, preexec_fn=default_sigpipe) - p2 = subprocess.Popen(["grep", "-m 1", "^image_name"], stdin=p1.stdout, stdout=subprocess.PIPE, preexec_fn=default_sigpipe) - p3 = subprocess.Popen(["sed", "-n", r"s/^image_name=\"\image-\(.*\)\"$/\1/p"], stdin=p2.stdout, stdout=subprocess.PIPE, preexec_fn=default_sigpipe) - else: - p1 = subprocess.Popen(["cat", "-v", binary_image_path], stdout=subprocess.PIPE, preexec_fn=default_sigpipe) - p2 = subprocess.Popen(["grep", "-m 1", "^image_version"], stdin=p1.stdout, stdout=subprocess.PIPE, preexec_fn=default_sigpipe) - p3 = subprocess.Popen(["sed", "-n", r"s/^image_version=\"\(.*\)\"$/\1/p"], stdin=p2.stdout, stdout=subprocess.PIPE, preexec_fn=default_sigpipe) - - stdout = p3.communicate()[0] - p3.wait() - version_num = stdout.rstrip('\n') - - # If we didn't read a version number, this doesn't appear to be a valid SONiC image file - if len(version_num) == 0: - return None - - return IMAGE_PREFIX + version_num - -# Sets specified image as default image to boot from -def set_default_image(image): - images = get_installed_images() - if image not in images: - return False - - if get_running_image_type() == IMAGE_TYPE_ABOOT: - image_path = aboot_image_path(image) - aboot_boot_config_set(SWI=image_path, SWI_DEFAULT=image_path) - elif BOOTLOADER == BOOTLOADER_TYPE_GRUB: - command = 'grub-set-default --boot-directory=' + HOST_PATH + ' ' + str(images.index(image)) - run_command(command) - elif BOOTLOADER == BOOTLOADER_TYPE_UBOOT: - if image in images[0]: - run_command('/usr/bin/fw_setenv boot_next "run sonic_image_1"') - elif image in images[1]: - run_command('/usr/bin/fw_setenv boot_next "run sonic_image_2"') - - return True - -def aboot_read_boot_config(path): - config = collections.OrderedDict() - with open(path) as f: - for line in f.readlines(): - line = line.strip() - if not line or line.startswith('#') or '=' not in line: - continue - key, value = line.split('=', 1) - config[key] = value - return config - -def aboot_write_boot_config(path, config): - with open(path, 'w') as f: - f.write(''.join( '%s=%s\n' % (k, v) for k, v in config.items())) - -def aboot_boot_config_set(**kwargs): - path = kwargs.get('path', HOST_PATH + ABOOT_BOOT_CONFIG) - config = aboot_read_boot_config(path) - for key, value in kwargs.items(): - config[key] = value - aboot_write_boot_config(path, config) - -def aboot_image_path(image): - image_dir = image.replace(IMAGE_PREFIX, IMAGE_DIR_PREFIX) - return 'flash:%s/.sonic-boot.swi' % image_dir - -# Run bash command and print output to stdout -def run_command(command): - click.echo(click.style("Command: ", fg='cyan') + click.style(command, fg='green')) - - proc = subprocess.Popen(command, shell=True, stdout=subprocess.PIPE) - (out, err) = proc.communicate() - - click.echo(out) - - if proc.returncode != 0: - sys.exit(proc.returncode) - -# Returns list of installed images -def get_installed_images(): - images = [] - if get_running_image_type() == IMAGE_TYPE_ABOOT: - for filename in os.listdir(HOST_PATH): - if filename.startswith(IMAGE_DIR_PREFIX): - images.append(filename.replace(IMAGE_DIR_PREFIX, IMAGE_PREFIX)) - elif BOOTLOADER == BOOTLOADER_TYPE_GRUB: - config = open(HOST_PATH + '/grub/grub.cfg', 'r') - for line in config: - if line.startswith('menuentry'): - image = line.split()[1].strip("'") - if IMAGE_PREFIX in image: - images.append(image) - config.close() - elif BOOTLOADER == BOOTLOADER_TYPE_UBOOT: - proc = subprocess.Popen("/usr/bin/fw_printenv -n sonic_version_1", shell=True, stdout=subprocess.PIPE) - (out, err) = proc.communicate() - image = out.rstrip() - if IMAGE_PREFIX in image: - images.append(image) - proc = subprocess.Popen("/usr/bin/fw_printenv -n sonic_version_2", shell=True, stdout=subprocess.PIPE) - (out, err) = proc.communicate() - image = out.rstrip() - if IMAGE_PREFIX in image: - images.append(image) - return images - -# Returns name of current image -def get_current_image(): - cmdline = open('/proc/cmdline', 'r') - current = re.search("loop=(\S+)/fs.squashfs", cmdline.read()).group(1) - cmdline.close() - return current.replace(IMAGE_DIR_PREFIX, IMAGE_PREFIX) - -# Returns name of next boot image -def get_next_image(): - if get_running_image_type() == IMAGE_TYPE_ABOOT: - config = open(HOST_PATH + ABOOT_BOOT_CONFIG, 'r') - next_image = re.search("SWI=flash:(\S+)/", config.read()).group(1).replace(IMAGE_DIR_PREFIX, IMAGE_PREFIX) - config.close() - elif BOOTLOADER == BOOTLOADER_TYPE_GRUB: - images = get_installed_images() - grubenv = subprocess.check_output(["/usr/bin/grub-editenv", HOST_PATH + "/grub/grubenv", "list"]) - m = re.search("next_entry=(\d+)", grubenv) - if m: - next_image_index = int(m.group(1)) - else: - m = re.search("saved_entry=(\d+)", grubenv) - if m: - next_image_index = int(m.group(1)) - else: - next_image_index = 0 - next_image = images[next_image_index] - elif BOOTLOADER == BOOTLOADER_TYPE_UBOOT: - images = get_installed_images() - proc = subprocess.Popen("/usr/bin/fw_printenv -n boot_next", shell=True, stdout=subprocess.PIPE) - (out, err) = proc.communicate() - image = out.rstrip() - if "sonic_image_2" in image: - next_image_index = 1 - else: - next_image_index = 0 - next_image = images[next_image_index] - return next_image - -def remove_image(image): - if get_running_image_type() == IMAGE_TYPE_ABOOT: - nextimage = get_next_image() - current = get_current_image() - if image == nextimage: - image_path = aboot_image_path(current) - aboot_boot_config_set(SWI=image_path, SWI_DEFAULT=image_path) - click.echo("Set next and default boot to current image %s" % current) - - image_dir = image.replace(IMAGE_PREFIX, IMAGE_DIR_PREFIX) - click.echo('Removing image root filesystem...') - subprocess.call(['rm','-rf', os.path.join(HOST_PATH, image_dir)]) - click.echo('Image removed') - elif BOOTLOADER == BOOTLOADER_TYPE_GRUB: - click.echo('Updating GRUB...') - config = open(HOST_PATH + '/grub/grub.cfg', 'r') - old_config = config.read() - menuentry = re.search("menuentry '" + image + "[^}]*}", old_config).group() - config.close() - config = open(HOST_PATH + '/grub/grub.cfg', 'w') - # remove menuentry of the image in grub.cfg - config.write(old_config.replace(menuentry, "")) - config.close() - click.echo('Done') - - image_dir = image.replace(IMAGE_PREFIX, IMAGE_DIR_PREFIX) - click.echo('Removing image root filesystem...') - subprocess.call(['rm','-rf', HOST_PATH + '/' + image_dir]) - click.echo('Done') - - run_command('grub-set-default --boot-directory=' + HOST_PATH + ' 0') - click.echo('Image removed') - elif BOOTLOADER == BOOTLOADER_TYPE_UBOOT: - click.echo('Updating next boot ...') - images = get_installed_images() - if image in images[0]: - run_command('/usr/bin/fw_setenv boot_next "run sonic_image_2"') - run_command('/usr/bin/fw_setenv sonic_version_1 "NONE"') - elif image in images[1]: - run_command('/usr/bin/fw_setenv boot_next "run sonic_image_1"') - run_command('/usr/bin/fw_setenv sonic_version_2 "NONE"') - image_dir = image.replace(IMAGE_PREFIX, IMAGE_DIR_PREFIX) - click.echo('Removing image root filesystem...') - subprocess.call(['rm','-rf', HOST_PATH + '/' + image_dir]) - click.echo('Done') - # TODO: Embed tag name info into docker image meta data at build time, # and extract tag name from docker image file. def get_docker_tag_name(image): # Try to get tag name from label metadata cmd = "docker inspect --format '{{.ContainerConfig.Labels.Tag}}' " + image proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, shell=True) - (out, err) = proc.communicate() + (out, _) = proc.communicate() if proc.returncode != 0: return "unknown" tag = out.rstrip() @@ -292,7 +62,7 @@ def validate_url_or_abort(url): urlfile = urllib.urlopen(url) response_code = urlfile.getcode() urlfile.close() - except IOError, err: + except IOError: response_code = None if not response_code: @@ -313,7 +83,7 @@ def get_container_image_name(container_name): # example image: docker-lldp-sv2:latest cmd = "docker inspect --format '{{.Config.Image}}' " + container_name proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, shell=True) - (out, err) = proc.communicate() + (out, _) = proc.communicate() if proc.returncode != 0: sys.exit(proc.returncode) image_latest = out.rstrip() @@ -374,52 +144,42 @@ def cli(): @click.argument('url') def install(url, force, skip_migration=False): """ Install image from local binary or URL""" - if get_running_image_type() == IMAGE_TYPE_ABOOT: - DEFAULT_IMAGE_PATH = ABOOT_DEFAULT_IMAGE_PATH - else: - DEFAULT_IMAGE_PATH = ONIE_DEFAULT_IMAGE_PATH + bootloader = get_bootloader() if url.startswith('http://') or url.startswith('https://'): click.echo('Downloading image...') validate_url_or_abort(url) try: - urllib.urlretrieve(url, DEFAULT_IMAGE_PATH, reporthook) + urllib.urlretrieve(url, bootloader.DEFAULT_IMAGE_PATH, reporthook) + click.echo('') except Exception as e: click.echo("Download error", e) raise click.Abort() - image_path = DEFAULT_IMAGE_PATH + image_path = bootloader.DEFAULT_IMAGE_PATH else: image_path = os.path.join("./", url) - running_image_type = get_running_image_type() - binary_image_type = get_binary_image_type(image_path) - binary_image_version = get_binary_image_version(image_path) - if not binary_image_type or not binary_image_version: + binary_image_version = bootloader.get_binary_image_version(image_path) + if not binary_image_version: click.echo("Image file does not exist or is not a valid SONiC image file") raise click.Abort() # Is this version already installed? - if binary_image_version in get_installed_images(): + if binary_image_version in bootloader.get_installed_images(): click.echo("Image {} is already installed. Setting it as default...".format(binary_image_version)) - if not set_default_image(binary_image_version): + if not bootloader.set_default_image(binary_image_version): click.echo('Error: Failed to set image as default') raise click.Abort() else: # Verify that the binary image is of the same type as the running image - if (binary_image_type != running_image_type) and not force: - click.echo("Image file '{}' is of a different type than running image.\n" + - "If you are sure you want to install this image, use -f|--force.\n" + + if not bootloader.verify_binary_image(image_path) and not force: + click.echo("Image file '{}' is of a different type than running image.\n" + "If you are sure you want to install this image, use -f|--force.\n" "Aborting...".format(image_path)) raise click.Abort() click.echo("Installing image {} and setting it as default...".format(binary_image_version)) - if running_image_type == IMAGE_TYPE_ABOOT: - run_command("/usr/bin/unzip -od /tmp %s boot0" % image_path) - run_command("swipath=%s target_path=/host sonic_upgrade=1 . /tmp/boot0" % image_path) - else: - run_command("bash " + image_path) - if BOOTLOADER == BOOTLOADER_TYPE_GRUB: - run_command('grub-set-default --boot-directory=' + HOST_PATH + ' 0') + bootloader.install_image(image_path) # Take a backup of current configuration if skip_migration: click.echo("Skipping configuration migration as requested in the command option.") @@ -433,12 +193,13 @@ def install(url, force, skip_migration=False): # List installed images -@cli.command() -def list(): +@cli.command('list') +def list_command(): """ Print installed images """ - images = get_installed_images() - curimage = get_current_image() - nextimage = get_next_image() + bootloader = get_bootloader() + images = bootloader.get_installed_images() + curimage = bootloader.get_current_image() + nextimage = bootloader.get_next_image() click.echo("Current: " + curimage) click.echo("Next: " + nextimage) click.echo("Available: ") @@ -450,32 +211,22 @@ def list(): @click.argument('image') def set_default(image): """ Choose image to boot from by default """ - if not set_default_image(image): + bootloader = get_bootloader() + if image not in bootloader.get_installed_images(): click.echo('Error: Image does not exist') raise click.Abort() - + bootloader.set_default_image(image) # Set image for next boot @cli.command('set_next_boot') @click.argument('image') def set_next_boot(image): """ Choose image for next reboot (one time action) """ - images = get_installed_images() - if image not in images: - click.echo('Image does not exist') + bootloader = get_bootloader() + if image not in bootloader.get_installed_images(): + click.echo('Error: Image does not exist') sys.exit(1) - if get_running_image_type() == IMAGE_TYPE_ABOOT: - image_path = aboot_image_path(image) - aboot_boot_config_set(SWI=image_path) - elif BOOTLOADER == BOOTLOADER_TYPE_GRUB: - command = 'grub-reboot --boot-directory=' + HOST_PATH + ' ' + str(images.index(image)) - run_command(command) - elif BOOTLOADER == BOOTLOADER_TYPE_UBOOT: - if image in images[0]: - run_command('/usr/bin/fw_setenv boot_once "run sonic_image_1"') - elif image in images[1]: - run_command('/usr/bin/fw_setenv boot_once "run sonic_image_2"') - + bootloader.set_next_image(image) # Uninstall image @cli.command() @@ -484,28 +235,30 @@ def set_next_boot(image): @click.argument('image') def remove(image): """ Uninstall image """ - images = get_installed_images() - current = get_current_image() + bootloader = get_bootloader() + images = bootloader.get_installed_images() + current = bootloader.get_current_image() if image not in images: click.echo('Image does not exist') sys.exit(1) if image == current: click.echo('Cannot remove current image') sys.exit(1) - - remove_image(image) + # TODO: check if image is next boot or default boot and fix these + bootloader.remove_image(image) # Retrieve version from binary image file and print to screen @cli.command('binary_version') @click.argument('binary_image_path') def binary_version(binary_image_path): """ Get version from local binary image file """ - binary_version = get_binary_image_version(binary_image_path) - if not binary_version: + bootloader = get_bootloader() + version = bootloader.get_binary_image_version(binary_image_path) + if not version: click.echo("Image file does not exist or is not a valid SONiC image file") sys.exit(1) else: - click.echo(binary_version) + click.echo(version) # Remove installed images which are not current and next @cli.command() @@ -513,14 +266,15 @@ def binary_version(binary_image_path): expose_value=False, prompt='Remove images which are not current and next, continue?') def cleanup(): """ Remove installed images which are not current and next """ - images = get_installed_images() - curimage = get_current_image() - nextimage = get_next_image() + bootloader = get_bootloader() + images = bootloader.get_installed_images() + curimage = bootloader.get_current_image() + nextimage = bootloader.get_next_image() image_removed = 0 for image in images: if image != curimage and image != nextimage: click.echo("Removing image %s" % image) - remove_image(image) + bootloader.remove_image(image) image_removed += 1 if image_removed == 0: