Skip to content

Commit a4e64d1

Browse files
authored
[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.
1 parent 90efd62 commit a4e64d1

File tree

9 files changed

+487
-299
lines changed

9 files changed

+487
-299
lines changed

setup.py

+1
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@
4949
'pddf_ledutil',
5050
'show',
5151
'sonic_installer',
52+
'sonic_installer.bootloader',
5253
'sonic-utilities-tests',
5354
'undebug',
5455
'utilities_common',
+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
2+
from .aboot import AbootBootloader
3+
from .grub import GrubBootloader
4+
from .uboot import UbootBootloader
5+
6+
BOOTLOADERS = [
7+
AbootBootloader,
8+
GrubBootloader,
9+
UbootBootloader,
10+
]
11+
12+
def get_bootloader():
13+
for bootloaderCls in BOOTLOADERS:
14+
if bootloaderCls.detect():
15+
return bootloaderCls()
16+
raise RuntimeError('Bootloader could not be detected')

sonic_installer/bootloader/aboot.py

+125
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
"""
2+
Bootloader implementation for Aboot used on Arista devices
3+
"""
4+
5+
import collections
6+
import os
7+
import re
8+
import subprocess
9+
10+
import click
11+
12+
from ..common import (
13+
HOST_PATH,
14+
IMAGE_DIR_PREFIX,
15+
IMAGE_PREFIX,
16+
run_command,
17+
)
18+
from .bootloader import Bootloader
19+
20+
_secureboot = None
21+
def isSecureboot():
22+
global _secureboot
23+
if _secureboot is None:
24+
with open('/proc/cmdline') as f:
25+
m = re.search(r"secure_boot_enable=[y1]", f.read())
26+
_secureboot = bool(m)
27+
return _secureboot
28+
29+
class AbootBootloader(Bootloader):
30+
31+
NAME = 'aboot'
32+
BOOT_CONFIG_PATH = os.path.join(HOST_PATH, 'boot-config')
33+
DEFAULT_IMAGE_PATH = '/tmp/sonic_image.swi'
34+
35+
def _boot_config_read(self, path=BOOT_CONFIG_PATH):
36+
config = collections.OrderedDict()
37+
with open(path) as f:
38+
for line in f.readlines():
39+
line = line.strip()
40+
if not line or line.startswith('#') or '=' not in line:
41+
continue
42+
key, value = line.split('=', 1)
43+
config[key] = value
44+
return config
45+
46+
def _boot_config_write(self, config, path=BOOT_CONFIG_PATH):
47+
with open(path, 'w') as f:
48+
f.write(''.join('%s=%s\n' % (k, v) for k, v in config.items()))
49+
50+
def _boot_config_set(self, **kwargs):
51+
path = kwargs.pop('path', self.BOOT_CONFIG_PATH)
52+
config = self._boot_config_read(path=path)
53+
for key, value in kwargs.items():
54+
config[key] = value
55+
self._boot_config_write(config, path=path)
56+
57+
def _swi_image_path(self, image):
58+
image_dir = image.replace(IMAGE_PREFIX, IMAGE_DIR_PREFIX)
59+
if isSecureboot():
60+
return 'flash:%s/sonic.swi' % image_dir
61+
return 'flash:%s/.sonic-boot.swi' % image_dir
62+
63+
def get_current_image(self):
64+
with open('/proc/cmdline') as f:
65+
current = re.search(r"loop=/*(\S+)/", f.read()).group(1)
66+
return current.replace(IMAGE_DIR_PREFIX, IMAGE_PREFIX)
67+
68+
def get_installed_images(self):
69+
images = []
70+
for filename in os.listdir(HOST_PATH):
71+
if filename.startswith(IMAGE_DIR_PREFIX):
72+
images.append(filename.replace(IMAGE_DIR_PREFIX, IMAGE_PREFIX))
73+
return images
74+
75+
def get_next_image(self):
76+
config = self._boot_config_read()
77+
match = re.search(r"flash:/*(\S+)/", config['SWI'])
78+
return match.group(1).replace(IMAGE_DIR_PREFIX, IMAGE_PREFIX)
79+
80+
def set_default_image(self, image):
81+
image_path = self._swi_image_path(image)
82+
self._boot_config_set(SWI=image_path, SWI_DEFAULT=image_path)
83+
return True
84+
85+
def set_next_image(self, image):
86+
image_path = self._swi_image_path(image)
87+
self._boot_config_set(SWI=image_path)
88+
return True
89+
90+
def install_image(self, image_path):
91+
run_command("/usr/bin/unzip -od /tmp %s boot0" % image_path)
92+
run_command("swipath=%s target_path=/host sonic_upgrade=1 . /tmp/boot0" % image_path)
93+
94+
def remove_image(self, image):
95+
nextimage = self.get_next_image()
96+
current = self.get_current_image()
97+
if image == nextimage:
98+
image_path = self._swi_image_path(current)
99+
self._boot_config_set(SWI=image_path, SWI_DEFAULT=image_path)
100+
click.echo("Set next and default boot to current image %s" % current)
101+
102+
image_dir = image.replace(IMAGE_PREFIX, IMAGE_DIR_PREFIX)
103+
click.echo('Removing image root filesystem...')
104+
subprocess.call(['rm','-rf', os.path.join(HOST_PATH, image_dir)])
105+
click.echo('Image removed')
106+
107+
def get_binary_image_version(self, image_path):
108+
try:
109+
version = subprocess.check_output(['/usr/bin/unzip', '-qop', image_path, '.imagehash'])
110+
except subprocess.CalledProcessError:
111+
return None
112+
return IMAGE_PREFIX + version.strip()
113+
114+
def verify_binary_image(self, image_path):
115+
try:
116+
subprocess.check_call(['/usr/bin/unzip', '-tq', image_path])
117+
# TODO: secureboot check signature
118+
except subprocess.CalledProcessError:
119+
return False
120+
return True
121+
122+
@classmethod
123+
def detect(cls):
124+
with open('/proc/cmdline') as f:
125+
return 'Aboot=' in f.read()
+50
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
"""
2+
Abstract Bootloader class
3+
"""
4+
5+
class Bootloader(object):
6+
7+
NAME = None
8+
DEFAULT_IMAGE_PATH = None
9+
10+
def get_current_image(self):
11+
"""returns name of the current image"""
12+
raise NotImplementedError
13+
14+
def get_next_image(self):
15+
"""returns name of the next image"""
16+
raise NotImplementedError
17+
18+
def get_installed_images(self):
19+
"""returns list of installed images"""
20+
raise NotImplementedError
21+
22+
def set_default_image(self, image):
23+
"""set default image to boot from"""
24+
raise NotImplementedError
25+
26+
def set_next_image(self, image):
27+
"""set next image to boot from"""
28+
raise NotImplementedError
29+
30+
def install_image(self, image_path):
31+
"""install new image"""
32+
raise NotImplementedError
33+
34+
def remove_image(self, image):
35+
"""remove existing image"""
36+
raise NotImplementedError
37+
38+
def get_binary_image_version(self, image_path):
39+
"""returns the version of the image"""
40+
raise NotImplementedError
41+
42+
def verify_binary_image(self, image_path):
43+
"""verify that the image is supported by the bootloader"""
44+
raise NotImplementedError
45+
46+
@classmethod
47+
def detect(cls):
48+
"""returns True if the bootloader is in use"""
49+
return False
50+

sonic_installer/bootloader/grub.py

+86
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
"""
2+
Bootloader implementation for grub based platforms
3+
"""
4+
5+
import os
6+
import re
7+
import subprocess
8+
9+
import click
10+
11+
from ..common import (
12+
HOST_PATH,
13+
IMAGE_DIR_PREFIX,
14+
IMAGE_PREFIX,
15+
run_command,
16+
)
17+
from .onie import OnieInstallerBootloader
18+
19+
class GrubBootloader(OnieInstallerBootloader):
20+
21+
NAME = 'grub'
22+
23+
def get_installed_images(self):
24+
images = []
25+
config = open(HOST_PATH + '/grub/grub.cfg', 'r')
26+
for line in config:
27+
if line.startswith('menuentry'):
28+
image = line.split()[1].strip("'")
29+
if IMAGE_PREFIX in image:
30+
images.append(image)
31+
config.close()
32+
return images
33+
34+
def get_next_image(self):
35+
images = self.get_installed_images()
36+
grubenv = subprocess.check_output(["/usr/bin/grub-editenv", HOST_PATH + "/grub/grubenv", "list"])
37+
m = re.search(r"next_entry=(\d+)", grubenv)
38+
if m:
39+
next_image_index = int(m.group(1))
40+
else:
41+
m = re.search(r"saved_entry=(\d+)", grubenv)
42+
if m:
43+
next_image_index = int(m.group(1))
44+
else:
45+
next_image_index = 0
46+
return images[next_image_index]
47+
48+
def set_default_image(self, image):
49+
images = self.get_installed_images()
50+
command = 'grub-set-default --boot-directory=' + HOST_PATH + ' ' + str(images.index(image))
51+
run_command(command)
52+
return True
53+
54+
def set_next_image(self, image):
55+
images = self.get_installed_images()
56+
command = 'grub-reboot --boot-directory=' + HOST_PATH + ' ' + str(images.index(image))
57+
run_command(command)
58+
return True
59+
60+
def install_image(self, image_path):
61+
run_command("bash " + image_path)
62+
run_command('grub-set-default --boot-directory=' + HOST_PATH + ' 0')
63+
64+
def remove_image(self, image):
65+
click.echo('Updating GRUB...')
66+
config = open(HOST_PATH + '/grub/grub.cfg', 'r')
67+
old_config = config.read()
68+
menuentry = re.search("menuentry '" + image + "[^}]*}", old_config).group()
69+
config.close()
70+
config = open(HOST_PATH + '/grub/grub.cfg', 'w')
71+
# remove menuentry of the image in grub.cfg
72+
config.write(old_config.replace(menuentry, ""))
73+
config.close()
74+
click.echo('Done')
75+
76+
image_dir = image.replace(IMAGE_PREFIX, IMAGE_DIR_PREFIX)
77+
click.echo('Removing image root filesystem...')
78+
subprocess.call(['rm','-rf', HOST_PATH + '/' + image_dir])
79+
click.echo('Done')
80+
81+
run_command('grub-set-default --boot-directory=' + HOST_PATH + ' 0')
82+
click.echo('Image removed')
83+
84+
@classmethod
85+
def detect(cls):
86+
return os.path.isfile(os.path.join(HOST_PATH, 'grub/grub.cfg'))

sonic_installer/bootloader/onie.py

+48
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
"""
2+
Common logic for bootloaders using an ONIE installer image
3+
"""
4+
5+
import os
6+
import re
7+
import signal
8+
import subprocess
9+
10+
from ..common import (
11+
IMAGE_DIR_PREFIX,
12+
IMAGE_PREFIX,
13+
)
14+
from .bootloader import Bootloader
15+
16+
# Needed to prevent "broken pipe" error messages when piping
17+
# output of multiple commands using subprocess.Popen()
18+
def default_sigpipe():
19+
signal.signal(signal.SIGPIPE, signal.SIG_DFL)
20+
21+
class OnieInstallerBootloader(Bootloader): # pylint: disable=abstract-method
22+
23+
DEFAULT_IMAGE_PATH = '/tmp/sonic_image'
24+
25+
def get_current_image(self):
26+
cmdline = open('/proc/cmdline', 'r')
27+
current = re.search(r"loop=(\S+)/fs.squashfs", cmdline.read()).group(1)
28+
cmdline.close()
29+
return current.replace(IMAGE_DIR_PREFIX, IMAGE_PREFIX)
30+
31+
def get_binary_image_version(self, image_path):
32+
"""returns the version of the image"""
33+
p1 = subprocess.Popen(["cat", "-v", image_path], stdout=subprocess.PIPE, preexec_fn=default_sigpipe)
34+
p2 = subprocess.Popen(["grep", "-m 1", "^image_version"], stdin=p1.stdout, stdout=subprocess.PIPE, preexec_fn=default_sigpipe)
35+
p3 = subprocess.Popen(["sed", "-n", r"s/^image_version=\"\(.*\)\"$/\1/p"], stdin=p2.stdout, stdout=subprocess.PIPE, preexec_fn=default_sigpipe)
36+
37+
stdout = p3.communicate()[0]
38+
p3.wait()
39+
version_num = stdout.rstrip('\n')
40+
41+
# If we didn't read a version number, this doesn't appear to be a valid SONiC image file
42+
if not version_num:
43+
return None
44+
45+
return IMAGE_PREFIX + version_num
46+
47+
def verify_binary_image(self, image_path):
48+
return os.path.isfile(image_path)

0 commit comments

Comments
 (0)