diff --git a/README.md b/README.md index 519644f..a5d970c 100644 --- a/README.md +++ b/README.md @@ -66,7 +66,6 @@ There are other options as well, as detailed below: | Option | Action | |-------------------------------------------|--------------------------------------------------------| |`-h`, `--help` | Display the help Text | -|`-c`, `--dry-run` | Don't actually copy any files or set anything up. | |`-p`, `--print-config` | Print the current configuration and exit. | |*_Path Options_* | | |`-r `, `--root-path ` | Manually specify the root filesystem path. | @@ -122,21 +121,22 @@ If kernelstub is going to be used in a scripted environment, it is useful to know what return codes it provides in the event of errors. The table below details these codes and their meaning: -| Exit Code | Meaning | -|-----------|--------------------------------------------------------------| -| 0 | Success | -| 166 | The kernel path supplied/detected was invalid | -| 167 | The initrd path supplied/detected was invalid | -| 168 | No kernel options found/supplied | -| 169 | Malformed configuration found | -| 170 | Couldn't copy kernel image to ESP | -| 171 | Couldn't copy initrd image to ESP | -| 172 | Couldn't create a new NVRAM entry | -| 173 | Couldn't remove an old NVRAM entry | -| 174 | Couldn't detect the block device file for the root partition | -| 175 | Coundn't detect the block device file for the ESP | -| 176 | Wasn't run as root | -| 177 | Couldn't get a required UUID | +| Exit Code | Meaning | +|-----------|---------------------------------------------------------------| +| 0 | Success | +| 166 | The kernel path supplied/detected was invalid | +| 167 | The initrd path supplied/detected was invalid | +| 168 | No kernel options found/supplied | +| 169 | Malformed configuration found | +| 170 | Couldn't copy kernel image to ESP | +| 171 | Couldn't copy initrd image to ESP | +| 172 | Couldn't create a new NVRAM entry | +| 173 | Couldn't remove an old NVRAM entry | +| 174 | Couldn't detect the block device file for the root partition | +| 175 | Coundn't detect the block device file for the ESP | +| 176 | Wasn't run as root | +| 177 | Couldn't get a required UUID | +| 178 | Simulate option used | ### Licence diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..c3a2ba6 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,15 @@ +# Security Policy + +## Supported Versions +The following versions of Kernelstub currently receive updates for security: + +| Version | Supported | +| ------- | ------------------ | +| 3.2.0 | :white_check_mark: | +| 3.1.x | :white_check_mark: | +| < 3.1 | :x: | + +## Reporting a Vulnerability + +When Filing an issue for a potential security vulnerability, please be sure +to include a `[SECURITY]` tag in the issue title. diff --git a/bin/kernelstub b/bin/kernelstub index 96afdfc..15bfa29 100644 --- a/bin/kernelstub +++ b/bin/kernelstub @@ -39,193 +39,202 @@ terms. kernelstub will load parameters from the /etc/default/kernelstub config file. """ -import argparse, os +import argparse +import os from kernelstub import application -def main(options=None): # Do the thing +def main(options=None): + """ Do the thing - Main Kernelstub Function""" kernelstub = application.Kernelstub() # Set up argument processing parser = argparse.ArgumentParser( - description = "Automatic Kernel EFIstub manager") + description="Automatic Kernel EFIstub manager") loader_stub = parser.add_mutually_exclusive_group() install_loader = parser.add_mutually_exclusive_group() parser.add_argument( '-c', '--dry-run', - action = 'store_true', - dest = 'dry_run', - help = 'Don\'t perform any actions, just simulate them.' + action='store_true', + dest='dry_run', + help=argparse.SUPPRESS ) parser.add_argument( '-p', '--print-config', - action = 'store_true', - dest = 'print_config', - help = 'Print the current configuration and exit' + action='store_true', + dest='print_config', + help='Print the current configuration and exit' ) parser.add_argument( '-e', - dest = 'esp_path', - metavar = 'ESP,', - help = '' + dest='esp_path', + metavar='ESP,', + help='' ) parser.add_argument( '--esp-path', - dest = 'esp_path', - metavar = 'ESP', - help = 'Manually specify the path to the ESP. Default is /boot/efi' + dest='esp_path', + metavar='ESP', + help='Manually specify the path to the ESP. Default is /boot/efi' ) parser.add_argument( '-r', - dest = 'root_path', - metavar = 'ROOT', - help = '' + dest='root_path', + metavar='ROOT', + help='' ) parser.add_argument( '--root-path', - dest = 'root_path', - metavar = 'ROOT', - help = 'The path where the root filesystem to use is mounted.' + dest='root_path', + metavar='ROOT', + help='The path where the root filesystem to use is mounted.' ) parser.add_argument( '-k', - dest = 'kernel_path', - metavar= 'PATH,', - help = '' + dest='kernel_path', + metavar='PATH,', + help='' ) parser.add_argument( '--kernel-path', - dest = 'kernel_path', - metavar= 'PATH', - help = 'The path to the kernel image.' + dest='kernel_path', + metavar='PATH', + help='The path to the kernel image.' ) parser.add_argument( '-i', - dest = 'initrd_path', - metavar = 'PATH,', - help = '' + dest='initrd_path', + metavar='PATH,', + help='' ) parser.add_argument( '--initrd-path', - dest = 'initrd_path', - metavar = 'PATH', - help = 'The path to the initrd image.' + dest='initrd_path', + metavar='PATH', + help='The path to the initrd image.' ) parser.add_argument( '-o', - dest = 'k_options', - metavar = '"OPTIONS",', - help = '' + dest='k_options', + metavar='"OPTIONS",', + help='' ) parser.add_argument( '--options', - dest = 'k_options', - metavar = '"OPTIONS"', - help = 'The total boot options to be passed to the kernel' + dest='k_options', + metavar='"OPTIONS"', + help='The total boot options to be passed to the kernel' ) parser.add_argument( '-a', - dest = 'add_options', - metavar = '"OPTIONS",', - help = '' + dest='add_options', + metavar='"OPTIONS",', + help='' ) parser.add_argument( '--add-options', - dest = 'add_options', - metavar = '"OPTIONS"', - help = ('Boot options to add to the configuration ' - '(if they aren\'t already present)') - ) + dest='add_options', + metavar='"OPTIONS"', + help=( + 'Boot options to add to the configuration (if they aren\'t ' + 'already present)' + ) + ) parser.add_argument( '-d', - dest = 'remove_options', - metavar = "OPTIONS", - help = '' + dest='remove_options', + metavar="OPTIONS", + help='' ) parser.add_argument( '--delete-options', - dest = 'remove_options', - metavar = '"OPTIONS"', - help = ('Boot options to remove from the configuration ' - '(if they\'re present already)') + dest='remove_options', + metavar='"OPTIONS"', + help=( + 'Boot options to remove from the configuration (if they\'re ' + 'present already)' + ) ) parser.add_argument( '-g', - dest = 'log_file', - metavar = 'LOG', - help = '' + dest='log_file', + metavar='LOG', + help='' ) parser.add_argument( '--log-file', - dest = 'log_file', - metavar = 'LOG', - help = ('The path to the log file to use. Defaults to ' - '/var/log/kernelstub.log') + dest='log_file', + metavar='LOG', + help=( + 'The path to the log file to use. Defaults to ' + '/var/log/kernelstub.log' + ) ) install_loader.add_argument( '-l', '--loader', - action = 'store_true', - dest = 'setup_loader', - help = 'Creates a systemd-boot compatible loader configuration' + action='store_true', + dest='setup_loader', + help='Creates a systemd-boot compatible loader configuration' ) install_loader.add_argument( '-n', '--no-loader', - action = 'store_true', - dest = 'off_loader', - help = 'Turns off creating loader configuration' + action='store_true', + dest='off_loader', + help='Turns off creating loader configuration' ) loader_stub.add_argument( '-s', '--stub', - action = 'store_true', - dest = 'install_stub', - help = 'Set up NVRAM entries for the copied kernel' + action='store_true', + dest='install_stub', + help='Set up NVRAM entries for the copied kernel' ) loader_stub.add_argument( '-m', '--manage-only', - action = 'store_true', - dest = 'manage_mode', - help = 'Only copy entries, don\'t set up the NVRAM' + action='store_true', + dest='manage_mode', + help='Only copy entries, don\'t set up the NVRAM' ) parser.add_argument( '-f', '--force-update', - action = 'store_true', - dest = 'force_update', - help = ('Forcibly update any loader.conf to set the new entry as the ' - 'default') + action='store_true', + dest='force_update', + help=( + 'Forcibly update any loader.conf to set the new entry as the default' + ) ) parser.add_argument( '-v', '--verbose', - action = 'count', - dest = 'verbosity', - help = 'Increase program verbosity and display extra output.' + action='count', + dest='verbosity', + help='Increase program verbosity and display extra output.' ) parser.add_argument( '--preserve-live-mode', - action = 'store_true', - dest = 'preserve_live', - help = argparse.SUPPRESS + action='store_true', + dest='preserve_live', + help=argparse.SUPPRESS ) args = parser.parse_args() @@ -234,8 +243,9 @@ def main(options=None): # Do the thing if os.geteuid() != 0: parser.print_help() - print('kernelstub: ERROR: You need to be root or use sudo to run ' - 'kernelstub!') + print( + 'kernelstub:ERROR: You need to be root or use sudo to run kernelstub' + ) exit(176) kernelstub.main(args) diff --git a/data/kernelstub.1 b/data/kernelstub.1 new file mode 100644 index 0000000..7dc9cab --- /dev/null +++ b/data/kernelstub.1 @@ -0,0 +1,332 @@ +.TH KERNELSTUB "1" +.\" To view this file while editing, run it through groff: +.\" groff -Tascii -man kernelstub.1 | less + +.SH NAME +kernelstub \- The automatic Linux kernel EFIstub manager +.SH SYNOPSIS +.B kernelstub +[ +.B \-e +.I esp-path +] +[ +.B \-r +.I root-fs-path +] +[ +.B \-k +.I kernel-image-path +] +.br + [ +.B \-i +.I initrd-image-path +] +[ +.B \-o +.I "kernel-options" +] +.br + [ +.B \-a +.I "kernel-options" +] +[ +.B \-d +.I "kernel-options" +] +.br + [ +.B \-g +.I log-file-path +] +[ +.B \-h +] +[ +.B \-p +] +[ +.B \-f +] +[ +.B \-l +| +.B \-n +] +.br + [ +.B \-s +| +.B \-m +] +[ +.B \-v... +] + +.SH DESCRIPTION +Kernelstub is a program to setup and configure booting without a traditional +bootloader. +It can configure booting through systemd-boot compatible loader files, or +through the kernel's built-in efi stub loader. +It also runs automatically when the kernel is updated to keep this +configuration up to date. +.br +It supports adding/setting different kernel command line options as well as +keeping older kernels available for use as a backup. +It can operate on an ESP and root partition which are different from the one +the system is currently booted from. +.SH COMMAND LINE OPTIONS +.TP +.BI "\-e, --esp-path " path +Manually setting the path to the EFI System Partition. +This value is saved into the configuration. +Default is /boot/efi. +.TP +.BI "\-r, --root-path " path +Manually specify the path to the root partition, in case kernelstub is being +run on a system other than the target. +Defaults to /. +.TP +.BI "\-k, --kernel-path " path +Specify the location of the Linux kernel image/vmlinuz. +Defaults to /vmlinuz. +.TP +.BI "\-i, --initrd-path " path +Specify the location of the initrd image. +Defaults to /inird.img. +.TP +.BI "\-o, --options " 'options' +Specify the kernel cmdline options/boot flags. +This will overwrite any existing options in the configuration. +Use single or double quotes to surround multiple options. +This option will save the specified changes into the configuration. +.br +If you want to add/remove options from the existing +configuration, see +.B \-a +and +.B \-d +.TP +.BI "\-a, --add-options " 'options' +Add kernel cmdliing options into the existing configuration. +This will avoid adding duplicate items if they're already present. +Use single or double quotes to surround multiple options. +This option will save the specified changes into the configuration. +.TP +.BI "\-d, --delete-options " 'options' +Remove existing kernel cmdline options from the existing configuration. +If an option specified here is not present in the list of options, kernelstub +will silently ignore it. +Use single or double quotes to surround multiple options. +This option will save the specified changes into the configuration. +.TP +.BI "\-g, --log-file " 'path' +Specify an alternative log file location. +.TP +.B \-h, --help +Prints the usage information and exits. +.TP +.B \-p, --print-config +Prints the current configuration settings and exits. +.TP +.B \-f, --force-update +Forcibly update the system loader.conf file to set the current OS as the +default. +This may change your system default boot order. +.TP +.B \-l, --loader +Creates a systemd-boot compatible loader entry in ESP/loader/entries for the +current OS. +This option is saved in the configuration. +.TP +.B \-n, --no-loader. +Don't create a loader entry for the current OS. +This option disables the behavior of +.B \-l +and is saved in the configuration. +.TP +.B \-s, --stub +Enables automatic management of the kernel efistub bootloader. +This option is saved in the configuration. +.TP +.B \-m, --manage-only +Disables automatic management of the kernel efistub bootloader. +This option disables the behavior of +.B \-s +and is saved in the configuration. +.TP +.B \-v, --verbose +Make program output more verbose. +Up to two +.B \-v +flags can be used at once (additional flags are ignored). +.TP +.SH FILES +.IP \fI/etc/kernelstub/configuration\fP +Default location of the kernelstub configuration file. +The file is a JSON format file with two main configurations in it; +.I 'default' +and +.I 'user'. +The 'esp_path' key is a string, 'config_rev' is an int, 'kernel_options' is a +list of strings, and all other keys are booleans. +.br +It is highly recommended to use the kernelstub utility to modify the +configuration rather than by editing the configuration file directly. +See the +.B CONFIGURATION +section for more details. +.PP +.IP \fI/etc/default/kernelstub\fP +This is a vendor-supplied file that can contain certain options for individual +OSs or hardware-specific values. +.SH CONFIGURATION +Specific configuration defaults may have been modified by your OS developer or +hardware vendor. +.br +Each kernelstub configuration contains the following keys: +.IP \fIkernel_options\fP +This is a list of strings, with each string being an individual kernel cmdline +option. +.br +Default: ["quiet", "splash"] +.br +Configured using the +.B \-a, -d, +and +.B -o +flags. +.PP +.IP \fIesp_path\fP +String - Points to the path where the EFI System Partition is mounted. +.br +Default: "/boot/efi". +.br +Configured using the +.B \-e +flag. +.PP +.IP \fIsetup_loader\fP +boolean - enables or disables installing the loader entry file. +.br +.I false: +(default) Skips installing a loader entry file. +.br +.I true: +Installs a loader entry file. +.br +Configured using the +.B \-l +/ +.B \-n +flags. +.PP +.IP \fImanage_mode\fP +boolean - toggles between installing the efistub bootloader or using +management-only mode. +.br +.I false: +(default) sets up the Linux kernel built-in efistub bootloader in the system +NVRAM. +.br +.I true: +Skips setting up the built-in efistub bootloader. +.br +Configured using the +.B \-s +/ +.B \-m +flags. +.PP +.IP \fIforce_update\fP +boolean - Forcibly overwrites the main systemd-boot configuration on each +update. +.br +.I false: +(default) Does not automatically modify the systemd-boot configuration to make +the current OS the default. +.br +.I true: +Overwrites the systemd-boot configuration on each update to ensure the current +OS is the default. +.br +This option cannot be enabled from the command line and must be enabled in the +configuration file directly. +This is due to its ability cause the system to lose alternate boot entries. +.PP +.IP \fIlive_mode\fP +boolean - Live mode allows updates on run on the live system without triggering +kernelstub. +When live mode is enabled, kernelstub silently exits successfully +to allow software updates to work without overwriting the current boot +configuration. +If kernelstub is run manually, live mode will be automatically disabled. +.I false: +(default) Disables live mode. +.br +.I true: +Enables live mode. +.PP +.IP \fIconfig_rev\fP +integer - Tells kernelstub what format of configuration to expect. +.br +If this value is lower than the current configuration revision supported by the +code, kernelstub will attempt to automatically migrate the configuration to the +new version. +.PP +.SH BUGS +Please report bugs to https://github.com/isantop/kernelstub/issues +.SH EXAMPLE +To set up the kernel efistub bootloader to be the default boot option +.PP +.RS +\f(CWsudo kernelstub\fP +.RE +.PP +To include some output +.PP +.RS +\f(CWsudo kernelstub \-v\fP +.RE +.PP +To use kernelstub as a manager for systemd-boot configurations +.PP +.RS +\f(CWsudo kernelstub \-vlm\fP +.RE +Note that the l and m flags are only required once; they are saved in the +configuration file. +.PP +To add the "quiet" kernel option and remove the "splash" option: +.PP +.RS +\f(CWsudo kernelstub \-a 'quiet' -d 'splash'\fP +.RE +.PP +If you have lost your boot configuration because another OS overwrote your +setup, you can recover like so +.PP +.RS +\f(CWsudo mount /dev/root_partition /mnt\fP +.br +\f(CWsudo mount /dev/esp_partition /mnt/boot/efi\fP +.br +\f(CWsudo kernelstub \\\fP +\f(CW \--root-partition /mnt \\\fP +\f(CW \--esp-path /mnt/boot/efi \\\fP +\f(CW \--kernel-path /mnt/vmlinuz \\\fP +\f(CW \--initrd-path /mnt/initrd.img \\\fP +\f(CW \--options 'quiet splash' \\\fP +\f(CW \-vslf\fP +.RE +.PP +Adjust your mount commands to correctly mount your root and ESP partitions. +.SH AUTHOR +Ian Santopietro +.SH INTERNET RESOURCES +Main website/git repository: https://github.com/isantop/kernelstub +.br +.SH SEE ALSO +efiboomgr(8), systemd-boot(7) diff --git a/debian/changelog b/debian/changelog index 03631fc..9290e82 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,12 @@ +kernelstub (3.2.0) disco; urgency=medium + + * Reformatted output + * Revisions to make code cleaner and more pythonic + * Adds fix for buggy UUID detection code (#22) + * Adds UUID and hostname to entries for better organization (#23) + + -- Ian Santopietro Fri, 17 May 2019 11:43:49 -0600 + kernelstub (3.1.0) bionic; urgency=medium * Add logging to systemd journald diff --git a/debian/kernelstub.manpages b/debian/kernelstub.manpages new file mode 100644 index 0000000..99f5f71 --- /dev/null +++ b/debian/kernelstub.manpages @@ -0,0 +1 @@ +data/kernelstub.1 \ No newline at end of file diff --git a/kernelstub/application.py b/kernelstub/application.py index d1ee448..6402a6d 100755 --- a/kernelstub/application.py +++ b/kernelstub/application.py @@ -39,17 +39,9 @@ kernelstub will load parameters from the /etc/default/kernelstub config file. """ -import logging, os - -systemd_support = False -try: - from systemd.journal import JournalHandler - systemd_support = True - -except ImportError: - pass - +import logging import logging.handlers as handlers +import os from . import drive as Drive from . import nvram as Nvram @@ -57,32 +49,33 @@ from . import installer as Installer from . import config as Config -class CmdLineError(Exception): +SYSTEMD_SUPPORT = False +try: + from systemd.journal import JournalHandler + SYSTEMD_SUPPORT = True + +except ImportError: pass +class CmdLineError(Exception): + """ Exception raised when we can't find any kernel parameters """ + class Kernelstub(): + """ Main Kernelstub Class """ + + def mktable(self, data, padding): + """ + Makes a printable table from a dictionary. - def parse_options(self, options): - for index, option in enumerate(options): - if '"' in option: - matched = False - itr = 1 - while matched == False: - try: - next_option = options[index + itr] - option = '%s %s' % (option, next_option) - options[index + itr] = "" - if '"' in next_option: - matched = True - else: - itr = itr + 1 - except IndexError: - matched = True - options[index] = option - return options - - def main(self, args): # Do the thing + returns: a str containing the table. + """ + table = '' + for i in data: + table += ' {0:{pad}} {1}\n'.format(i, data[i], pad=padding) + return table + def main(self, args): + """ Do the thing """ log_file_path = '/var/log/kernelstub.log' if args.log_file: log_file_path = args.log_file @@ -123,7 +116,7 @@ def main(self, args): # Do the thing log.addHandler(console_log) log.addHandler(file_log) - if systemd_support: + if SYSTEMD_SUPPORT: journald_log = JournalHandler() journald_log.setLevel(file_level) journald_log.setFormatter(stream_fmt) @@ -131,12 +124,18 @@ def main(self, args): # Do the thing log.setLevel(logging.DEBUG) - log.debug('Got command line options: %s' % args) + # Figure out our command line options. + log.debug('Got command line options: %s', args) - # Figure out runtime options - no_run = False if args.dry_run: - no_run = True + log.warning( + 'DEPRECATED!\n\n' + 'The simulate or dry-run option has been removed from ' + 'kernelstub and no longer functions. This will be removed in a ' + 'future version. Since you likely intend no action, we will now ' + 'exit.' + ) + exit() config = Config.Config() configuration = config.config['user'] @@ -151,45 +150,44 @@ def main(self, args): # Do the thing opsys = Opsys.OS() if args.kernel_path: - log.debug( - 'Manually specified kernel path:\n ' + - ' %s' % args.kernel_path) + log.debug('Manual kernel path:\n %s', args.kernel_path) opsys.kernel_path = args.kernel_path else: opsys.kernel_path = os.path.join(root_path, opsys.kernel_name) if args.initrd_path: - log.debug( - 'Manually specified initrd path:\n ' + - ' %s' % args.initrd_path) + log.debug('Manual initrd path:\n %s', args.initrd_path) opsys.initrd_path = args.initrd_path else: opsys.initrd_path = os.path.join(root_path, opsys.initrd_name) if not os.path.exists(opsys.kernel_path): - log.exception('Can\'t find the kernel image! \n\n' - 'Please use the --kernel-path option to specify ' - 'the path to the kernel image') + log.exception( + 'Can\'t find the kernel image! \n\n Please use the ' + '--kernel-path option to specify the path to the kernel image' + ) exit(0) if not os.path.exists(opsys.initrd_path): - log.exception('Can\'t find the initrd image! \n\n' - 'Please use the --initrd-path option to specify ' - 'the path to the initrd image') + log.exception( + 'Can\'t find the initrd image! \n\n Please use the ' + '--initrd-path option to specify the path to the initrd image' + ) exit(0) # Check for kernel parameters. Without them, stop and fail if args.k_options: - configuration['kernel_options'] = self.parse_options(args.k_options.split()) + configuration['kernel_options'] = config.parse_options(args.k_options.split()) else: try: configuration['kernel_options'] except KeyError: - error = ("cmdline was 'InvalidConfig'\n\n" - "Could not find any valid configuration. This " - "probably means that the configuration file is " - "corrupt. Either remove it to regenerate it from" - "default or fix the existing one.") + error = ( + 'cmdline was "InvalidConfig"\n\n Could not find any valid ' + 'configuration. This probably means that the configuration ' + 'file is corrupt. Either remove it to regenerate it from ' + 'default or fix the existing one.' + ) log.exception(error) raise CmdLineError("No Kernel Parameters found") exit(168) @@ -228,16 +226,18 @@ def main(self, args): # Do the thing setup_loader = configuration['setup_loader'] manage_mode = configuration['manage_mode'] force = configuration['force_update'] + live_mode = configuration['live_mode'] except KeyError: log.exception( 'Malformed configuration! \n' - 'The configuration we got is bad, and we can\'nt continue. ' + 'The configuration we got is bad, and we can\'t continue. ' 'Please check the config files and make sure they are correct. ' 'If you can\'t figure it out, then deleting them should fix ' 'the errors and cause kernelstub to regenerate them from ' - 'Default. \n\n You can use "-vv" to get the configuration used.') - log.debug('Configuration we got: \n\n%s' % config.print_config()) + 'Default. \n\n You can use "-vv" to get the configuration used.' + ) + log.debug('Configuration we got: \n\n%s', config.print_config()) exit(169) @@ -257,7 +257,7 @@ def main(self, args): # Do the thing if args.force_update: force = True - if configuration['force_update'] == True: + if configuration['force_update'] is True: force = True log.debug('Structing objects') @@ -267,58 +267,89 @@ def main(self, args): # Do the thing installer = Installer.Installer(nvram, opsys, drive) # Log some helpful information, to file and optionally console - info = ( - ' OS:..................%s %s\n' %(opsys.name_pretty,opsys.version) + - ' Root partition:......%s\n' % drive.root_fs + - ' Root FS UUID:........%s\n' % drive.root_uuid + - ' ESP Path:............%s\n' % esp_path + - ' ESP Partition:.......%s\n' % drive.esp_fs + - ' ESP Partition #:.....%s\n' % drive.esp_num + - ' NVRAM entry #:.......%s\n' % nvram.os_entry_index + - ' Boot Variable #:.....%s\n' % nvram.order_num + - ' Kernel Boot Options:.%s\n' % " ".join(kernel_opts) + - ' Kernel Image Path:...%s\n' % opsys.kernel_path + - ' Initrd Image Path:...%s\n' % opsys.initrd_path + - ' Force-overwrite:.....%s\n' % str(force)) - - log.info('System information: \n\n%s' % info) + data_system = { + 'Root:': drive.root_fs, + 'ESP:': drive.esp_fs, + 'Kernel Path:': opsys.kernel_path, + 'Initrd Path:': opsys.initrd_path, + 'Boot Options:': " ".join(kernel_opts), + } + data_debug = { + 'OS:': "{} {}".format(opsys.name_pretty, opsys.version), + 'ESP Partition #:': drive.esp_num, + 'NVRAM entry #:': nvram.os_entry_index, + 'Boot Variable #:': nvram.order_num, + 'Root FS UUID:': drive.root_uuid, + } + data_config = { + 'Kernel Options:': " ".join(kernel_opts), + 'ESP Path:': esp_path, + 'Install loader config:': setup_loader, + 'Management Mode:': manage_mode, + 'Force Overwrite:': str(force), + 'Live Disk Mode:': live_mode, + 'Config revision:': configuration['config_rev'] + } + if args.print_config: + log.info( + 'System information:\n\n%s', self.mktable(data_system, 22) + ) + log.debug( + 'Debug information:\n\n%s', self.mktable(data_debug, 22) + ) + log.info( + 'Active configuration details:\n\n%s', + self.mktable(data_config, 22) + ) + exit(0) + + log.info( + 'System information:\n\n%s', self.mktable(data_system, 16) + ) + log.debug( + 'Debug information:\n\n%s', self.mktable(data_debug, 16) + ) + log.debug( + 'Active configuration:\n\n%s', self.mktable(data_config, 22) + ) if args.print_config: - all_config = ( - ' ESP Location:..................%s\n' % configuration['esp_path'] + - ' Management Mode:...............%s\n' % configuration['manage_mode'] + - ' Install Loader configuration:..%s\n' % configuration['setup_loader'] + - ' Configuration version:.........%s\n' % configuration['config_rev']) - log.info('Configuration details: \n\n%s' % all_config) + log.info( + 'Active configuration details:\n\n%s', + self.mktable(data_config, 22) + ) exit(0) log.debug('Setting up boot...') - kopts = 'root=UUID=%s ro %s' % (drive.root_uuid, " ".join(kernel_opts)) - log.debug('kopts: %s' % kopts) + kopts = 'root=UUID={uuid} ro {options}'.format( + uuid=drive.root_uuid, + options=" ".join(kernel_opts) + ) + log.debug('kopts: %s', kopts) installer.setup_kernel( kopts, setup_loader=setup_loader, - overwrite=force, - simulate=no_run) + overwrite=force) try: installer.backup_old( kopts, - setup_loader=setup_loader, - simulate=no_run) - except Exception as e: - log.debug('Couldn\'t back up old kernel. \nThis might just mean ' + - 'You don\'t have an old kernel installed. If you do, try ' + - 'with -vv to see debuging information') - log.debug(e) + setup_loader=setup_loader) + except Exception as e_e: + log.debug( + 'Couldn\'t back up old kernel. \nThis might just mean you ' + 'don\'t have an older kernel installed. If you do, try with -vv' + ' to see debugging information' + ) + log.debug(e_e) - installer.copy_cmdline(simulate=no_run) + installer.copy_cmdline() if not manage_mode: - installer.setup_stub(kopts, simulate=no_run) + installer.setup_stub(kopts) log.debug('Saving configuration to file') @@ -328,4 +359,3 @@ def main(self, args): # Do the thing log.debug('Setup complete!\n\n') return 0 - diff --git a/kernelstub/config.py b/kernelstub/config.py index b2c3ed9..a8abcfd 100644 --- a/kernelstub/config.py +++ b/kernelstub/config.py @@ -22,12 +22,20 @@ terms. """ -import json, os, logging +import json +import logging +import os class ConfigError(Exception): - pass + """Exception raised when we can't get a valid configuration.""" class Config(): + """ + Kernelstub Configuration Object + + Loads, parses and saves configuration files and parameters for + kernelstub. + """ config_path = "/etc/kernelstub/configuration" config = {} @@ -51,10 +59,19 @@ def __init__(self, path='/etc/kernelstub/configuration'): os.makedirs('/etc/kernelstub/', exist_ok=True) def load_config(self): + """ + Loads a configuration from a file, or loads the default. + + If the configuration is old, it should be upgraded to a newer version. + If the configuration file doesn't match expected conventions, try to + correct it and warn the user about the issue. + + Returns a valid configuration dictionary. + """ self.log.info('Looking for configuration...') if os.path.exists(self.config_path): - self.log.debug('Checking %s' % self.config_path) + self.log.debug('Checking %s', self.config_path) with open(self.config_path) as config_file: self.config = json.load(config_file) @@ -77,7 +94,7 @@ def load_config(self): self.config['user'] = self.config['default'].copy() try: - self.log.debug('Configuration version: %s' % self.config['user']['config_rev']) + self.log.debug('Configuration version: %s', self.config['user']['config_rev']) if self.config['user']['config_rev'] < self.config_default['default']['config_rev']: self.log.warning("Updating old configuration.") self.config = self.update_config(self.config) @@ -85,12 +102,16 @@ def load_config(self): elif self.config['user']['config_rev'] == self.config_default['default']['config_rev']: self.log.debug("Configuration up to date") # Double-checking in case OEMs do bad things with the config file - if type(self.config['user']['kernel_options']) is str: - self.log.warning('Invalid kernel_options format!\n\n' - 'Usually outdated or buggy maintainer packages from your hardware OEM. ' - 'Contact your hardware vendor to inform them to fix their packages.') + if isinstance(self.config['user']['kernel_options'], str): + self.log.warning( + 'Invalid kernel_options format!\n\n Usually outdated or ' + 'buggy maintainer packages from your hardware OEM. ' + 'Contact your hardware vendor to inform them to fix ' + 'their packages.' + ) try: - self.config['user']['kernel_options'] = self.parse_options(self.config['user']['kernel_options'].split()) + options = self.parse_options(self.config['user']['kernel_options']) + self.config['user']['kernel_options'] = options.split() except: raise ConfigError('Malformed configuration file found!') exit(169) @@ -103,47 +124,62 @@ def load_config(self): return self.config def save_config(self, path='/etc/kernelstub/configuration'): - self.log.debug('Saving configuration to %s' % path) + """Saves the configuration we've used to the file.""" + self.log.debug('Saving configuration to %s', path) with open(path, mode='w') as config_file: json.dump(self.config, config_file, indent=2) - + self.log.debug('Configuration saved!') return 0 def update_config(self, config): + """Updates old configuration to a new version and returns the new one""" if config['user']['config_rev'] < 2: config['user']['live_mode'] = False config['default']['live_mode'] = False if config['user']['config_rev'] < 3: - if type(config['user']['kernel_options']) is str: - config['user']['kernel_options'] = self.parse_options(config['user']['kernel_options'].split()) - if type(config['default']['kernel_options']) is str: - config['default']['kernel_options'] = self.parse_options(config['default']['kernel_options'].split()) + if isinstance(config['user']['kernel_options'], str): + options = self.parse_options(config['user']['kernel_options'].split()) + config['user']['kernel_options'] = options + if isinstance(config['default']['kernel_options'], str): + options = self.parse_options(config['default']['kernel_options'].split()) + config['default']['kernel_options'] = options config['user']['config_rev'] = 3 config['default']['config_rev'] = 3 return config def parse_options(self, options): + """ + Parse a list of kernel options + + Takes a list object and ensure that each item in the list is a single + linux kernel option. Returns the resulting list. + + Positional Argument: + options -- The list of kernel options. + + """ self.log.debug(options) for index, option in enumerate(options): if '"' in option: matched = False itr = 1 - while matched == False: + while matched is False: try: next_option = options[index + itr] - option = '%s %s' % (option, next_option) + option = '{} {}'.format(option, next_option) options[index + itr] = "" if '"' in next_option: matched = True else: - itr = itr + 1 + itr += 1 except IndexError: matched = True options[index] = option return options def print_config(self): + """Returns a printable version of the configuration""" output_config = json.dumps(self.config, indent=2) return output_config diff --git a/kernelstub/drive.py b/kernelstub/drive.py index 0ada384..623a00d 100644 --- a/kernelstub/drive.py +++ b/kernelstub/drive.py @@ -22,15 +22,22 @@ terms. """ -import os, logging +import os +import logging +import subprocess class NoBlockDevError(Exception): - pass + """No Block Device Found Exception""" class UUIDNotFoundError(Exception): - pass + """No UUID for device found Exception""" class Drive(): + """ + Kernelstub Drive Object + + Stores and retrieves information related to the current drive. + """ drive_name = 'none' root_fs = '/' @@ -46,8 +53,8 @@ def __init__(self, root_path="/", esp_path="/boot/efi"): self.esp_path = esp_path self.root_path = root_path - self.log.debug('root path = %s' % self.root_path) - self.log.debug('esp_path = %s' % self.esp_path) + self.log.debug('root path = %s', self.root_path) + self.log.debug('esp_path = %s', self.esp_path) self.mtab = self.get_drives() @@ -56,26 +63,32 @@ def __init__(self, root_path="/", esp_path="/boot/efi"): self.esp_fs = self.get_part_dev(self.esp_path) self.drive_name = self.get_drive_dev(self.esp_fs) self.esp_num = self.esp_fs[-1] - self.root_uuid = self.get_uuid(self.root_fs[5:]) - except NoBlockDevError as e: - self.log.exception('Could not find a block device for the a ' + - 'partition. This is a critical error and we ' + - 'cannot continue.') - self.log.debug(e) + self.root_uuid = self.get_uuid(self.root_path) + self.uuid_name = self.root_uuid.split('-') + self.uuid_name = self.uuid_name[0] + except NoBlockDevError as e_e: + self.log.exception( + 'Could not find a block device for the a partition. This is a' + 'critical error and we cannot continue.' + ) + self.log.debug(e_e) exit(174) - except UUIDNotFoundError as e: - self.log.exception('Could not get a UUID for the a filesystem. ' + - 'This is a critical error and we cannot continue') - self.log.debug(e) + except UUIDNotFoundError as e_e: + self.log.exception( + 'Could not find a block device for the a partition. This is a ' + 'critical error and we cannot continue.' + ) + self.log.debug(e_e) exit(177) - self.log.debug('Root is on /dev/%s' % self.drive_name) - self.log.debug('root_fs = %s ' % self.root_fs) - self.log.debug('root_uuid is %s' % self.root_uuid) + self.log.debug('Root is on /dev/%s', self.drive_name) + self.log.debug('root_fs = %s ', self.root_fs) + self.log.debug('root_uuid is %s', self.root_uuid) def get_drives(self): + """Returns a list of information about mounted filesystems.""" self.log.debug('Getting a list of drives') with open('/proc/mounts', mode='r') as proc_mounts: mtab = proc_mounts.readlines() @@ -84,33 +97,35 @@ def get_drives(self): return mtab def get_part_dev(self, path): - self.log.debug('Getting the block device file for %s' % path) + """Returns a block device file for `path`.""" + self.log.debug('Getting the block device file for %s', path) for mount in self.mtab: drive = mount.split(" ") if drive[1] == path: part_dev = os.path.realpath(drive[0]) - self.log.debug('%s is on %s' % (path, part_dev)) + self.log.debug('%s is on %s', path, part_dev) return part_dev - raise NoBlockDevError('Couldn\'t find the block device for %s' % path) + raise NoBlockDevError( + 'Couldn\'t find the block device for {}'.format(path) + ) - def get_drive_dev(self, esp): + def get_drive_dev(self, blockdev): + """Returns a block device for the drive partition blockdev is on.""" # Ported from bash, out of @jackpot51's firmware updater - efi_name = os.path.basename(esp) - efi_sys = os.readlink('/sys/class/block/%s' % efi_name) + efi_name = os.path.basename(blockdev) + efi_sys = os.readlink('/sys/class/block/{}'.format(efi_name)) disk_sys = os.path.dirname(efi_sys) disk_name = os.path.basename(disk_sys) - self.log.debug('ESP is a partition on /dev/%s' % disk_name) + self.log.debug('ESP is a partition on /dev/%s', disk_name) return disk_name - def get_uuid(self, fs): - all_uuids = os.listdir('/dev/disk/by-uuid') - self.log.debug('Looking for UUID for %s' % fs) - self.log.debug('List of UUIDs:\n%s' % all_uuids) - - for uuid in all_uuids: - uuid_path = os.path.join('/dev/disk/by-uuid', uuid) - if fs in os.path.realpath(uuid_path): - return uuid - - raise UUIDNotFoundError - + def get_uuid(self, path): + self.log.debug('Looking for UUID for path %s' % path) + try: + args = ['findmnt', '-n', '-o', 'uuid', '--mountpoint', path] + result = subprocess.run(args, stdout=subprocess.PIPE) + uuid = result.stdout.decode('ASCII') + uuid = uuid.strip() + return uuid + except OSError as e: + raise UUIDNotFoundError from e diff --git a/kernelstub/installer.py b/kernelstub/installer.py index 395cce0..2044542 100644 --- a/kernelstub/installer.py +++ b/kernelstub/installer.py @@ -22,13 +22,20 @@ terms. """ -import os, shutil, logging +import logging +import os +import shutil class FileOpsError(Exception): - pass + """Exception thrown when a file operation fails.""" class Installer(): + """ + Installer class for Kernelstub. + Takes the information processed by kernelstub and performs the boot + configuration and setup. + """ loader_dir = '/boot/efi/loader' entry_dir = '/boot/efi/loader/entries' os_dir_name = 'linux-kernelstub' @@ -46,7 +53,7 @@ def __init__(self, nvram, opsys, drive): self.work_dir = os.path.join(self.drive.esp_path, "EFI") self.loader_dir = os.path.join(self.drive.esp_path, "loader") self.entry_dir = os.path.join(self.loader_dir, "entries") - self.os_dir_name = "%s-%s" % (self.opsys.name, self.drive.root_uuid) + self.os_dir_name = "{}-{}".format(self.opsys.name, self.drive.root_uuid) self.os_folder = os.path.join(self.work_dir, self.os_dir_name) self.kernel_dest = os.path.join(self.os_folder, self.opsys.kernel_name) self.initrd_dest = os.path.join(self.os_folder, self.opsys.initrd_name) @@ -57,71 +64,84 @@ def __init__(self, nvram, opsys, drive): os.makedirs(self.entry_dir) - def backup_old(self, kernel_opts, setup_loader=False, simulate=False): + def backup_old(self, kernel_opts, setup_loader=False): + """Copy the previous kernel (if present) into the ESP.""" self.log.info('Backing up old kernel') - kernel_name = "%s-previous.efi" % self.opsys.kernel_name + kernel_name = "{}-previous.efi".format(self.opsys.kernel_name) kernel_dest = os.path.join(self.os_folder, kernel_name) try: self.copy_files( - '%s.old' % self.opsys.kernel_path, - kernel_dest, - simulate=simulate) - except: - self.log.debug('Couldn\'t back up old kernel. There\'s ' + - 'probably only one kernel installed.') + '{}.old'.format(self.opsys.kernel_path), + kernel_dest) + except OSError: + self.log.debug( + 'Couldn\'t back up old kernel. There\'s probably only one ' + 'kernel installed.' + ) self.old_kernel = False - pass - initrd_name = "%s-previous" % self.opsys.initrd_name + initrd_name = "{}-previous".format(self.opsys.initrd_name) initrd_dest = os.path.join(self.os_folder, initrd_name) try: self.copy_files( - '%s.old' % self.opsys.initrd_path, - initrd_dest, - simulate=simulate) - except: - self.log.debug('Couldn\'t back up old initrd.img. There\'s ' + - 'probably only one kernel installed.') + '{}.old'.format(self.opsys.initrd_path), + initrd_dest) + except OSError: + self.log.debug( + 'Couldn\'t back up old kernel. There\'s probably only one ' + 'kernel installed.' + ) self.old_kernel = False - pass if setup_loader and self.old_kernel: self.ensure_dir(self.entry_dir) - linux_line = '/EFI/%s-%s/%s-previous.efi' % (self.opsys.name, - self.drive.root_uuid, - self.opsys.kernel_name) - initrd_line = '/EFI/%s-%s/%s-previous' % (self.opsys.name, - self.drive.root_uuid, - self.opsys.initrd_name) + linux_line = '/EFI/{}-{}/{}-previous.efi'.format( + self.opsys.name, + self.drive.root_uuid, + self.opsys.kernel_name + ) + initrd_line = '/EFI/{}-{}/{}-previous'.format( + self.opsys.name, + self.drive.root_uuid, + self.opsys.initrd_name + ) + entry_file = '{}-{}({})-oldkern'.format( + self.opsys.name, + self.opsys.hostname, + self.drive.uuid_name + ) self.make_loader_entry( - self.opsys.name_pretty, + '{} ({}) - previous kernel'.format(self.opsys.name_pretty, self.opsys.hostname), linux_line, initrd_line, kernel_opts, - os.path.join(self.entry_dir, '%s-oldkern' % self.opsys.name)) + os.path.join( + self.entry_dir, entry_file + ) + ) - def setup_kernel(self, kernel_opts, setup_loader=False, overwrite=False, simulate=False): + def setup_kernel(self, kernel_opts, setup_loader=False, overwrite=False): + """Copy the active kernel into the ESP.""" self.log.info('Copying Kernel into ESP') self.kernel_dest = os.path.join( self.os_folder, - "%s.efi" % self.opsys.kernel_name) - self.ensure_dir(self.os_folder, simulate=simulate) - self.log.debug('kernel being copied to %s' % self.kernel_dest) + "{}.efi".format(self.opsys.kernel_name)) + self.ensure_dir(self.os_folder) + self.log.debug('kernel being copied to %s', self.kernel_dest) try: self.copy_files( self.opsys.kernel_path, - self.kernel_dest, - simulate=simulate) + self.kernel_dest) - except FileOpsError as e: + except FileOpsError as e_e: self.log.exception( - 'Couldn\'t copy the kernel onto the ESP!\n' + - 'This is a critical error and we cannot continue. Check your ' + - 'settings to see if there is a typo. Otherwise, check ' + + 'Couldn\'t copy the kernel onto the ESP!\n' + 'This is a critical error and we cannot continue. Check your ' + 'settings to see if there is a typo. Otherwise, check ' 'permissions and try again.') - self.log.debug(e) + self.log.debug(e_e) exit(170) self.log.info('Copying initrd.img into ESP') @@ -129,116 +149,118 @@ def setup_kernel(self, kernel_opts, setup_loader=False, overwrite=False, simulat try: self.copy_files( self.opsys.initrd_path, - self.initrd_dest, - simulate=simulate) - - except FileOpsError as e: - self.log.exception('Couldn\'t copy the initrd onto the ESP!\n' + - 'This is a critical error and we cannot ' + - 'continue. Check your settings to see if ' + - 'there is a typo. Otherwise, check permissions ' + - 'and try again.') - self.log.debug(e) + self.initrd_dest) + + except FileOpsError as e_e: + self.log.exception( + 'Couldn\'t copy the initrd onto the ESP!\n This is a critical ' + 'error and we cannot continue. Check your settings to see if ' + 'there is a typo. Otherwise, check permissions and try again.' + ) + self.log.debug(e_e) exit(171) self.log.debug('Copy complete') if setup_loader: self.log.info('Setting up loader.conf configuration') - linux_line = '/EFI/%s-%s/%s.efi' % (self.opsys.name, - self.drive.root_uuid, - self.opsys.kernel_name) - initrd_line = '/EFI/%s-%s/%s' % (self.opsys.name, - self.drive.root_uuid, - self.opsys.initrd_name) - if simulate: - self.log.info("Simulate creation of entry...") - self.log.info('Loader entry: %s/%s-current\n' %(self.entry_dir, - self.opsys.name) + - 'title %s\n' % self.opsys.name_pretty + - 'linux %s\n' % linux_line + - 'initrd %s\n' % initrd_line + - 'options %s\n' % kernel_opts) - return 0 + linux_line = '/EFI/{}-{}/{}.efi'.format( + self.opsys.name, + self.drive.root_uuid, + self.opsys.kernel_name + ) + initrd_line = '/EFI/{}-{}/{}'.format( + self.opsys.name, + self.drive.root_uuid, + self.opsys.initrd_name + ) if not overwrite: - if not os.path.exists('%s/loader.conf' % self.loader_dir): + if not os.path.exists('{}/loader.conf'.format(self.loader_dir)): overwrite = True if overwrite: self.ensure_dir(self.loader_dir) with open( - '%s/loader.conf' % self.loader_dir, mode='w') as loader: - - default_line = 'default %s-current\n' % self.opsys.name + '{}/loader.conf'.format(self.loader_dir), mode='w' + ) as loader: + default_name = '{}-{}({})-current'.format( + self.opsys.name, + self.opsys.hostname, + self.drive.uuid_name + ) + default_line = 'default {}\n'.format(default_name) loader.write(default_line) self.ensure_dir(self.entry_dir) + entry_file = '{}-{}({})-current'.format( + self.opsys.name, + self.opsys.hostname, + self.drive.uuid_name + ) self.make_loader_entry( - self.opsys.name_pretty, + '{} ({})'.format(self.opsys.name_pretty, self.opsys.hostname), linux_line, initrd_line, kernel_opts, - os.path.join(self.entry_dir, '%s-current' % self.opsys.name)) - - - - + os.path.join( + self.entry_dir, entry_file + ) + ) - def setup_stub(self, kernel_opts, simulate=False): + def setup_stub(self, kernel_opts): + """Set up the kernel efistub bootloader.""" self.log.info("Setting up Kernel EFISTUB loader...") - self.copy_cmdline(simulate=simulate) + self.copy_cmdline() self.nvram.update() if self.nvram.os_entry_index >= 0: self.log.info("Deleting old boot entry") - self.nvram.delete_boot_entry(self.nvram.order_num, simulate) + self.nvram.delete_boot_entry(self.nvram.order_num) else: self.log.debug("No old entry found, skipping removal.") - self.nvram.add_entry(self.opsys, self.drive, kernel_opts, simulate) + self.nvram.add_entry(self.opsys, self.drive, kernel_opts) self.nvram.update() nvram_lines = "\n".join(self.nvram.nvram) - self.log.info('NVRAM configured, new values: \n\n%s\n' % nvram_lines) + self.log.info('NVRAM configured, new values: \n\n%s\n', nvram_lines) - def copy_cmdline(self, simulate): + def copy_cmdline(self): + """Copy the current boot options into the ESP.""" self.copy_files( '/proc/cmdline', - self.os_folder, - simulate = simulate + self.os_folder ) def make_loader_entry(self, title, linux, initrd, options, filename): - self.log.info('Making entry file for %s' % title) - with open('%s.conf' % filename, mode='w') as entry: - entry.write('title %s\n' % title) - entry.write('linux %s\n' % linux) - entry.write('initrd %s\n' % initrd) - entry.write('options %s\n' % options) + """Create a systemd-boot loader entry file.""" + self.log.info('Making entry file for %s', title) + with open('{}.conf'.format(filename), mode='w') as entry: + entry.write('title {}\n'.format(title)) + entry.write('linux {}\n'.format(linux)) + entry.write('initrd {}\n'.format(initrd)) + entry.write('options {}\n'.format(options)) self.log.debug('Entry created!') - def ensure_dir(self, directory, simulate=False): - if not simulate: - try: - os.makedirs(directory, exist_ok=True) - return True - except Exception as e: - self.log.exception('Couldn\'t make sure %s exists.' % directory) - self.log.debug(e) - return False - - def copy_files(self, src, dest, simulate): # Copy file src into dest - if simulate: - self.log.info('Simulate copying: %s => %s' % (src, dest)) + def ensure_dir(self, directory): + """Ensure that a folder exists.""" + try: + os.makedirs(directory, exist_ok=True) return True - else: - try: - self.log.debug('Copying: %s => %s' % (src, dest)) - shutil.copy(src, dest) - return True - except Exception as e: - self.log.debug(e) - raise FileOpsError("Could not copy one or more files.") - return False + except Exception as e_e: + self.log.exception('Couldn\'t make sure %s exists.', directory) + self.log.debug(e_e) + return False + + def copy_files(self, src, dest): + """Copy src into dest.""" + try: + self.log.debug('Copying: %s => %s', src, dest) + shutil.copy(src, dest) + return True + except Exception as e_e: + self.log.debug(e_e) + raise FileOpsError("Could not copy one or more files.") + return False diff --git a/kernelstub/nvram.py b/kernelstub/nvram.py index 2e2be85..e1f2c5f 100644 --- a/kernelstub/nvram.py +++ b/kernelstub/nvram.py @@ -22,10 +22,15 @@ terms. """ -import subprocess, logging +import logging +import subprocess class NVRAM(): + """ + Kernelstub NVRAM object. + Provides methods for interacting with the system NVRAM variables. + """ os_entry_index = -1 os_label = "" nvram = [] @@ -35,10 +40,11 @@ def __init__(self, name, version): self.log = logging.getLogger('kernelstub.NVRAM') self.log.debug('loaded kernelstub.NVRAM') - self.os_label = "%s %s" % (name, version) + self.os_label = "{} {}".format(name, version) self.update() def update(self): + """Make sure we're looking at the correct NVRAM entry.""" self.log.debug('Updating NVRAM info') self.nvram = self.get_nvram() self.find_os_entry(self.nvram, self.os_label) @@ -46,6 +52,7 @@ def update(self): self.order_num = str(self.nvram[self.os_entry_index])[4:8] def get_nvram(self): + """Retrieve NVRAM data from system.""" self.log.debug('Getting NVRAM data') command = [ '/usr/bin/sudo', @@ -53,70 +60,74 @@ def get_nvram(self): ] try: return subprocess.check_output(command).decode('UTF-8').split('\n') - except Exception as e: + except Exception as e_e: self.log.exception('Failed to retrieve NVRAM data. Are you running in a chroot?') - self.log.debug(e) + self.log.debug(e_e) return [] def find_os_entry(self, nvram, os_label): - self.log.debug('Finding NVRAM entry for %s' % os_label) + """Find an NVRAM entry for the current OS.""" + self.log.debug('Finding NVRAM entry for %s', os_label) self.os_entry_index = -1 find_index = self.os_entry_index for entry in nvram: - find_index = find_index + 1 + find_index += 1 if os_label in entry: self.os_entry_index = find_index - self.log.debug('Entry found! Index: %s' % self.os_entry_index) + self.log.debug('Entry found! Index: %s', self.os_entry_index) return find_index - def add_entry(self, this_os, this_drive, kernel_opts, simulate=False): + def add_entry(self, this_os, this_drive, kernel_opts): + """Add an entry into the NVRAM.""" self.log.info('Creating NVRAM entry') - device = '/dev/%s' % this_drive.drive_name + device = '/dev/{}'.format(this_drive.drive_name) esp_num = this_drive.esp_num - entry_label = '%s %s' % (this_os.name, this_os.version) - entry_linux = '\\EFI\\%s-%s\\vmlinuz.efi' % (this_os.name, this_drive.root_uuid) - entry_initrd = 'EFI/%s-%s/initrd.img' % (this_os.name, this_drive.root_uuid) + entry_label = '{} {}'.format(this_os.name, this_os.version) + entry_linux = '\\EFI\\{}-{}\\vmlinuz.efi'.format(this_os.name, this_drive.root_uuid) + entry_initrd = 'EFI/{}-{}/initrd.img'.format(this_os.name, this_drive.root_uuid) command = [ '/usr/bin/sudo', 'efibootmgr', '-c', '-d', device, '-p', esp_num, - '-L', '%s' % entry_label, - '-l', '%s' % entry_linux, + '-L', '{}'.format(entry_label), + '-l', '{}'.format(entry_linux), '-u', - 'initrd=%s %s' % (entry_initrd, kernel_opts) + 'initrd={} {}'.format(entry_initrd, kernel_opts) ] - self.log.debug('NVRAM command:\n%s' % command) - if not simulate: - try: - subprocess.run(command) - except Exception as e: - self.log.exception('Couldn\'t create boot entry for kernel! ' + - 'This means that the system will not boot from ' + - 'the new kernel directly. Do NOT reboot without ' + - 'an alternate bootloader configured or fixing ' + - 'this problem. More information is available in ' + - 'the log or by running again with -vv') - self.log.debug(e) - exit(172) + self.log.debug('NVRAM command:\n%s', command) + try: + subprocess.run(command) + except subprocess.SubprocessError as e_e: + self.log.exception( + 'Couldn\'t create boot entry for kernel! This means that ' + 'the system will not boot from the new kernel directly. Do ' + 'NOT reboot without an alternate bootloader configured or ' + 'fixing this problem. More information is available in the ' + 'log or by running again with -vv' + ) + self.log.debug(e_e) + exit(172) self.update() - def delete_boot_entry(self, index, simulate): - self.log.info('Deleting old boot entry: %s' % index) + def delete_boot_entry(self, index): + """Delete an entry from the NVRAM.""" + self.log.info('Deleting old boot entry: %s', index) command = ['/usr/bin/sudo', 'efibootmgr', '-B', '-b', str(index)] - self.log.debug('NVRAM command:\n%s' % command) - if not simulate: - try: - subprocess.run(command) - except Exception as e: - self.log.exception('Couldn\'t delete old boot entry %s. ' % index + - 'This could cause problems, so kernelstub will ' + - 'not continue. Check again with -vv for more info.') - self.log.debug(e) - exit(173) + self.log.debug('NVRAM command:\n%s', command) + try: + subprocess.run(command) + except Exception as e_e: + self.log.exception( + 'Couldn\'t delete old boot entry %s. This could cause ' + 'problems, so kernelstub will not continue. Check again ' + 'with -vv for more info.', index + ) + self.log.debug(e_e) + exit(173) self.update() diff --git a/kernelstub/opsys.py b/kernelstub/opsys.py index 28b53f5..e06f765 100644 --- a/kernelstub/opsys.py +++ b/kernelstub/opsys.py @@ -25,7 +25,11 @@ import platform class OS(): + """ + Kernelstub OS object. + Provides helper functions for getting and storing OS information. + """ name_pretty = "Linux" name = "Linux" version = "1.0" @@ -33,6 +37,7 @@ class OS(): kernel_name = 'vmlinuz' initrd_name = 'initrd.img' kernel_release = platform.release() + hostname = platform.node() kernel_path = '/vmlinuz' initrd_path = '/initrd.img' @@ -43,8 +48,12 @@ def __init__(self): self.cmdline = self.get_os_cmdline() def clean_names(self, name): - # This is a list of characters we can't/don't want to have in technical - # names for the OS. name_pretty will still have them. + """ + Remove bad characters from names. + + This is a list of characters we can't/don't want to have in technical + names for the OS. name_pretty will still have them. + """ badchar = { ' ' : '_', '~' : '-', @@ -88,6 +97,7 @@ def clean_names(self, name): return name def get_os_cmdline(self): + """Gets a clean list of current OS boot options.""" with open('/proc/cmdline') as cmdline_file: cmdline_list = cmdline_file.readlines()[0].split(" ") @@ -100,6 +110,7 @@ def get_os_cmdline(self): return cmdline def get_os_name(self): + """Get the current OS name.""" os_release = self.get_os_release() for item in os_release: if item.startswith('NAME='): @@ -107,13 +118,15 @@ def get_os_name(self): return self.strip_quotes(name[:-1]) def get_os_version(self): + """Get the current OS version.""" os_release = self.get_os_release() for item in os_release: if item.startswith('VERSION_ID='): - version = item.split('=')[1] + version = item.split('=')[1] return self.strip_quotes(version[:-1]) def strip_quotes(self, value): + """Return `value` without quotation marks.""" new_value = value if value.startswith('"'): new_value = new_value[1:] @@ -122,13 +135,16 @@ def strip_quotes(self, value): return new_value def get_os_release(self): + """Return a list with the current OS release data.""" try: with open('/etc/os-release') as os_release_file: os_release = os_release_file.readlines() except FileNotFoundError: - os_release = ['NAME="%s"\n' % self.name, - 'ID=linux\n', - 'ID_LIKE=linux\n', - 'VERSION_ID="%s"\n' % self.version] + os_release = [ + 'NAME="{}"\n'.format(self.name), + 'ID=linux\n', + 'ID_LIKE=linux\n', + 'VERSION_ID="{}"\n'.format(self.version) + ] return os_release diff --git a/setup.py b/setup.py index 9d48aba..9c0dedb 100755 --- a/setup.py +++ b/setup.py @@ -21,21 +21,27 @@ from distutils.core import setup from distutils.cmd import Command -import os, subprocess, sys +import os +import subprocess +import sys TREE = os.path.dirname(os.path.abspath(__file__)) DIRS = [ 'kernelstub', - 'bin'] + 'bin' +] def run_under_same_interpreter(opname, script, args): + """Re-run with the same as current interpreter.""" print('\n** running: {}...'.format(script), file=sys.stderr) if not os.access(script, os.R_OK | os.X_OK): - print('ERROR: cannot read and execute: {!r}'.format(script), + print( + 'ERROR: cannot read and execute: {!r}'.format(script), file=sys.stderr ) - print('Consider running `setup.py test --skip-{}`'.format(opname), + print( + 'Consider running `setup.py test --skip-{}`'.format(opname), file=sys.stderr ) sys.exit(3) @@ -45,6 +51,7 @@ def run_under_same_interpreter(opname, script, args): print('** PASSED: {}\n'.format(script), file=sys.stderr) def run_pyflakes3(): + """Run a round of pyflakes3.""" script = '/usr/bin/pyflakes3' names = [ 'setup.py', @@ -55,6 +62,7 @@ def run_pyflakes3(): class Test(Command): + """Basic sanity checks on our code.""" description = 'run pyflakes3' user_options = [ @@ -72,8 +80,9 @@ def run(self): if not self.skip_flakes: run_pyflakes3() -setup(name='kernelstub', - version='3.1.0', +setup( + name='kernelstub', + version='3.2.0', description='Automatic kernel efistub manager for UEFI', url='https://launchpad.net/kernelstub', author='Ian Santopietro', @@ -85,5 +94,6 @@ def run(self): data_files=[ ('/etc/kernel/postinst.d', ['data/kernel/zz-kernelstub']), ('/etc/initramfs/post-update.d', ['data/initramfs/zz-kernelstub']), - ('/etc/default', ['data/config/kernelstub.SAMPLE'])] - ) + ('/etc/default', ['data/config/kernelstub.SAMPLE']) + ] +)