Source code for boom.command

# Copyright (C) 2017 Red Hat, Inc., Bryn M. Reeves <bmr@redhat.com>
#
# command.py - Boom BLS bootloader command interface
#
# This file is part of the boom project.
#
# This copyrighted material is made available to anyone wishing to use,
# modify, copy, or redistribute it subject to the terms and conditions
# of the GNU General Public License v.2.
#
# You should have received a copy of the GNU Lesser General Public License
# along with this program; if not, write to the Free Software Foundation,
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
"""The ``boom.command`` module provides both the Boom command line
interface infrastructure, and a simple procedural interface to the
``boom`` library modules.

The procedural interface is used by the ``boom`` command line tool,
and may be used by application programs, or interactively in the
Python shell by users who do not require all the features present
in the Boom object API.

In addition the module contains definitions for ``BoomReport``
object types and fields that may be of use in implementing custom
reports using the ``boom.report`` module.
"""
from __future__ import print_function

from os import environ, uname, getcwd
from os.path import basename, exists as path_exists, isabs, join, sep
from argparse import ArgumentParser
from stat import filemode
import platform
import logging
import re

from boom import *
from boom.osprofile import *
from boom.report import *
from boom.bootloader import *
from boom.hostprofile import *
from boom.legacy import *
from boom.config import *
from boom.cache import *
from boom.mounts import *

#: The environment variable from which to take the location of the
#: ``/boot`` file system.
BOOM_BOOT_PATH_ENV = "BOOM_BOOT_PATH"

#: Path to the system machine-id file
_MACHINE_ID = "/etc/machine-id"
#: Path to the legacy system machine-id file
_DBUS_MACHINE_ID = "/var/lib/dbus/machine-id"

# Module logging configuration
_log = logging.getLogger(__name__)
_log.set_debug_mask(BOOM_DEBUG_COMMAND)

_log_debug = _log.debug
_log_debug_cmd = _log.debug_masked
_log_info = _log.info
_log_warn = _log.warning
_log_error = _log.error

_default_log_level = logging.WARNING
_console_handler = None


#
# Reporting object types
#


class BoomReportObj(object):
    """BoomReportObj()
    The universal object type used for all reports generated by
    the Boom CLI. Individual fields map to one of the contained
    objects via the ``BoomReportObjType`` object's ``data_fn``
    method. It is an error to attempt to report an object that
    is undefined: the BoomReportObj used for a report must
    contain values for each object type that the specified list
    of fields will attempt to access.

    This allows a single report to include fields from both a
    ``BootEntry`` object and an attached ``OsProfile``.
    """

    be = None
    osp = None
    hp = None
    ce = None

    def __init__(
        self, boot_entry=None, os_profile=None, host_profile=None, cache_entry=None
    ):
        """Initialise new BoomReportObj objects.

        Construct a new BoomReportObj object containing the
        specified BootEntry and or OsProfile objects.

        :returns: a new BoomReportObj.
        :rtype: ``BoomReportObj``
        """
        self.be = boot_entry
        self.osp = os_profile
        self.hp = host_profile
        self.ce = cache_entry


#: BootEntry report object type
BR_ENTRY = 1
#: OsProfile report object type
BR_PROFILE = 2
#: BootParams report object type
BR_PARAMS = 4
#: HostProfile report object type
BR_HOST = 8
#: CacheEntry report object type
BR_CACHE = 16

#: Report object type table for ``boom.command`` reports.
_report_obj_types = [
    BoomReportObjType(BR_ENTRY, "Boot loader entries", "entry_", lambda o: o.be),
    BoomReportObjType(BR_PROFILE, "OS profiles", "profile_", lambda o: o.osp),
    BoomReportObjType(BR_PARAMS, "Boot parameters", "param_", lambda o: o.be.bp),
    BoomReportObjType(BR_HOST, "Host profiles", "host_", lambda o: o.hp),
    BoomReportObjType(BR_CACHE, "Cache entries", "cache_", lambda o: o.ce),
]

#
# Reporting field definitions
#

#: Fields derived from OsProfile data.
_profile_fields = [
    BoomFieldType(
        BR_PROFILE,
        "osid",
        "OsID",
        "OS identifier",
        7,
        REP_SHA,
        lambda f, d: f.report_sha(d.os_id),
    ),
    BoomFieldType(
        BR_PROFILE,
        "osname",
        "Name",
        "OS name",
        24,
        REP_STR,
        lambda f, d: f.report_str(d.os_name),
    ),
    BoomFieldType(
        BR_PROFILE,
        "osshortname",
        "OsShortName",
        "OS short name",
        12,
        REP_STR,
        lambda f, d: f.report_str(d.os_short_name),
    ),
    BoomFieldType(
        BR_PROFILE,
        "osversion",
        "OsVersion",
        "OS version",
        10,
        REP_STR,
        lambda f, d: f.report_str(d.os_version),
    ),
    BoomFieldType(
        BR_PROFILE,
        "osversion_id",
        "VersionID",
        "Version identifier",
        10,
        REP_STR,
        lambda f, d: f.report_str(d.os_version_id),
    ),
    BoomFieldType(
        BR_PROFILE,
        "unamepattern",
        "UnamePattern",
        "UTS name pattern",
        12,
        REP_STR,
        lambda f, d: f.report_str(d.uname_pattern),
    ),
    BoomFieldType(
        BR_PROFILE,
        "kernelpattern",
        "KernPattern",
        "Kernel image pattern",
        13,
        REP_STR,
        lambda f, d: f.report_str(d.kernel_pattern),
    ),
    BoomFieldType(
        BR_PROFILE,
        "initrdpattern",
        "InitrdPattern",
        "Initrd pattern",
        13,
        REP_STR,
        lambda f, d: f.report_str(d.initramfs_pattern),
    ),
    BoomFieldType(
        BR_PROFILE,
        "lvm2opts",
        "LVM2Opts",
        "LVM2 options",
        12,
        REP_STR,
        lambda f, d: f.report_str(d.root_opts_lvm2),
    ),
    BoomFieldType(
        BR_PROFILE,
        "btrfsopts",
        "BTRFSOpts",
        "BTRFS options",
        13,
        REP_STR,
        lambda f, d: f.report_str(d.root_opts_btrfs),
    ),
    BoomFieldType(
        BR_PROFILE,
        "options",
        "Options",
        "Kernel options",
        24,
        REP_STR,
        lambda f, d: f.report_str(d.options),
    ),
    BoomFieldType(
        BR_PROFILE,
        "profilepath",
        "ProfilePath",
        "On-disk profile path",
        12,
        REP_STR,
        lambda f, d: f.report_str(d._profile_path()),
    ),
]

_default_profile_fields = "osid,osname,osversion"
_verbose_profile_fields = _default_profile_fields + ",unamepattern,options"

_host_fields = [
    BoomFieldType(
        BR_HOST,
        "hostid",
        "HostID",
        "Host identifier",
        7,
        REP_SHA,
        lambda f, d: f.report_sha(d.host_id),
    ),
    BoomFieldType(
        BR_HOST,
        "machineid",
        "MachineID",
        "Machine identifier",
        10,
        REP_SHA,
        lambda f, d: f.report_sha(d.disp_machine_id),
    ),
    BoomFieldType(
        BR_HOST,
        "osid",
        "OsID",
        "OS identifier",
        7,
        REP_SHA,
        lambda f, d: f.report_sha(d.os_id),
    ),
    BoomFieldType(
        BR_HOST,
        "hostname",
        "HostName",
        "Host name",
        28,
        REP_STR,
        lambda f, d: f.report_str(d.host_name),
    ),
    BoomFieldType(
        BR_HOST,
        "label",
        "Label",
        "Host label",
        12,
        REP_STR,
        lambda f, d: f.report_str(d.label),
    ),
    BoomFieldType(
        BR_HOST,
        "kernelpattern",
        "KernPattern",
        "Kernel image pattern",
        13,
        REP_STR,
        lambda f, d: f.report_str(d.kernel_pattern),
    ),
    BoomFieldType(
        BR_HOST,
        "initrdpattern",
        "InitrdPattern",
        "Initrd pattern",
        13,
        REP_STR,
        lambda f, d: f.report_str(d.initramfs_pattern),
    ),
    BoomFieldType(
        BR_HOST,
        "lvm2opts",
        "LVM2Opts",
        "LVM2 options",
        12,
        REP_STR,
        lambda f, d: f.report_str(d.root_opts_lvm2),
    ),
    BoomFieldType(
        BR_HOST,
        "btrfsopts",
        "BTRFSOpts",
        "BTRFS options",
        13,
        REP_STR,
        lambda f, d: f.report_str(d.root_opts_btrfs),
    ),
    BoomFieldType(
        BR_HOST,
        "options",
        "Options",
        "Kernel options",
        24,
        REP_STR,
        lambda f, d: f.report_str(d.options),
    ),
    BoomFieldType(
        BR_HOST,
        "profilepath",
        "ProfilePath",
        "On-disk profile path",
        12,
        REP_STR,
        lambda f, d: f.report_str(d._profile_path()),
    ),
    BoomFieldType(
        BR_HOST,
        "addopts",
        "AddOptions",
        "Added Options",
        12,
        REP_STR,
        lambda f, d: f.report_str(d.add_opts),
    ),
    BoomFieldType(
        BR_HOST,
        "delopts",
        "DelOptions",
        "Deleted Options",
        12,
        REP_STR,
        lambda f, d: f.report_str(d.del_opts),
    ),
]

_default_host_fields = "hostid,hostname,machineid,osid"
_verbose_host_fields = _default_host_fields + ",options,addopts,delopts"


def _int_if_val(val):
    """Return an int if val is defined or None otherwise.

    A TypeError exception is raised if val is defined but does
    not contain a parsable integer value.

    :param val: The value to convert
    :returns: None if val is None or an integer representation of
              the string val
    :raises: TypeError is val cannot be converted to an int
    """
    return int(val) if val is not None else None


def _bool_to_yes_no(bval):
    """Return the string 'yes' if ``bval`` is ``True`` or 'no' otherwise."""
    return "yes" if bval else "no"


#: Fields derived from BootEntry data.
_entry_fields = [
    BoomFieldType(
        BR_ENTRY,
        "bootid",
        "BootID",
        "Boot identifier",
        7,
        REP_SHA,
        lambda f, d: f.report_sha(d.boot_id),
    ),
    BoomFieldType(
        BR_ENTRY,
        "title",
        "Title",
        "Entry title",
        24,
        REP_STR,
        lambda f, d: f.report_str(d.title),
    ),
    BoomFieldType(
        BR_ENTRY,
        "options",
        "Options",
        "Kernel options",
        24,
        REP_STR,
        lambda f, d: f.report_str(d.options),
    ),
    BoomFieldType(
        BR_ENTRY,
        "kernel",
        "Kernel",
        "Kernel image",
        32,
        REP_STR,
        lambda f, d: f.report_str(d.linux),
    ),
    BoomFieldType(
        BR_ENTRY,
        "initramfs",
        "Initramfs",
        "Initramfs image",
        40,
        REP_STR,
        lambda f, d: f.report_str(d.initrd),
    ),
    BoomFieldType(
        BR_ENTRY,
        "machineid",
        "MachineID",
        "Machine identifier",
        10,
        REP_SHA,
        lambda f, d: f.report_sha(d.machine_id),
    ),
    BoomFieldType(
        BR_ENTRY,
        "entrypath",
        "EntryPath",
        "On-disk entry path",
        12,
        REP_STR,
        lambda f, d: f.report_str(d.entry_path),
    ),
    BoomFieldType(
        BR_ENTRY,
        "entryfile",
        "EntryFile",
        "On-disk entry file name",
        12,
        REP_STR,
        lambda f, d: f.report_str(basename(d.entry_path)),
    ),
    BoomFieldType(
        BR_ENTRY,
        "readonly",
        "ReadOnly",
        "Entry is read-only",
        9,
        REP_STR,
        lambda f, d: f.report_str(_bool_to_yes_no(d.read_only)),
    ),
]

#: Fields derived from BootEntry data, with bootloader variables expanded.
_expand_entry_fields = [
    BoomFieldType(
        BR_ENTRY,
        "bootid",
        "BootID",
        "Boot identifier",
        7,
        REP_SHA,
        lambda f, d: f.report_sha(d.boot_id),
    ),
    BoomFieldType(
        BR_ENTRY,
        "title",
        "Title",
        "Entry title",
        24,
        REP_STR,
        lambda f, d: f.report_str(d.title),
    ),
    BoomFieldType(
        BR_ENTRY,
        "options",
        "Options",
        "Kernel options",
        24,
        REP_STR,
        lambda f, d: f.report_str(d.expand_options),
    ),
    BoomFieldType(
        BR_ENTRY,
        "kernel",
        "Kernel",
        "Kernel image",
        32,
        REP_STR,
        lambda f, d: f.report_str(d.linux),
    ),
    BoomFieldType(
        BR_ENTRY,
        "initramfs",
        "Initramfs",
        "Initramfs image",
        40,
        REP_STR,
        lambda f, d: f.report_str(d.initrd),
    ),
    BoomFieldType(
        BR_ENTRY,
        "machineid",
        "MachineID",
        "Machine identifier",
        10,
        REP_SHA,
        lambda f, d: f.report_sha(d.machine_id),
    ),
    BoomFieldType(
        BR_ENTRY,
        "entrypath",
        "EntryPath",
        "On-disk entry path",
        12,
        REP_STR,
        lambda f, d: f.report_str(d.entry_path),
    ),
    BoomFieldType(
        BR_ENTRY,
        "entryfile",
        "EntryFile",
        "On-disk entry file name",
        12,
        REP_STR,
        lambda f, d: f.report_str(basename(d.entry_path)),
    ),
    BoomFieldType(
        BR_ENTRY,
        "readonly",
        "ReadOnly",
        "Entry is read-only",
        9,
        REP_STR,
        lambda f, d: f.report_str(_bool_to_yes_no(d.read_only)),
    ),
]

#: Fields derived from BootParams data
_params_fields = [
    BoomFieldType(
        BR_PARAMS,
        "version",
        "Version",
        "Kernel version",
        24,
        REP_STR,
        lambda f, d: f.report_str(d.version),
    ),
    BoomFieldType(
        BR_PARAMS,
        "rootdev",
        "RootDevice",
        "Root device",
        10,
        REP_STR,
        lambda f, d: f.report_str(d.root_device),
    ),
    BoomFieldType(
        BR_PARAMS,
        "rootlv",
        "RootLV",
        "Root logical volume",
        6,
        REP_STR,
        lambda f, d: f.report_str(d.lvm_root_lv or ""),
    ),
    BoomFieldType(
        BR_PARAMS,
        "subvolpath",
        "SubvolPath",
        "BTRFS subvolume path",
        10,
        REP_STR,
        lambda f, d: f.report_str(d.btrfs_subvol_path or ""),
    ),
    BoomFieldType(
        BR_PARAMS,
        "subvolid",
        "SubvolID",
        "BTRFS subvolume ID",
        8,
        REP_NUM,
        lambda f, d: f.report_num(_int_if_val(d.btrfs_subvol_id)),
    ),
]

_default_entry_fields = "bootid,version,osname,rootdev"
_verbose_entry_fields = _default_entry_fields + ",options,machineid"


#: Fields derived from CacheEntry data
_cache_fields = [
    BoomFieldType(
        BR_CACHE,
        "imgid",
        "ImageID",
        "Image identifier",
        7,
        REP_SHA,
        lambda f, d: f.report_sha(d.img_id),
    ),
    BoomFieldType(
        BR_CACHE,
        "path",
        "Path",
        "Image path",
        24,
        REP_STR,
        lambda f, d: f.report_str(d.path),
    ),
    BoomFieldType(
        BR_CACHE,
        "mode",
        "Mode",
        "Path mode",
        8,
        REP_STR,
        lambda f, d: f.report_str(filemode(d.mode)),
    ),
    BoomFieldType(
        BR_CACHE, "uid", "User", "User ID", 5, REP_NUM, lambda f, d: f.report_num(d.uid)
    ),
    BoomFieldType(
        BR_CACHE,
        "gid",
        "Group",
        "Group ID",
        5,
        REP_NUM,
        lambda f, d: f.report_num(d.gid),
    ),
    BoomFieldType(
        BR_CACHE,
        "ts",
        "Timestamp",
        "Timestamp",
        10,
        REP_NUM,
        lambda f, d: f.report_num(d.timestamp),
    ),
    BoomFieldType(
        BR_CACHE,
        "state",
        "State",
        "State",
        8,
        REP_STR,
        lambda f, d: f.report_str(d.state),
    ),
    BoomFieldType(
        BR_CACHE,
        "count",
        "Count",
        "Use Count",
        5,
        REP_NUM,
        lambda f, d: f.report_num(d.count),
    ),
]

_default_cache_fields = "path,imgid,ts,state"
_verbose_cache_fields = "path,imgid,ts,mode,uid,gid,state,count"


def _get_machine_id():
    """Return the current host's machine-id.

    Get the machine-id value for the running system by reading from
    ``/etc/machine-id`` and return it as a string.

    :returns: The ``machine_id`` as a string
    :rtype: str
    """
    if path_exists(_MACHINE_ID):
        path = _MACHINE_ID
    elif path_exists(_DBUS_MACHINE_ID):
        path = _DBUS_MACHINE_ID
    else:
        return None

    with open(path, "r") as f:
        try:
            machine_id = f.read().strip()
        except Exception as e:
            _log_error("Could not read machine-id from '%s': %s" % (path, e))
            machine_id = None
    return machine_id


def _str_indent(string, indent):
    """Indent all lines of a multi-line string.

    Indent each line of the multi line string ``string`` to the
    specified indentation level.

    :param string: The string to be indented
    :param indent: The number of characters to indent by
    :returns: str
    """
    outstr = ""
    for line in string.splitlines():
        outstr += indent * " " + line + "\n"
    return outstr.rstrip("\n")


def _canonicalize_lv_name(lvname):
    """Canonicalize an LVM2 logical volume name as "VG/LV", removing any
    "/dev/" prefix and return the result as a string.

    The use of "/dev/mapper/VG-LV" names is not supported.
    """
    dev_prefix = DEV_PATTERN % ""
    if lvname.startswith(dev_prefix + "mapper/"):
        raise ValueError(
            "Logical volume names in /dev/mapper/VG-LV format " "are not supported."
        )
    if lvname.startswith(dev_prefix):
        lvname = lvname[len(dev_prefix) :]
    if "/" not in lvname or lvname.count("/") != 1:
        raise ValueError("Root logical volume name must be in VG/LV format.")
    return lvname


def __write_legacy():
    """Synchronise boom boot entries with the configured legacy
    bootloader format.
    """
    config = get_boom_config()
    if config.legacy_enable and config.legacy_sync:
        clear_legacy_loader()
        write_legacy_loader(selection=Selection(), loader=config.legacy_format)


def _do_print_type(
    report_fields, selected, output_fields=None, opts=None, sort_keys=None
):
    """Print an object type report (entry, osprofile, hostprofile).

    Helper for list function that generate BoomReports.

    Format a set of entry or profile objects matching the given
    criteria and format them as a report, returning the output
    as a string.

    Selection criteria may be expressed via a Selection object
    passed to the call using the ``selection`` parameter.

    :param selection: A Selection object giving selection
                      criteria for the operation
    :param output_fields: a comma-separated list of output fields
    :param opts: output formatting and control options
    :param sort_keys: a comma-separated list of sort keys
    :rtype: str
    """
    opts = opts if opts is not None else BoomReportOpts()

    br = BoomReport(
        _report_obj_types, report_fields, output_fields, opts, sort_keys, None
    )

    for obj in selected:
        # Fixme: handle bes with embedded hp (class test)
        br.report_object(obj)

    return br.report_output()


def _merge_add_del_opts(bp, add_opts, del_opts):
    """Merge a set of existing bootparams option alterations with
    a set of command-line provided values to produce a single
    set of options to add or remove from a cloned or edited
    ``BootEntry``.

    The sets are merged giving precedence to alterations on the
    current command line: i.e. if an option is present in both
    ``bp.del_opts`` and ``add_opts`` (or vice versa) then the
    option taken from the current command line will be effective.

    :param bp: A ``BootParams`` object with the original ``add_opts``
               and ``del_opts`` values.
    :param add_opts: A space-separated string containing a list of
                     additional options taken from the current
                     command line.
    :param del_opts: A space-separated string containing a list of
                     options to delete taken from the current
                     command line.
    :returns: A tuple ``(effective_add_opts, effective_del_opts)``
              giving the final effective values as a list of
              strings, one per option word.
    """

    def _merge_opts(orig_opts, opts, r_opts):
        # Merge new and cloned kernel options
        all_opts = []
        if orig_opts:
            for opt in orig_opts:
                if opt not in all_opts:
                    all_opts.append(opt)
        if opts:
            for opt in opts:
                if opt not in all_opts:
                    all_opts.append(opt)
        return [o for o in all_opts if o not in r_opts]

    _log_debug_cmd("Add opts: %s" % add_opts)
    _log_debug_cmd("Del opts: %s" % del_opts)
    _log_debug_cmd("Original add_opts: %s" % bp.add_opts)
    _log_debug_cmd("Original del_opts: %s" % bp.del_opts)

    r_del_opts = []
    r_add_opts = []

    add_opts = add_opts.split() if add_opts else []
    del_opts = del_opts.split() if del_opts else []

    for add_opt in list(add_opts):
        # Do not allow conflicting command line add/del opts
        if add_opt in del_opts:
            raise ValueError(
                "Conflicting --add-opts %s and --del-opts %s" % (add_opt, add_opt)
            )

        if add_opt in bp.del_opts:
            r_del_opts.append(add_opt)
            add_opts.remove(add_opt)

    for del_opt in list(del_opts):
        if del_opt in bp.add_opts:
            r_add_opts.append(del_opt)
            del_opts.remove(del_opt)

    add_opts = _merge_opts(bp.add_opts, add_opts, r_add_opts)
    del_opts = _merge_opts(bp.del_opts, del_opts, r_del_opts)

    _log_debug_cmd("Effective add options: %s" % add_opts)
    _log_debug_cmd("Effective del options: %s" % del_opts)

    return (add_opts, del_opts)


#
# Command driven API: BootEntry and OsProfile management and reporting.
#

# Boot image cache modes

#: Use original image (no caching)
I_NONE = None
I_CACHE = "cache"
I_BACKUP = "backup"


#
# BootEntry manipulation
#


def _find_backup_name(img_path):
    """Generate a new, unique backup pathname."""
    img_backup = ("%s.boom" % img_path)[1:] + "%d"

    def _backup_img(backup_nr):
        return sep + img_backup % backup_nr

    def _backup_path(backup_nr):
        return join(get_boot_path(), img_backup[1:] % backup_nr)

    backup_nr = 0
    while path_exists(_backup_path(backup_nr)):
        if find_cache_paths(Selection(path=_backup_img(backup_nr))):
            break
        backup_nr += 1
    return sep + img_backup % backup_nr


def _cache_image(img_path, backup):
    """Cache the image found at ``img_path`` and optionally create
    a backup copy.
    """
    if "." in img_path:
        ext = img_path.rsplit(".", 1)[1]
        if ext.startswith("boom") and ext[4:].isdigit():
            if find_cache_paths(Selection(path=img_path)):
                return img_path
    try:
        if backup:
            img_backup = _find_backup_name(img_path)
            _log_debug("backing up '%s' as '%s'" % (img_path, img_backup))
            ce = backup_path(img_path, img_backup)
            return img_backup
        else:
            ce = cache_path(img_path)
    except (OSError, ValueError) as e:
        _log_error("Could not cache path %s: %s" % (img_path, e))
        raise e
    return img_path


def _find_one_entry(select):
    """Find exactly one entry, and raise ValueError if zero or more
    than one entry is found.

    :param: An instance of ``Selection`` specificying match criteria.
    :returns: A single instance of ``BootEntry``
    :raises: ValueError if selection results are empty or non-unique
    """
    bes = find_entries(select)
    if not bes:
        raise ValueError("No matching entry found for boot ID %s" % select.boot_id)
    if len(bes) > 1:
        raise ValueError("Selection criteria must match exactly one entry")
    return bes[0]


[docs] def create_entry( title, version, machine_id, root_device, lvm_root_lv=None, btrfs_subvol_path=None, btrfs_subvol_id=None, profile=None, add_opts=None, del_opts=None, write=True, architecture=None, expand=False, allow_no_dev=False, images=I_NONE, no_fstab=False, mounts=None, swaps=None, ): """Create new boot loader entry. Create the specified boot entry in the configured loader directory. An error is raised if a matching entry already exists. :param title: the title of the new entry. :param version: the version string for the new entry. :param machine_id: the machine id for the new entry. :param root_device: the root device path for the new entry. :param lvm_root_lv: an optional LVM2 root logical volume. :param btrfs_subvol_path: an optional BTRFS subvolume path. :param btrfs_subvol_id: an optional BTRFS subvolume id. :param profile: A profile to use for this entry. :param add_opts: A list of additional kernel options to append. :param del_opts: A list of template-supplied options to drop. :param write: ``True`` if the entry should be written to disk, or ``False`` otherwise. :param architecture: An optional BLS architecture string. :param expand: Expand bootloader environment variables. :param allow_no_dev: Accept a non-existent or invalid root dev. :param images: Whether to cache or backup boot images in the new entry. :param no_fstab: Disable parsing of the fstab for the new entry. :param mounts: A list of colon separated command-line mount specifications for the new entry. :param swaps: A list of colon separated command-line swap specifications for the new entry. :returns: a ``BootEntry`` object corresponding to the new entry. :rtype: ``BootEntry`` :raises: ``ValueError`` if either required values are missing or a duplicate entry exists, or ``OsError`` if an error occurs while writing the entry file. """ if not title and not profile.title: raise ValueError("Entry title cannot be empty.") if not version: raise ValueError("Entry version cannot be empty.") if not machine_id: raise ValueError("Entry machine_id cannot be empty.") if not root_device: raise ValueError("Entry requires a root_device.") if not profile: raise ValueError("Cannot create entry without OsProfile.") bc = get_boom_config() if images is not I_NONE and not bc.cache_enable: raise BoomConfigError( "Cannot use images=%s with image cache disabled" " (config.cache_enable=False)" % images ) add_opts = add_opts.split() if add_opts else [] del_opts = del_opts.split() if del_opts else [] if no_fstab: add_opts.append("fstab=no") if mounts: mount_units = parse_mount_units(mounts) add_opts.extend(mount_units) if swaps: swap_units = parse_swap_units(swaps) add_opts.extend(swap_units) _log_debug_cmd("Effective add options: %s" % add_opts) _log_debug_cmd("Effective del options: %s" % del_opts) bp = BootParams( version, root_device, lvm_root_lv=lvm_root_lv, btrfs_subvol_path=btrfs_subvol_path, btrfs_subvol_id=btrfs_subvol_id, add_opts=add_opts, del_opts=del_opts, ) be = BootEntry( title=title, machine_id=machine_id, osprofile=profile, boot_params=bp, architecture=architecture, allow_no_dev=allow_no_dev, ) if images in (I_BACKUP, I_CACHE): be.initrd = _cache_image(be.initrd, images == I_BACKUP) be.linux = _cache_image(be.linux, images == I_BACKUP) if find_entries(Selection(boot_id=be.boot_id)): raise ValueError("Entry already exists (boot_id=%s)." % be.disp_boot_id) if write: be.write_entry(expand=expand) __write_legacy() return be
[docs] def delete_entries(selection=None): """Delete entries matching selection criteria. Delete the specified boot entry or entries from the configured loader directory. If ``boot_id`` is used, or if the criteria specified match exactly one entry, a single entry is removed. If ``boot_id`` is not used, and more than one matching entry is present, all matching entries will be removed. Selection criteria may also be expressed via a Selection object passed to the call using the ``selection`` parameter. On success the number of entries removed is returned. :param selection: A Selection object giving selection criteria for the operation. :returns: the number of entries removed. :rtype: ``int`` """ bes = find_entries(selection=selection) if not bes: raise IndexError("No matching entry found.") deleted = 0 for be in bes: be.delete_entry() deleted += 1 __write_legacy() return deleted
[docs] def clone_entry( selection=None, title=None, version=None, machine_id=None, root_device=None, lvm_root_lv=None, btrfs_subvol_path=None, btrfs_subvol_id=None, profile=None, architecture=None, add_opts=None, del_opts=None, write=True, expand=False, allow_no_dev=False, images=I_NONE, no_fstab=False, mounts=None, swaps=None, ): """Clone an existing boot loader entry. Create the specified boot entry in the configured loader directory by cloning all un-set parameters from the boot entry selected by the ``selection`` argument. An error is raised if a matching entry already exists. :param selection: criteria matching the entry to clone. :param title: the title of the new entry. :param version: the version string for the new entry. :param machine_id: the machine id for the new entry. :param root_device: the root device path for the new entry. :param lvm_root_lv: an optional LVM2 root logical volume. :param btrfs_subvol_path: an optional BTRFS subvolume path. :param btrfs_subvol_id: an optional BTRFS subvolume id. :param profile: A profile to use for this entry. :param architecture: An optional BLS architecture string. :param add_opts: A list of additional kernel options to append. :param del_opts: A list of template-supplied options to drop. :param write: ``True`` if the entry should be written to disk, or ``False`` otherwise. :param expand: Expand bootloader environment variables. :param allow_no_dev: Allow the block device to not exist. :param images: Whether to cache or backup boot images in the new entry. :returns: a ``BootEntry`` object corresponding to the new entry. :param no_fstab: Disable parsing of the fstab for the new entry. :param mounts: A list of colon separated command-line mount specifications for the new entry. :rtype: ``BootEntry`` :raises: ``ValueError`` if either required values are missing or a duplicate entry exists, or``OsError`` if an error occurs while writing the entry file. """ if not selection.boot_id or selection.boot_id is None: raise ValueError("clone requires boot_id") all_args = ( title, version, machine_id, root_device, lvm_root_lv, btrfs_subvol_path, btrfs_subvol_id, profile, ) if not any(all_args): raise ValueError( "clone requires one or more of:\ntitle, version, " "machine_id, root_device, lvm_root_lv, " "btrfs_subvol_path, btrfs_subvol_id, profile" ) bc = get_boom_config() if images is not I_NONE and not bc.cache_enable: raise BoomConfigError( "Cannot use images=%s with image cache disabled" " (config.cache_enable=False)" % images ) be = _find_one_entry(selection) _log_debug("Cloning entry with boot_id='%s'" % be.disp_boot_id) title = title if title else be.title version = version if version else be.version machine_id = machine_id if machine_id else be.machine_id profile = profile if profile else be._osp bp = BootParams.from_entry(be, expand=expand) (add_opts, del_opts) = _merge_add_del_opts(bp, add_opts, del_opts) if no_fstab: add_opts.append("fstab=no") if mounts: mount_units = parse_mount_units(mounts) add_opts.extend(mount_units) if swaps: swap_units = parse_swap_units(swaps) add_opts.extend(swap_units) bp.root_device = root_device if root_device else bp.root_device bp.lvm_root_lv = lvm_root_lv if lvm_root_lv else bp.lvm_root_lv if btrfs_subvol_path and btrfs_subvol_id: raise ValueError("cannot set btrfs_subvol_path and btrfs_subvol_id") if btrfs_subvol_path: bp.btrfs_subvol_path = btrfs_subvol_path elif btrfs_subvol_id: bp.btrfs_subvol_id = btrfs_subvol_id clone_be = BootEntry( title=title, machine_id=machine_id, osprofile=profile, boot_params=bp, architecture=architecture, allow_no_dev=allow_no_dev, ) if be.options != be.expand_options and not expand: clone_be.options = be.options else: clone_be.bp.add_opts = add_opts clone_be.bp.del_opts = del_opts # Clone optional keys allowed by profile for optional_key in be._osp.optional_keys.split(): if optional_key in clone_be._osp.optional_keys: if hasattr(be, optional_key): setattr(clone_be, optional_key, getattr(be, optional_key)) # Boot image overrides? if be.initrd != clone_be.initrd: clone_be.initrd = be.initrd if be.linux != clone_be.linux: clone_be.linux = be.linux if images in (I_BACKUP, I_CACHE): clone_be.initrd = _cache_image(clone_be.initrd, images == I_BACKUP) clone_be.linux = _cache_image(clone_be.linux, images == I_BACKUP) if find_entries(Selection(boot_id=clone_be.boot_id)): raise ValueError("Entry already exists (boot_id=%s)." % clone_be.disp_boot_id) if write: clone_be.write_entry() __write_legacy() return clone_be
[docs] def edit_entry( selection=None, title=None, version=None, machine_id=None, root_device=None, lvm_root_lv=None, btrfs_subvol_path=None, btrfs_subvol_id=None, profile=None, architecture=None, add_opts=None, del_opts=None, expand=False, images=I_NONE, ): """Edit an existing boot loader entry. Modify an existing BootEntry by changing one or more of the entry values or boot parameters. The modified BootEntry is written to disk and returned on success. Modifying a BootEntry causes the entry's boot_id to change, since the ID is based on the values of all configured boot keys. :param selection: A Selection specifying the boot_id to edit :param title: The new entry title :param version: The new entry version :param machine_id: The new machine_id :param root_device: The new root device :param lvm_root_lv: The new LVM root LV :param btrfs_subvol_path: The new BTRFS subvolume path :param btrfs_subvol_id: The new BTRFS subvolme ID :param profile: The host or OS profile for the edited entry :param architecture: An optional BLS architecture string. :param add_opts: A list of additional kernel options to append. :param del_opts: A list of template-supplied options to drop. :param expand: Expand bootloader environment variables. :returns: The modified ``BootEntry`` :rtype: ``BootEntry`` """ all_args = ( title, version, machine_id, root_device, lvm_root_lv, btrfs_subvol_path, btrfs_subvol_id, profile, ) if not any(all_args): raise ValueError( "edit requires one or more of:\ntitle, version, " "machine_id, root_device, lvm_root_lv, " "btrfs_subvol_path, btrfs_subvol_id, profile" ) bc = get_boom_config() if images is not I_NONE and not bc.cache_enable: raise BoomConfigError( "Cannot use images=%s with image cache disabled" " (config.cache_enable=False)" % images ) # Discard all selection criteria but boot_id. selection = Selection(boot_id=selection.boot_id) be = _find_one_entry(selection) _log_debug("Editing entry with boot_id='%s'" % be.disp_boot_id) # Use a matching HostProfile is one exists, or the command line # OsProfile argument if set. machine_id = machine_id or be.machine_id version = version or be.version (add_opts, del_opts) = _merge_add_del_opts(be.bp, add_opts, del_opts) be._osp = profile or be._osp be.title = title or be.title be.machine_id = machine_id or be.machine_id be.architecture = architecture or be.architecture be.bp.version = version be.bp.root_device = root_device or be.bp.root_device be.bp.lvm_root_lv = lvm_root_lv or be.bp.lvm_root_lv be.bp.btrfs_subvol_path = btrfs_subvol_path or be.bp.btrfs_subvol_path be.bp.btrfs_subvol_id = btrfs_subvol_id or be.bp.btrfs_subvol_id be.bp.add_opts = add_opts be.bp.del_opts = del_opts if images in (I_BACKUP, I_CACHE): be.initrd = _cache_image(be.initrd, images == I_BACKUP) be.linux = _cache_image(be.linux, images == I_BACKUP) # Is the entry now identical to an existing entry? if len(find_entries(Selection(boot_id=be.boot_id))) > 1: raise ValueError("Entry already exists (boot_id=%s)." % be.disp_boot_id) be.update_entry(expand=expand) __write_legacy() return be
[docs] def list_entries(selection=None): """List entries matching selection criteria. Return a list of ``boom.bootloader.BootEntry`` objects matching the given criteria. Selection criteria may be expressed via a Selection object passed to the call using the ``selection`` parameter. :param selection: A Selection object giving selection criteria for the operation. :returns: A list of matching BootEntry objects. :rtype: list """ bes = find_entries(selection=selection) return bes
def _expand_fields(default_fields, output_fields): """Expand output fields list from command line arguments.""" if not output_fields: output_fields = default_fields elif output_fields.startswith("+"): output_fields = default_fields + "," + output_fields[1:] return output_fields # # OsProfile manipulation # def _find_profile(cmd_args, version, machine_id, command, optional=True): """Find a matching profile (HostProfile or OsProfile) for this combination of version, machine_id, label and command line profile arguments :param cmd_args: The command argument namespace :param version: A version string to match :machine_id: The machine identifier to match :command: The command name to use in error messages :returns: A matching ``OsProfile``, ``HostProfile``, or ``None`` if no match is found. """ if not cmd_args.profile: # Attempt to find a matching OsProfile by version string osp = match_os_profile_by_version(version) os_id = osp.os_id if osp else None if not osp: print("No matching OsProfile found for version '%s'" % version) else: os_id = cmd_args.profile osps = find_profiles(Selection(os_id=os_id)) if os_id else None # Fail if an explicit profile was given and it is not found. if not osps and os_id is not None and os_id == cmd_args.profile: print("OsProfile not found: %s" % os_id) return None if osps and len(osps) > 1: print("OsProfile ID '%s' is ambiguous" % os_id) return None osp = osps[0] if osps else None if osp: _log_debug("Found OsProfile: %s" % osp.os_id) # Attempt to match a host profile to the running host label = cmd_args.label or "" host_select = Selection(machine_id=machine_id, host_label=label) hps = find_host_profiles(host_select) hp = hps[0] if hps else None if len(hps) > 1: # This can only occur if host profiles have been edited outside # boom's control, such that there are one or more profiles with # matching machine_id and label. _log_error("Ambiguous host profile selection") return None elif len(hps) == 1: _log_debug("Found HostProfile: %s" % hps[0].host_id) if (hp and osp) and not osp.os_id.startswith(hp.os_id): _log_error( "Active host profile (host_id=%s, os_id=%s) " "conflicts with --profile=%s" % (hp.disp_host_id, hp.disp_os_id, osp.disp_os_id) ) return None elif not osp and not hps: if not optional: _log_error( "%s requires --profile or a matching OsProfile " "or HostProfile" % command ) return None return hp or osp def _uname_heuristic(name, version_id): """Attempt to guess a uname pattern for a given OS name and version_id value. This is currently supported for Red Hat Enterprise Linux and Fedora since both distributions provide a fixed string in the UTS release string that can be used to match candidate kernel versions against. :returns: ``True`` if uname pattern heuristics should be used for this OS or ``False`` otherwise. """ el_uname = "el" fc_uname = "fc" _name_to_uname = { "Red Hat Enterprise Linux": el_uname, "Red Hat Enterprise Linux Server": el_uname, "Red Hat Enterprise Linux Workstation": el_uname, "Fedora Linux": fc_uname, "Fedora": fc_uname, } # Strip trailing minor version ident from elX_Y if "_" in version_id: version_id = version_id[0 : version_id.find("_")] if "." in version_id: version_id = version_id[0 : version_id.find(".")] if name in _name_to_uname: return "%s%s" % (_name_to_uname[name], version_id) return None def _default_optional_keys(osp): """Set default optional keys for OsProfile Attempt to set default optional keys for a given OsProfile if the distribution is known to support the Red Hat BLS extensions. """ all_optional_keys = "grub_users grub_arg grub_class id" _default_optional_keys = [ "Red Hat Enterprise Linux", "Red Hat Enterprise Linux Server", "Red Hat Enterprise Linux Workstation", "CentOS Linux", "Fedora Linux", "Fedora", ] if osp.os_name in _default_optional_keys: return all_optional_keys return "" def _os_profile_from_file(os_release, uname_pattern, profile_data=None): """Create OsProfile from os-release file. Construct a new ``OsProfile`` object from the specified path, substituting each set kwarg parameter with the supplied value in the resulting object. :param os_release: The os-release file to read :param uname_pattern: A replacement uname_pattern value :param profile_data: Dictionary of profile key:value pairs :returns: A new OsProfile :rtype: OsProfile """ profile_data[BOOM_OS_UNAME_PATTERN] = uname_pattern osp = OsProfile.from_os_release_file(os_release, profile_data=profile_data) # When creating an OsProfile from an os-release file we cannot # guess the uname_pattern until after the file has been read and # the os_name and os_version_id values have been set. if uname_pattern: osp.uname_pattern = uname_pattern else: # Attempt to guess a uname_pattern for operating systems # that have predictable UTS release patterns. osp.uname_pattern = _uname_heuristic(osp.os_name, osp.os_version_id) if not osp.uname_pattern: raise ValueError("Could not determine uname pattern for '%s'" % osp.os_name) if not osp.optional_keys: osp.optional_keys = _default_optional_keys(osp) osp.write_profile() return osp
[docs] def create_profile( name, short_name, version, version_id, uname_pattern=None, kernel_pattern=None, initramfs_pattern=None, root_opts_lvm2=None, root_opts_btrfs=None, options=None, optional_keys=None, profile_data=None, profile_file=None, ): """Create new operating system profile. Create the specified OsProfile in the configured profiles directory. OsProfile key values may be specified either by passing individual keyword arguments, or by passing a dictionary of OsProfile key name to value pairs as the ``profile_data`` argument. If a key is present as both a keyword argument and in the ``profile_data`` dictionary, the argument will take precedence. An error is raised if a matching profile already exists. :param name: The name of the new OsProfile :param short_name: The short name of the new OsProfile :param version: The version string of the new OsProfile :param version_id: The version ID string of the new OsProfile :param uname_pattern: A uname pattern to match for this profile :param kernel_pattern: Pattern to generate kernel paths :param initramfs_pattern: Pattern to generate initramfs paths :param root_opts_lvm2: Template options for LVM2 entries :param root_opts_btrfs: Template options for BTRFS entries :param options: Template kernel command line options :param profile_data: Dictionary of profile key:value pairs :param profile_file: File to be used for profile :returns: an ``OsProfile`` object for the new profile :rtype: ``OsProfile`` :raises: ``ValueError`` if either required values are missing or a duplicate profile exists, or``OsError`` if an error occurs while writing the profile file. """ def _have_key(pd, arg, key): return arg or pd and key in pd if not profile_data: profile_data = {} if not profile_file: if not _have_key(profile_data, name, BOOM_OS_NAME): raise ValueError("Profile name cannot be empty.") if not _have_key(profile_data, short_name, BOOM_OS_SHORT_NAME): raise ValueError("Profile short name cannot be empty.") if not _have_key(profile_data, version, BOOM_OS_VERSION): raise ValueError("Profile version cannot be empty.") if not _have_key(profile_data, version_id, BOOM_OS_VERSION_ID): raise ValueError("Profile version ID cannot be empty.") # Allow keyword arguments to override if name: profile_data[BOOM_OS_NAME] = name if short_name: profile_data[BOOM_OS_SHORT_NAME] = short_name if version: profile_data[BOOM_OS_VERSION] = version if version_id: profile_data[BOOM_OS_VERSION_ID] = version_id if uname_pattern: profile_data[BOOM_OS_UNAME_PATTERN] = uname_pattern elif BOOM_OS_UNAME_PATTERN not in profile_data: # Attempt to guess a uname_pattern for operating systems # that have predictable UTS release patterns. pattern = _uname_heuristic( profile_data[BOOM_OS_NAME], profile_data[BOOM_OS_VERSION_ID] ) if pattern: profile_data[BOOM_OS_UNAME_PATTERN] = pattern else: raise ValueError( "Could not determine uname pattern for '%s'" % profile_data[BOOM_OS_NAME] ) if kernel_pattern: profile_data[BOOM_OS_KERNEL_PATTERN] = kernel_pattern if initramfs_pattern: profile_data[BOOM_OS_INITRAMFS_PATTERN] = initramfs_pattern if root_opts_lvm2: profile_data[BOOM_OS_ROOT_OPTS_LVM2] = root_opts_lvm2 if root_opts_btrfs: profile_data[BOOM_OS_ROOT_OPTS_BTRFS] = root_opts_btrfs if options: profile_data[BOOM_OS_OPTIONS] = options if optional_keys: profile_data[BOOM_OS_OPTIONAL_KEYS] = optional_keys if profile_file: return _os_profile_from_file( profile_file, uname_pattern, profile_data=profile_data ) osp = OsProfile(name, short_name, version, version_id, profile_data=profile_data) if not osp.optional_keys: osp.optional_keys = _default_optional_keys(osp) osp.write_profile() return osp
[docs] def delete_profiles(selection=None): """Delete profiles matching selection criteria. Delete the specified OsProfile or profiles from the configured profile directory. If ``os_id`` is used, or if the criteria specified match exactly one profile, a single entry is removed. If ``os_id`` is not used, and more than one matching profile is present, all matching profiles will be removed. Selection criteria are expressed via a Selection object passed to the call using the ``selection`` parameter. On success the number of profiles removed is returned. :param selection: A Selection object giving selection criteria for the operation. :returns: the number of entries removed. :rtype: ``int`` """ osps = find_profiles(selection=selection) if not osps: raise IndexError("No matching profiles found.") deleted = 0 for osp in osps: osp.delete_profile() deleted += 1 return deleted
[docs] def clone_profile( selection=None, name=None, short_name=None, version=None, version_id=None, uname_pattern=None, kernel_pattern=None, initramfs_pattern=None, root_opts_lvm2=None, root_opts_btrfs=None, options=None, ): """Clone an existing operating system profile. Create the specified profile in the configured profile directory by cloning all un-set parameters from the profile selected by the ``selection`` argument. An error is raised if a matching profile already exists, or if the selection criteria match more than one profile. :param selection: criteria matching the profile to clone. :param name: the name of the new profile. :param short_name: the short name of the new profile. :param version: the version string for the new profile. :param version_id: the version ID string for the new profile. :param uname_pattern: a uname pattern to match this profile. :param kernel_pattern: a kernel pattern to match this profile. :param initramfs_pattern: a initramfs pattern to match this profile. :param root_opts_lvm2: LVM2 root options template. :param root_opts_btrfs: BTRFS root options template. :param options: Kernel options template. :returns: a new ``OsProfile`` object. :rtype: ``OsProfile`` :raises: ``ValueError`` if either required values are missing or a duplicate profile exists, or``OsError`` if an error occurs while writing the profile file. """ if not selection.os_id: raise ValueError("clone requires os_id") all_args = ( name, short_name, version, version_id, uname_pattern, kernel_pattern, initramfs_pattern, root_opts_lvm2, root_opts_btrfs, options, ) if not any(all_args): raise ValueError( "clone requires one or more of:\nname, " "short_name, version, version_id, uname_pattern," "kernel_pattern, initramfs_pattern, root_opts_lvm2, " "root_opts_btrfs, options" ) osps = find_profiles(selection) if not osps: raise ValueError("No matching profile found: %s" % selection.os_id) if len(osps) > 1: raise ValueError("Clone criteria must match exactly one profile") osp = osps.pop() # Clone unset keys name = name or osp.os_name short_name = short_name or osp.os_short_name version = version or osp.os_version version_id = version_id or osp.os_version_id uname_pattern = uname_pattern or osp.uname_pattern kernel_pattern = kernel_pattern or osp.kernel_pattern initramfs_pattern = initramfs_pattern or osp.initramfs_pattern root_opts_lvm2 = root_opts_lvm2 or osp.root_opts_lvm2 root_opts_btrfs = root_opts_btrfs or osp.root_opts_btrfs options = options or osp.options clone_osp = OsProfile( name, short_name, version, version_id, uname_pattern=uname_pattern, kernel_pattern=kernel_pattern, initramfs_pattern=initramfs_pattern, root_opts_lvm2=root_opts_lvm2, root_opts_btrfs=root_opts_btrfs, options=options, ) clone_osp.write_profile() return clone_osp
[docs] def edit_profile( selection=None, uname_pattern=None, kernel_pattern=None, initramfs_pattern=None, root_opts_lvm2=None, root_opts_btrfs=None, options=None, optional_keys=None, ): """Edit an existing operating system profile. Modify an existing OsProfile by changing one or more of the profile values. The modified OsProfile is written to disk and returned on success. :param selection: A Selection specifying the boot_id to edit :param uname_pattern: The new uname pattern :param kernel_pattern: The new kernel pattern :param initramfs_pattern: The new initramfs pattern :param root_opts_lvm2: The new LVM2 root options :param root_opts_btrfs: The new BTRFS root options :param options: The new kernel options template :returns: The modified ``OsProfile`` :rtype: ``OsProfile`` """ # Discard all selection criteria but os_id. selection = Selection(os_id=selection.os_id) osp = None osps = find_profiles(Selection(os_id=selection.os_id)) if not osps: raise ValueError("No matching profile found: %s" % selection.os_id) if len(osps) > 1: raise ValueError("OS profile identifier '%s' is ambiguous" % selection.os_id) osp = osps.pop() osp.uname_pattern = uname_pattern or osp.uname_pattern osp.kernel_pattern = kernel_pattern or osp.kernel_pattern osp.initramfs_pattern = initramfs_pattern or osp.initramfs_pattern osp.root_opts_lvm2 = root_opts_lvm2 or osp.root_opts_lvm2 osp.root_opts_btrfs = root_opts_btrfs or osp.root_opts_btrfs osp.options = options or osp.options osp.optional_keys = optional_keys or osp.optional_keys osp.write_profile() return osp
[docs] def list_profiles(selection=None): """List operating system profiles matching selection criteria. Return a list of ``boom.osprofile.OsProfile`` objects matching the given criteria. Selection criteria may be expressed via a Selection object passed to the call using the ``selection`` parameter. :param selection: A Selection object giving selection criteria for the operation. :returns: a list of ``OsProfile`` objects. :rtype: list """ osps = find_profiles(selection=selection) return osps
[docs] def create_host( machine_id=None, host_name=None, os_id=None, label=None, kernel_pattern=None, initramfs_pattern=None, root_opts_lvm2=None, root_opts_btrfs=None, options=None, add_opts=None, del_opts=None, host_data=None, ): """Create new host profile. Create the specified HostProfile in the configured profiles directory. HostProfile key values may be specified either by passing individual keyword arguments, or by passing a dictionary of HostProfile key name to value pairs as the ``host_data`` argument. If a key is present as both a keyword argument and in the ``host_data`` dictionary, the argument will take precedence. An error is raised if a matching profile already exists. :param machine_id: The machine_id of the host :param host_name: The full name of the new HostProfile :param label: An optional host label :param os_id: The os_id for the new host :param kernel_pattern: Pattern to generate kernel paths :param initramfs_pattern: Pattern to generate initramfs paths :param root_opts_lvm2: Template options for LVM2 entries :param root_opts_btrfs: Template options for BTRFS entries :param options: Template kernel command line options :param add_opts: Additional boot options for this profile :param del_opts: Boot options to delete for this profile :param host_data: Dictionary of profile key:value pairs :returns: a ``HostProfile`` object for the new profile :rtype: ``HostProfile`` :raises: ``ValueError`` if either required values are missing or a duplicate profile exists, or``OsError`` if an error occurs while writing the profile file. """ def _have_key(hd, arg, key): return arg or hd and key in hd if not _have_key(host_data, host_name, BOOM_OS_NAME): raise ValueError("Host name cannot be empty.") if not _have_key(host_data, machine_id, BOOM_OS_VERSION): raise ValueError("Host machine_id cannot be empty.") if not _have_key(host_data, os_id, BOOM_OS_ID): raise ValueError("Host OS ID cannot be empty.") label = label or "" if not host_data: host_data = {} # FIXME use kwarg style # Allow keyword arguments to override if machine_id: host_data[BOOM_ENTRY_MACHINE_ID] = machine_id if host_name: host_data[BOOM_HOST_NAME] = host_name if label: host_data[BOOM_HOST_LABEL] = label if os_id: host_data[BOOM_OS_ID] = os_id if kernel_pattern: host_data[BOOM_OS_KERNEL_PATTERN] = kernel_pattern if initramfs_pattern: host_data[BOOM_OS_INITRAMFS_PATTERN] = initramfs_pattern if root_opts_lvm2: host_data[BOOM_OS_ROOT_OPTS_LVM2] = root_opts_lvm2 if root_opts_btrfs: host_data[BOOM_OS_ROOT_OPTS_BTRFS] = root_opts_btrfs if options: host_data[BOOM_OS_OPTIONS] = options if add_opts: host_data[BOOM_HOST_ADD_OPTS] = add_opts if del_opts: host_data[BOOM_HOST_DEL_OPTS] = del_opts hp = HostProfile(machine_id=machine_id, profile_data=host_data) hp.write_profile() return hp
[docs] def delete_hosts(selection=None): """Delete host profiles matching selection criteria. Delete the specified ``HostProfile`` or profiles from the configured profile directory. If ``os_id`` is used, or if the criteria specified match exactly one profile, a single entry is removed. If ``host_id`` is not used, and more than one matching profile is present, all matching profiles will be removed. Selection criteria are expressed via a Selection object passed to the call using the ``selection`` parameter. On success the number of profiles removed is returned. :param selection: A Selection object giving selection criteria for the operation. :returns: the number of entries removed. :rtype: ``int`` """ hps = find_host_profiles(selection=selection) if not hps: raise IndexError("No matching host profiles found.") deleted = 0 for hp in hps: hp.delete_profile() deleted += 1 return deleted
[docs] def clone_host( selection=None, machine_id=None, host_name=None, label=None, os_id=None, kernel_pattern=None, initramfs_pattern=None, root_opts_lvm2=None, root_opts_btrfs=None, add_opts=None, del_opts=None, options=None, ): """Clone an existing host profile. Create the specified profile in the configured profile directory by cloning all un-set parameters from the profile selected by the ``selection`` argument. An error is raised if a matching profile already exists, or if the selection criteria match more than one profile. :param selection: criteria matching the profile to clone. :param machine_id: the machine_id of the new host profile. :param host_name: the hostname of the new host profile. :param label: an optional host label. :param os_id: the operating system identifier for the host. :param kernel_pattern: The kernel pattern for the host. :param initramfs_pattern: The initramfs pattern for the host. :param root_opts_lvm2: LVM2 root options template. :param root_opts_btrfs: BTRFS root options template. :param add_opts: Additional boot options for this profile. :param del_opts: Boot options to delete for this profile. :param options: Kernel options template. :returns: a new ``HostProfile`` object. :rtype: ``HostProfile`` :raises: ``ValueError`` if either required values are missing or a duplicate profile exists, or``OsError`` if an error occurs while writing the profile file. """ if not selection.host_id: raise ValueError("clone requires host_id") all_args = ( machine_id, label, host_name, os_id, kernel_pattern, initramfs_pattern, root_opts_lvm2, root_opts_btrfs, add_opts, del_opts, options, ) if not any(all_args): raise ValueError( "clone requires one or more of:\n" "--machine-id, --label, --name, --os-id, " "--kernel-pattern, --initramfs_pattern, " "--root-opts-lvm2, --root_opts-btrfs, " "--add-opts, --del-opts, --options" ) hps = find_host_profiles(selection) if not hps: raise ValueError("No matching host profile found: %s" % selection.host_id) if len(hps) > 1: raise ValueError("Clone criteria must match exactly one profile") hp = hps.pop() # Clone unset keys machine_id = machine_id or hp.machine_id host_name = host_name or hp.host_name label = label or "" os_id = os_id or hp.os_id initramfs_pattern = initramfs_pattern or hp.initramfs_pattern kernel_pattern = kernel_pattern or hp.kernel_pattern root_opts_lvm2 = root_opts_lvm2 or hp.root_opts_lvm2 root_opts_btrfs = root_opts_btrfs or hp.root_opts_btrfs add_opts = add_opts or hp.add_opts del_opts = del_opts or hp.del_opts options = options or hp.options clone_hp = HostProfile( machine_id=machine_id, host_name=host_name, label=label, os_id=os_id, kernel_pattern=kernel_pattern, initramfs_pattern=initramfs_pattern, root_opts_lvm2=root_opts_lvm2, root_opts_btrfs=root_opts_btrfs, add_opts=add_opts, del_opts=del_opts, options=options, ) clone_hp.write_profile() return clone_hp
[docs] def edit_host( selection=None, machine_id=None, os_id=None, host_name=None, label=None, kernel_pattern=None, initramfs_pattern=None, root_opts_lvm2=None, root_opts_btrfs=None, add_opts=None, del_opts=None, options=None, ): """Edit an existing host profile. Modify an existing HostProfile by changing one or more of the profile values. The modified HostProfile is written to disk and returned on success. :param selection: A Selection specifying the boot_id to edit :param machine_id: The machine id for the edited host profile :param os_id: The OS id for the edited host profile :param host_name: The host name for the edited host profile :param label: an optional host label :param kernel_pattern: The new kernel pattern :param initramfs_pattern: The new initramfs pattern :param root_opts_lvm2: The new LVM2 root options :param root_opts_btrfs: The new BTRFS root options :param add_opts: Additional boot options for this profile. :param del_opts: Boot options to delete for this profile. :param options: The new kernel options template :returns: The modified ``HostProfile`` :rtype: ``HostProfile`` """ # Discard all selection criteria but host_id. selection = Selection(host_id=selection.host_id) hps = None hps = find_host_profiles(selection) if not hps: raise ValueError("No matching profile found: %s" % selection.host_id) if len(hps) > 1: raise ValueError("OS profile identifier '%s' is ambiguous" % selection.os_id) hp = hps.pop() hp.delete_profile() hp.machine_id = machine_id or hp.os_id hp.host_name = host_name or hp.host_name hp.label = label or hp.label hp.os_id = os_id or hp.os_id hp.kernel_pattern = kernel_pattern or hp.kernel_pattern hp.initramfs_pattern = initramfs_pattern or hp.initramfs_pattern hp.root_opts_lvm2 = root_opts_lvm2 or hp.root_opts_lvm2 hp.root_opts_btrfs = root_opts_btrfs or hp.root_opts_btrfs hp.options = options or hp.options hp.write_profile() return hp
[docs] def list_hosts(selection=None): """List host profiles matching selection criteria. Return a list of ``boom.hostprofile.HostProfile`` objects matching the given criteria. Selection criteria may be expressed via a Selection object passed to the call using the ``selection`` parameter. :param selection: A Selection object giving selection criteria for the operation. :returns: a list of ``HostProfile`` objects. :rtype: list """ hps = find_host_profiles(selection=selection) return hps
def _print_cache( find_fn, selection=None, opts=None, output_fields=None, sort_keys=None, expand=False ): """Print cache entries (with or without images) matching selection criteria. Selection criteria may be expressed via a ``Selection`` object passed to the call using the ``Selection`` parameter. :param selection: A Selection object giving selection criteria for the operation :param output_fields: a comma-separated list of output fields :param opts: output formatting and control options :param sort_keys: a comma-separated list of sort keys :param expand: unused :returns: the number of matching profiles output :rtype: int """ output_fields = _expand_fields(_default_cache_fields, output_fields) ces = find_fn(selection=selection) selected = [BoomReportObj(None, None, None, ce) for ce in ces] report_fields = _cache_fields return _do_print_type( report_fields, selected, output_fields=output_fields, opts=opts, sort_keys=sort_keys, ) def show_legacy(selection=None, loader=BOOM_LOADER_GRUB1): """Print boot entries in legacy boot loader formats. :param selection: A Selection object giving selection criteria for the operation :param loader: Which boot loader to use """ (name, decorator, path) = find_legacy_loader(loader, None) bes = find_entries(selection=selection) [print(decorator(be)) for be in bes] # # boom command line tool # def _apply_profile_overrides(boot_entry, cmd_args): if cmd_args.linux: boot_entry.linux = cmd_args.linux if cmd_args.initrd: boot_entry.initrd = cmd_args.initrd def _optional_key_to_arg(optional_key): """Map a Boom optional key name constant to the boom command line argument it corresponds to. Returns the argument name in long option style, or None if no matching optional key exists. """ _key_map = { BOOM_ENTRY_GRUB_USERS: "--grub-users", BOOM_ENTRY_GRUB_ARG: "--grub-arg", BOOM_ENTRY_GRUB_CLASS: "--grub-class", BOOM_ENTRY_GRUB_ID: "--grub-id", } return _key_map[optional_key] if optional_key in _key_map else None def _apply_optional_keys(be, cmd_args): """Set the optional key values defined by ``cmd_args`` in the ``BootEntry`` ``be``. This function assumes that the caller has already checked that the active ``OsProfile`` accepts these optional keys, or will handle exceptions raised by setting an invalid optional key. """ if cmd_args.id: be.id = cmd_args.id.strip() if cmd_args.grub_arg: be.grub_arg = cmd_args.grub_arg.strip() if cmd_args.grub_class: be.grub_class = cmd_args.grub_class.strip() if cmd_args.grub_users: be.grub_users = cmd_args.grub_users.strip() def _set_optional_key_defaults(profile, cmd_args): """Apply default values for all optional keys supported by ``profile`` to command line arguments ``cmd_args``. """ for opt_key in OPTIONAL_KEYS: bls_key = key_to_bls_name(opt_key) if bls_key not in profile.optional_keys: if getattr(cmd_args, bls_key) is not None: print( "Profile with os_id='%s' does not support %s" % (profile.disp_os_id, _optional_key_to_arg(opt_key)) ) return 1 else: if getattr(cmd_args, bls_key) is None: setattr(cmd_args, bls_key, optional_key_default(opt_key)) def _lv_from_device_string(dev_path): """Return an LVM2 vg/lv name from a /dev/mapper device path.""" if dev_path.startswith("/dev/mapper"): vg_lv_name = basename(dev_path) vg_lv_name = re.sub(r"([^-])-([^-])", r"\1/\2", vg_lv_name) vg_lv_name = re.sub(r"--", r"-", vg_lv_name) return vg_lv_name elif dev_path.startswith("/dev"): try: (dev, vg, lv) = dev_path.lstrip("/").split("/") except ValueError: return None return join(vg, lv) else: return None
[docs] def os_options_from_cmdline(): """Generate an os-options template from the running system.""" options = "" vg_lv_name = None have_root = False with open("/proc/cmdline") as proc_cmdline: cmdline = proc_cmdline.read().strip() for word in cmdline.split(): if word.startswith("root="): have_root = True root_dev = word.split("=")[1] vg_lv_name = _lv_from_device_string(root_dev) options += "root=%{root_device} ro %{root_opts} " elif word.startswith("rd.lvm.lv="): if vg_lv_name: if vg_lv_name in word: continue else: options += word + " " elif word.startswith("rootflags="): continue elif word == "ro": continue elif word.startswith("BOOT_IMAGE"): continue elif word.startswith("stratis.rootfs.pool_uuid"): continue else: options += word + " " return options.strip() if have_root else None
def _create_cmd(cmd_args, select, opts, identifier): """Create entry command handler. Attempt to create a new boot entry using the arguments supplied in ``cmd_args`` and return the command status as an integer. :param cmd_args: Command line arguments for the command :param select: Unused :returns: integer status code returned from ``main()`` """ if identifier is not None: print("entry create does not accept <identifier>") return 1 if not cmd_args.version: version = get_uts_release() if not version: print("create requires --version") return 1 else: version = cmd_args.version if not cmd_args.machine_id: # Use host machine-id by default machine_id = _get_machine_id() if not machine_id: print("Could not determine machine_id") return 1 else: machine_id = cmd_args.machine_id if not cmd_args.root_device: print("create requires --root-device") return 1 else: root_device = cmd_args.root_device lvm_root_lv = cmd_args.root_lv if cmd_args.root_lv else None subvol = cmd_args.btrfs_subvolume (subvol_path, subvol_id) = parse_btrfs_subvol(subvol) no_dev = cmd_args.no_dev profile = _find_profile(cmd_args, version, machine_id, "create", optional=False) if not profile: return 1 _set_optional_key_defaults(profile, cmd_args) if not cmd_args.title and not profile.title: print("create requires --title") return 1 else: # Empty title will be filled out by profile title = cmd_args.title add_opts = cmd_args.add_opts del_opts = cmd_args.del_opts arch = cmd_args.architecture images = I_BACKUP if cmd_args.backup else I_NONE try: be = create_entry( title, version, machine_id, root_device, lvm_root_lv=lvm_root_lv, btrfs_subvol_path=subvol_path, btrfs_subvol_id=subvol_id, profile=profile, add_opts=add_opts, del_opts=del_opts, architecture=arch, write=False, expand=cmd_args.expand_variables, allow_no_dev=no_dev, images=images, no_fstab=cmd_args.no_fstab, mounts=cmd_args.mount, swaps=cmd_args.swap, ) except BoomRootDeviceError as brde: print(brde) print("Creating an entry with no valid root device requires --no-dev") return 1 except ValueError as e: print(e) return 1 _apply_profile_overrides(be, cmd_args) _apply_optional_keys(be, cmd_args) try: be.write_entry(expand=cmd_args.expand_variables) __write_legacy() except Exception as e: if cmd_args.debug: raise print(e) return 1 print("Created entry with boot_id %s:" % be.disp_boot_id) print(_str_indent(str(be), 2)) return 0 def _delete_cmd(cmd_args, select, opts, identifier): """Delete entry command handler. Attempt to delete boot entries matching the selection criteria given in ``select``. :param cmd_args: Command line arguments for the command :param select: Selection criteria for the entries to remove :returns: integer status code returned from ``main()`` """ # If a boot_id is given as a command line argument treat it as # a single boot entry to delete and ignore any other criteria. identifier = identifier or cmd_args.boot_id if identifier is not None: select = Selection(boot_id=identifier) if not select or select.is_null(): print("delete requires selection criteria") return 1 if cmd_args.options: fields = cmd_args.options elif cmd_args.verbose: fields = _verbose_entry_fields else: fields = None try: if cmd_args.verbose: print_entries( selection=select, output_fields=fields, opts=opts, sort_keys=cmd_args.sort, ) nr = delete_entries(select) except (ValueError, IndexError) as e: print(e) return 1 print("Deleted %d entr%s" % (nr, "ies" if nr > 1 else "y")) return 0 def _clone_cmd(cmd_args, select, opts, identifier): """Clone entry command handler. Attempt to create a new boot entry by cloning an existing entry. The ``boot_id`` of the supplied ``Selection`` object is used to select the entry to clone. Any set entry values supplied in ``cmd_args`` will be used to modify the newly cloned entry. :param cmd_args: Command line arguments for the command :param select: The ``boot_id`` to clone :returns: integer status code returned from ``main()`` """ identifier = identifier or cmd_args.boot_id if identifier is not None: select = Selection(boot_id=identifier) if not select or select.is_null(): print("clone requires selection criteria") return 1 title = cmd_args.title root_device = cmd_args.root_device lvm_root_lv = cmd_args.root_lv subvol = cmd_args.btrfs_subvolume (subvol_path, subvol_id) = parse_btrfs_subvol(subvol) # Discard all selection criteria but boot_id. select = Selection(boot_id=select.boot_id) try: be = _find_one_entry(select) except ValueError as e: print(e) return 1 version = cmd_args.version or be.version machine_id = cmd_args.machine_id or be.machine_id profile = _find_profile(cmd_args, version, machine_id, "clone") add_opts = cmd_args.add_opts del_opts = cmd_args.del_opts arch = cmd_args.architecture images = I_BACKUP if cmd_args.backup else I_NONE try: be = clone_entry( select, title=title, version=version, machine_id=machine_id, root_device=root_device, lvm_root_lv=lvm_root_lv, btrfs_subvol_path=subvol_path, btrfs_subvol_id=subvol_id, profile=profile, add_opts=add_opts, del_opts=del_opts, architecture=arch, expand=cmd_args.expand_variables, allow_no_dev=cmd_args.no_dev, images=images, no_fstab=cmd_args.no_fstab, mounts=cmd_args.mount, swaps=cmd_args.swap, ) except ValueError as e: print(e) return 1 # Command-line overrides take precedence over any overridden values # in the cloned BootEntry. _apply_profile_overrides(be, cmd_args) _apply_optional_keys(be, cmd_args) try: be.write_entry(expand=cmd_args.expand_variables) __write_legacy() except Exception as e: if cmd_args.debug: raise print(e) return 1 print( "Cloned entry with boot_id %s as boot_id %s:" % (select.boot_id, be.disp_boot_id) ) print(_str_indent(str(be), 2)) return 0 def _show_cmd(cmd_args, select, opts, identifier): """Show entry command handler. Show the boot entries that match the given selection criteria in BLS boot entry notation: one key per line, with keys and values separated by a single space character. :param cmd_args: Command line arguments for the command :param select: Selection criteria for the entries to show. :returns: integer status code returned from ``main()`` """ identifier = identifier or cmd_args.boot_id if identifier is not None: select = Selection(boot_id=identifier) if identifier or cmd_args.all: if select: select.allow_null_profile = True else: select = Selection(allow_null=True) try: bes = find_entries(selection=select) except ValueError as e: print(e) return 1 first = True for be in bes: ws = "" if first else "\n" be_str = be.expanded() if cmd_args.expand_variables else str(be) be_str = _str_indent(be_str, 2) print("%sBoot Entry (boot_id=%s)\n%s" % (ws, be.disp_boot_id, be_str)) first = False return 0 def _generic_list_cmd(cmd_args, select, opts, verbose_fields, print_fn): """Generic list command implementation. Implements a simple list command that applies selection criteria and calls a print_*() API function to display results. Callers should initialise identifier and select appropriately for the specific command arguments. :param cmd_args: the command arguments :param select: selection criteria :param opts: reporting options object :param print_fn: the API call to display results. The function must accept the selection, output_fields, opts, and sort_keys keyword arguments :returns: None """ if cmd_args.options: fields = cmd_args.options elif cmd_args.verbose: fields = verbose_fields else: fields = None try: print_fn( selection=select, output_fields=fields, opts=opts, sort_keys=cmd_args.sort, expand=cmd_args.expand_variables, ) except ValueError as e: print(e) return 1 return 0 def _list_cmd(cmd_args, select, opts, identifier): """List entry command handler. List the boot entries that match the given selection criteria as a tabular report, with one boot entry per row. :param cmd_args: Command line arguments for the command :param select: Selection criteria fore the entries to list :returns: integer status code returned from ``main()`` """ identifier = identifier or cmd_args.boot_id if identifier is not None: select = Selection(boot_id=identifier) if cmd_args.all: if select: select.allow_null_profile = True else: select = Selection(allow_null=True) return _generic_list_cmd( cmd_args, select, opts, _verbose_entry_fields, print_entries ) def _edit_cmd(cmd_args, select, opts, identifier): """Edit entry command handler. Attempt to edit an existing boot entry. The ``boot_id`` of the supplied ``Selection`` object is used to select the entry to edit. Any set entry values supplied in ``cmd_args`` will be used to modify the edited entry. :param cmd_args: Command line arguments for the command :param select: The ``boot_id`` of the entry to edit :returns: integer status code returned from ``main()`` """ identifier = identifier or cmd_args.boot_id if identifier is not None: select = Selection(boot_id=identifier) if not select or select.is_null(): print("edit requires selection criteria") return 1 title = cmd_args.title root_device = cmd_args.root_device lvm_root_lv = cmd_args.root_lv subvol = cmd_args.btrfs_subvolume (subvol_path, subvol_id) = parse_btrfs_subvol(subvol) # Discard all selection criteria but boot_id. select = Selection(boot_id=select.boot_id) try: be = _find_one_entry(select) except ValueError as e: print(e) return 1 version = cmd_args.version or be.version machine_id = cmd_args.machine_id or be.machine_id profile = _find_profile(cmd_args, version, machine_id, "edit") add_opts = cmd_args.add_opts del_opts = cmd_args.del_opts arch = cmd_args.architecture try: be = edit_entry( selection=select, title=title, version=version, machine_id=machine_id, root_device=root_device, lvm_root_lv=lvm_root_lv, btrfs_subvol_path=subvol_path, btrfs_subvol_id=subvol_id, profile=profile, add_opts=add_opts, del_opts=del_opts, architecture=arch, expand=cmd_args.expand_variables, ) except ValueError as e: print(e) return 1 # Command-line overrides take precedence over any overridden values # in the edited BootEntry. _apply_profile_overrides(be, cmd_args) try: be.write_entry(expand=cmd_args.expand_variables) __write_legacy() except Exception as e: if cmd_args.debug: raise print(e) return 1 print("Edited entry, boot_id now: %s" % be.disp_boot_id) print(_str_indent(str(be), 2)) return 0 def _create_profile_cmd(cmd_args, select, opts, identifier): """Create profile command handler. Attempt to create a new OS profile using the arguments supplied in ``cmd_args`` and return the command status as an integer. :param cmd_args: Command line arguments for the command :param select: Unused :returns: integer status code returned from ``main()`` """ if identifier is not None: print("profile create does not accept <identifier>") return 1 if cmd_args.options: print("Invalid argument for profile create: --options") return 1 if cmd_args.os_release or cmd_args.from_host: name = None short_name = None version = None version_id = None release = cmd_args.os_release or "/etc/os-release" else: if not cmd_args.name: print("profile create requires --name") return 1 else: name = cmd_args.name if not cmd_args.short_name: print("profile create requires --short-name") return 1 else: short_name = cmd_args.short_name if not cmd_args.os_version: print("profile create requires --os-version") return 1 else: version = cmd_args.os_version if not cmd_args.os_version_id: print("profile create requires --os-version-id") return 1 else: version_id = cmd_args.os_version_id release = None if cmd_args.from_host and not cmd_args.os_options: options = os_options_from_cmdline() else: options = cmd_args.os_options try: osp = create_profile( name, short_name, version, version_id, uname_pattern=cmd_args.uname_pattern, kernel_pattern=cmd_args.kernel_pattern, initramfs_pattern=cmd_args.initramfs_pattern, root_opts_lvm2=cmd_args.lvm_opts, root_opts_btrfs=cmd_args.btrfs_opts, options=options, optional_keys=cmd_args.optional_keys, profile_file=release, ) except ValueError as e: print(e) return 1 print("Created profile with os_id %s:" % osp.disp_os_id) print(_str_indent(str(osp), 2)) return 0 def _delete_profile_cmd(cmd_args, select, opts, identifier): """Delete profile command handler. Attempt to delete OS profiles matching the selection criteria given in ``select``. :param cmd_args: Command line arguments for the command :param select: Selection criteria for the profiles to remove :returns: integer status code returned from ``main()`` """ # If an os_id is given as a command line argument treat it as # a single OsProfile to delete and ignore any other criteria. identifier = identifier or cmd_args.profile if identifier is not None: select = Selection(os_id=identifier) if not select or select.is_null(): print("profile delete requires selection criteria") return 1 if cmd_args.options: fields = cmd_args.options elif cmd_args.verbose: fields = _verbose_profile_fields else: fields = None try: if cmd_args.verbose: print_profiles(select, output_fields=fields, sort_keys=cmd_args.sort) nr = delete_profiles(select) except (ValueError, IndexError) as e: print(e) return 1 print("Deleted %d profile%s" % (nr, "s" if nr > 1 else "")) return 0 def _clone_profile_cmd(cmd_args, select, opts, identifier): """Clone profile command handler. Attempt to create a new OS profile by cloning an existing profile. The ``os_id`` of the supplied ``Selection`` object is used to select the profile to clone. Any set profile values supplied in ``cmd_args`` will be used to modify the newly cloned profile. :param cmd_args: Command line arguments for the command :param select: The ``os_id`` to clone :returns: integer status code returned from ``main()`` """ name = cmd_args.name short_name = cmd_args.short_name version = cmd_args.os_version version_id = cmd_args.os_version_id uname_pattern = cmd_args.uname_pattern kernel_pattern = cmd_args.kernel_pattern initramfs_pattern = cmd_args.initramfs_pattern root_opts_lvm2 = cmd_args.lvm_opts root_opts_btrfs = cmd_args.btrfs_opts options = cmd_args.os_options identifier = identifier or cmd_args.profile if identifier is not None: select = Selection(os_id=identifier) if not select or select.is_null(): print("profile delete requires selection criteria") return 1 # Discard all selection criteria but os_id. select = Selection(os_id=select.os_id) try: osp = clone_profile( selection=select, name=name, short_name=short_name, version=version, version_id=version_id, uname_pattern=uname_pattern, kernel_pattern=kernel_pattern, initramfs_pattern=initramfs_pattern, root_opts_lvm2=root_opts_lvm2, root_opts_btrfs=root_opts_btrfs, options=options, ) except ValueError as e: print(e) return 1 print("Cloned profile with os_id %s as %s:" % (select.os_id, osp.disp_os_id)) print(_str_indent(str(osp), 2)) return 0 def _generic_show_cmd(select, find_fn, fmt, get_data): """Generic show command handler. Show the objects returned by calling `find_fn` with selection criteria `select`, using the format string `fmt`, and the data tuple returned by calling `get_data` for each object. :param select: Selection() object with search criteria. :param find_fn: A find_*() function accepting Selection. :param fmt: A Python format string. :param get_data: A function returning a tuple of data values satisfying the format string `fmt`. """ try: objs = find_fn(select) except ValueError as e: print(e) return 1 first = True for obj in objs: ws = "" if first else "\n" print(ws + fmt % get_data(obj)) first = False return 0 def _show_profile_cmd(cmd_args, select, opts, identifier): """Show profile command handler. Show the OS profiles that match the given selection criteria in human readable form. Each matching profile is printed as a multi-line record, with like attributes grouped together on a line. :param cmd_args: Command line arguments for the command :param select: Selection criteria for the profiles to show. :returns: integer status code returned from ``main()`` """ identifier = identifier or cmd_args.profile if identifier is not None: select = Selection(os_id=identifier) def _profile_get_data(osp): return (osp.disp_os_id, _str_indent(str(osp), 2)) fmt = "OS Profile (os_id=%s)\n%s" return _generic_show_cmd(select, find_profiles, fmt, _profile_get_data) def _list_profile_cmd(cmd_args, select, opts, identifier): """List profile command handler. List the OS profiles that match the given selection criteria as a tabular report, with one profile per row. :param cmd_args: Command line arguments for the command :param select: Selection criteria fore the profiles to list :returns: integer status code returned from ``main()`` """ identifier = identifier or cmd_args.profile if identifier is not None: select = Selection(os_id=identifier) return _generic_list_cmd( cmd_args, select, opts, _verbose_profile_fields, print_profiles ) def _edit_profile_cmd(cmd_args, select, opts, identifier): """Edit profile command handler. Attempt to edit an existing OS profile. The ``os_id`` of the supplied ``Selection`` object is used to select the profile to edit. Any set entry values supplied in ``cmd_args`` will be used to modify the edited profile. :param cmd_args: Command line arguments for the command :param select: The ``os_id`` of the profile to edit :returns: integer status code returned from ``main()`` """ identifier = identifier or cmd_args.profile if identifier is not None: select = Selection(os_id=identifier) id_keys = ( cmd_args.name, cmd_args.short_name, cmd_args.version, cmd_args.os_version_id, ) if cmd_args.options: print("Invalid argument for profile edit: --options") return 1 if any(id_keys): print( "Cannot edit name, short_name, version, or version_id:\n" "Use 'clone --profile OS_ID'." ) return 1 uname_pattern = cmd_args.uname_pattern kernel_pattern = cmd_args.kernel_pattern initramfs_pattern = cmd_args.initramfs_pattern root_opts_lvm2 = cmd_args.lvm_opts root_opts_btrfs = cmd_args.btrfs_opts options = cmd_args.os_options optional_keys = cmd_args.optional_keys try: osp = edit_profile( selection=select, uname_pattern=uname_pattern, kernel_pattern=kernel_pattern, initramfs_pattern=initramfs_pattern, root_opts_lvm2=root_opts_lvm2, root_opts_btrfs=root_opts_btrfs, options=options, optional_keys=optional_keys, ) except ValueError as e: print(e) return 1 print("Edited profile:") print(_str_indent(str(osp), 2)) return 0 def _create_host_cmd(cmd_args, select, opts, identifier): """Create host profile command handler. Attempt to create a new host profile using the arguments supplied in ``cmd_args`` and return the command status as an integer. :param cmd_args: Command line arguments for the command :param select: Unused :returns: integer status code returned from ``main()`` """ if identifier is not None: print("host profile create does not accept <identifier>") return 1 host_name = cmd_args.host_name or platform.node() if not host_name: print( "host profile create requires a valid host name to be set" "or --host-name" ) return 1 if not cmd_args.machine_id: # Use host machine-id by default machine_id = _get_machine_id() if not machine_id: print("Could not determine machine_id") return 1 else: machine_id = cmd_args.machine_id if not cmd_args.profile: print("host profile create requires --profile") return 1 else: os_id = cmd_args.profile try: hp = create_host( machine_id=machine_id, os_id=os_id, host_name=host_name, label=cmd_args.label, kernel_pattern=cmd_args.kernel_pattern, initramfs_pattern=cmd_args.initramfs_pattern, root_opts_lvm2=cmd_args.lvm_opts, root_opts_btrfs=cmd_args.btrfs_opts, add_opts=cmd_args.add_opts, del_opts=cmd_args.del_opts, options=cmd_args.os_options, ) except ValueError as e: print(e) return 1 print("Created host profile with host_id %s:" % hp.disp_host_id) print(_str_indent(str(hp), 2)) return 0 def _delete_host_cmd(cmd_args, select, opts, identifier): """Delete host profile command handler. Attempt to delete host profiles matching the selection criteria given in ``select``. :param cmd_args: Command line arguments for the command :param select: Selection criteria for the profiles to remove :returns: integer status code returned from ``main()`` """ # If a host_id is given as a command line argument treat it as # a single HostProfile to delete and ignore any other criteria. identifier = identifier or cmd_args.host_id if identifier: select = Selection(host_id=identifier) if not select or select.is_null(): print("host profile delete requires selection criteria") return 1 if cmd_args.options: fields = cmd_args.options elif cmd_args.verbose: fields = _verbose_host_fields else: fields = _default_host_fields try: if cmd_args.verbose: print_hosts(select, output_fields=fields, sort_keys=cmd_args.sort) nr = delete_hosts(select) except (ValueError, IndexError) as e: print(e) return 1 print("Deleted %d profile%s" % (nr, "s" if nr > 1 else "")) return 0 def _clone_host_cmd(cmd_args, select, opts, identifier): """Clone host profile command handler. Attempt to create a new host profile by cloning an existing profile. The ``host_id`` of the supplied ``Selection`` object is used to select the profile to clone. Any set profile values supplied in ``cmd_args`` will be used to modify the newly cloned profile. :param cmd_args: Command line arguments for the command :param select: The ``host_id`` to clone :returns: integer status code returned from ``main()`` """ host_name = cmd_args.host_name os_id = cmd_args.profile identifier = identifier or cmd_args.host_id if identifier is not None: select = Selection(host_id=identifier) # For clone allow the machine_id to be inherited from the original # HostProfile unless the user has given an explicit argument. machine_id = cmd_args.machine_id # Cloning to modify only the host label is permitted label = cmd_args.label # Discard all selection criteria but host_id. select = Selection(host_id=select.host_id) try: hp = clone_host( selection=select, machine_id=machine_id, label=label, os_id=os_id, host_name=host_name, kernel_pattern=cmd_args.kernel_pattern, initramfs_pattern=cmd_args.initramfs_pattern, root_opts_lvm2=cmd_args.lvm_opts, root_opts_btrfs=cmd_args.btrfs_opts, add_opts=cmd_args.add_opts, del_opts=cmd_args.del_opts, options=cmd_args.os_options, ) except ValueError as e: print(e) return 1 print("Cloned profile with host_id %s as %s:" % (select.host_id, hp.disp_host_id)) print(_str_indent(str(hp), 2)) return 0 def _show_host_cmd(cmd_args, select, opts, identifier): """Show host profile command handler. Show the host profiles that match the given selection criteria in human readable form. Each matching profile is printed as a multi-line record, with like attributes grouped together on a line. :param cmd_args: Command line arguments for the command :param select: Selection criteria for the profiles to show. :returns: integer status code returned from ``main()`` """ identifier = identifier or cmd_args.host_id if identifier is not None: select = Selection(host_id=identifier) def _host_get_data(hp): return (hp.disp_host_id, _str_indent(str(hp), 2)) fmt = "Host Profile (host_id=%s)\n%s" return _generic_show_cmd(select, find_host_profiles, fmt, _host_get_data) def _list_host_cmd(cmd_args, select, opts, identifier): """List host profile command handler. List the host profiles that match the given selection criteria as a tabular report, with one profile per row. :param cmd_args: Command line arguments for the command :param select: Selection criteria fore the profiles to list :returns: integer status code returned from ``main()`` """ identifier = identifier or cmd_args.host_id if identifier is not None: select = Selection(host_id=identifier) return _generic_list_cmd(cmd_args, select, opts, _verbose_host_fields, print_hosts) def _edit_host_cmd(cmd_args, select, opts, identifier): """Edit profile command handler. Attempt to edit an existing host profile. The ``host_id`` of the supplied ``Selection`` object is used to select the profile to edit. Any set entry values supplied in ``cmd_args`` will be used to modify the edited profile. :param cmd_args: Command line arguments for the command :param select: The ``host_id`` of the profile to edit :returns: integer status code returned from ``main()`` """ identifier = identifier or cmd_args.host_id if identifier is not None: select = Selection(host_id=identifier) if cmd_args.options: print( "Invalid argument for 'host edit': --options\n" "To modify profile options template use --os-options" ) return 1 machine_id = cmd_args.machine_id os_id = cmd_args.profile host_name = cmd_args.host_name kernel_pattern = cmd_args.kernel_pattern initramfs_pattern = cmd_args.initramfs_pattern root_opts_lvm2 = cmd_args.lvm_opts root_opts_btrfs = cmd_args.btrfs_opts add_opts = cmd_args.add_opts del_opts = cmd_args.del_opts options = cmd_args.os_options try: hp = edit_host( selection=select, machine_id=machine_id, os_id=os_id, host_name=host_name, kernel_pattern=kernel_pattern, initramfs_pattern=initramfs_pattern, root_opts_lvm2=root_opts_lvm2, root_opts_btrfs=root_opts_btrfs, add_opts=add_opts, del_opts=del_opts, options=options, ) except ValueError as e: print(e) return 1 print("Edited profile:") print(_str_indent(str(hp), 2)) return 0 def _show_cache_cmd(cmd_args, select, opts, identifier): """Show cache command handler. Show the cache entries that match the given selection criteria in human readable form. Each matching entry is printed as a multi-line record. :param cmd_args: Command line arguments for the command :param select: Selection criteria for the profiles to show. :returns: integer status code returned from ``main()`` """ if identifier is not None: select = Selection(path=identifier) try: find_fn = find_cache_images if cmd_args.verbose else find_cache_paths ces = find_fn(selection=select) except ValueError as e: print(e) return 1 first = True for ce in ces: ws = "" if first else "\n" ce_str = str(ce) ce_str = _str_indent(ce_str, 2) print("%sCache Entry (img_id=%s)\n%s" % (ws, ce.disp_img_id, ce_str)) first = False return 0 def _list_cache_cmd(cmd_args, select, opts, identifier): """List cache command handler. List the cache entries that match the given selection criteria as a tabular report, with one entry per row. :param cmd_args: Command line arguments for the command :param select: Selection criteria fore the profiles to list :returns: integer status code returned from ``main()`` """ print_fn = print_cache if not cmd_args.verbose else print_cache_images return _generic_list_cmd(cmd_args, select, opts, _verbose_cache_fields, print_fn) def _write_legacy_cmd(cmd_args, select, opts, identifier): if identifier: print("write legacy does not accept a boot_id") return 1 config = get_boom_config() try: clear_legacy_loader() write_legacy_loader(selection=select, loader=config.legacy_format) except Exception as e: print(e) return 1 def _clear_legacy_cmd(cmd_args, select, opts, identifier): """Remove all boom entries from the legacy bootloader configuration. :param cmd_args: Command line arguments for the command :returns: integer status code returned from ``main()`` """ if identifier: print("write legacy does not accept a boot_id") return 1 try: clear_legacy_loader() except BoomLegacyFormatError as e: print(e) return 1 def _show_legacy_cmd(cmd_args, select, opts, identifier): # FIXME: args config = get_boom_config() show_legacy(selection=select, loader=config.legacy_format) CREATE_CMD = "create" DELETE_CMD = "delete" CLONE_CMD = "clone" CLEAR_CMD = "clear" SHOW_CMD = "show" LIST_CMD = "list" EDIT_CMD = "edit" WRITE_CMD = "write" ENTRY_TYPE = "entry" PROFILE_TYPE = "profile" HOST_TYPE = "host" CACHE_TYPE = "cache" LEGACY_TYPE = "legacy" _boom_entry_commands = [ (CREATE_CMD, _create_cmd), (DELETE_CMD, _delete_cmd), (CLONE_CMD, _clone_cmd), (SHOW_CMD, _show_cmd), (LIST_CMD, _list_cmd), (EDIT_CMD, _edit_cmd), ] _boom_profile_commands = [ (CREATE_CMD, _create_profile_cmd), (DELETE_CMD, _delete_profile_cmd), (CLONE_CMD, _clone_profile_cmd), (SHOW_CMD, _show_profile_cmd), (LIST_CMD, _list_profile_cmd), (EDIT_CMD, _edit_profile_cmd), ] _boom_host_commands = [ (CREATE_CMD, _create_host_cmd), (DELETE_CMD, _delete_host_cmd), (CLONE_CMD, _clone_host_cmd), (SHOW_CMD, _show_host_cmd), (LIST_CMD, _list_host_cmd), (EDIT_CMD, _edit_host_cmd), ] _boom_cache_commands = [(SHOW_CMD, _show_cache_cmd), (LIST_CMD, _list_cache_cmd)] _boom_legacy_commands = [ (WRITE_CMD, _write_legacy_cmd), (CLEAR_CMD, _clear_legacy_cmd), (SHOW_CMD, _show_legacy_cmd), ] _boom_command_types = [ (ENTRY_TYPE, _boom_entry_commands), (PROFILE_TYPE, _boom_profile_commands), (HOST_TYPE, _boom_host_commands), (CACHE_TYPE, _boom_cache_commands), (LEGACY_TYPE, _boom_legacy_commands), ] def _get_command_verbs(): """Return the set of command verbs known to boom.""" verbs = set() all_cmds = [ _boom_entry_commands, _boom_profile_commands, _boom_host_commands, _boom_cache_commands, _boom_legacy_commands, ] for cmd_list in all_cmds: verbs.update([cmd[0] for cmd in cmd_list]) return verbs def _id_from_arg(cmd_args, cmdtype, cmd): if cmd == CREATE_CMD: if cmdtype == ENTRY_TYPE: return cmd_args.boot_id if cmdtype == PROFILE_TYPE: return cmd_args.profile else: if cmd_args.identifier: return cmd_args.identifier if cmdtype == ENTRY_TYPE: return cmd_args.boot_id if cmdtype == PROFILE_TYPE: return cmd_args.profile return None def _match_cmd_type(cmdtype): for t in _boom_command_types: if t[0].startswith(cmdtype): return t return None def _match_command(cmd, cmds): for c in cmds: if cmd == c[0]: return c return None def _report_opts_from_args(cmd_args): opts = BoomReportOpts() if not cmd_args: return opts if cmd_args.rows: opts.columns_as_rows = True if cmd_args.separator: opts.separator = cmd_args.separator if cmd_args.name_prefixes: opts.field_name_prefix = "BOOM_" opts.unquoted = False opts.aligned = False if cmd_args.no_headings: opts.headings = False return opts def get_uts_release(): return uname()[2] def setup_logging(cmd_args): global _console_handler level = _default_log_level if cmd_args.verbose and cmd_args.verbose > 1: level = logging.DEBUG elif cmd_args.verbose and cmd_args.verbose > 0: level = logging.INFO # Configure the package-level logger boom_log = logging.getLogger("boom") formatter = logging.Formatter("%(levelname)s - %(message)s") boom_log.setLevel(level) _console_handler = logging.StreamHandler() _console_handler.setLevel(level) _console_handler.setFormatter(formatter) boom_log.addHandler(_console_handler) def shutdown_logging(): logging.shutdown() def set_debug(debug_arg): if not debug_arg: return mask_map = { "profile": BOOM_DEBUG_PROFILE, "entry": BOOM_DEBUG_ENTRY, "report": BOOM_DEBUG_REPORT, "command": BOOM_DEBUG_COMMAND, "all": BOOM_DEBUG_ALL, } mask = 0 for name in debug_arg.split(","): if name not in mask_map: raise ValueError("Unknown debug mask: %s" % name) mask |= mask_map[name] set_debug_mask(mask) def main(args): global _boom_entry_commands, _boom_profile_commands, _boom_command_types parser = ArgumentParser(prog=basename(args[0]), description="Boom Boot Manager") # Default type is boot entry. if len(args) > 1 and _match_command(args[1], _boom_entry_commands): args.insert(1, "entry") parser.add_argument( "type", metavar="[TYPE]", type=str, help="The command type to run: profile or entry", action="store", ) parser.add_argument( "command", metavar="COMMAND", type=str, action="store", help="The command to run: create, delete, list, edit, clone, show", ) parser.add_argument( "identifier", metavar="ID", type=str, action="store", help="An optional profile or boot identifier to operate on", nargs="?", default=None, ) parser.add_argument( "-a", "--add-opts", "--addopts", metavar="OPTIONS", help="Additional kernel options to append", type=str, ) parser.add_argument( "--all", action="store_true", help="Include entries with no valid profile" ) parser.add_argument( "--architecture", metavar="ARCH", default=None, help="An optional BLS architecture string", type=str, ) parser.add_argument( "--backup", action="store_true", help="Back up boot images used by entry" ) parser.add_argument( "-b", "--boot-id", "--bootid", metavar="BOOT_ID", type=str, help="The BOOT_ID of a boom boot entry", ) parser.add_argument( "--boot-dir", "--bootdir", metavar="PATH", type=str, help="The path to the /boot file system", ) parser.add_argument( "-B", "--btrfs-subvolume", "--btrfssubvolume", metavar="SUBVOL", type=str, help="The path or ID of a BTRFS subvolume", ) parser.add_argument( "--btrfs-opts", "--btrfsopts", metavar="OPTS", type=str, help="A template option string for BTRFS devices", ) parser.add_argument( "-c", "--config", metavar="FILE", type=str, help="Path to a boom configuration file", default=None, ) parser.add_argument( "-d", "--del-opts", "--delopts", metavar="OPTIONS", help="List of kernel options to be dropped", type=str, ) parser.add_argument( "--debug", metavar="DEBUGOPTS", type=str, help="A list of debug options to enable", ) parser.add_argument( "-e", "--efi", metavar="IMG", type=str, help="An executable EFI application image", ) parser.add_argument( "-E", "--expand-variables", action="store_true", help="Expand bootloader environment variables", ) parser.add_argument( "--grub-arg", metavar="ARGS", type=str, help="Pass additional arguments to the Grub2 loader", ) parser.add_argument( "--grub-class", metavar="CLASS", type=str, help="Specify a Grub2 class for this entry", ) parser.add_argument( "--grub-users", metavar="USERS", type=str, help="Grub user list for password protection", ) parser.add_argument( "--grub-id", metavar="ID", type=str, dest="id", help="Grub menu identifier string", ) parser.add_argument( "-H", "--from-host", "--fromhost", help="Take os-release values from the running host", action="store_true", ) parser.add_argument( "-P", "--host-profile", metavar="PROFILE", type=str, help="A boom host profile identifier", dest="host_id", ) parser.add_argument( "--host-name", metavar="HOSTNAME", type=str, help="The host name associated with a host profile", ) parser.add_argument( "-i", "--initrd", metavar="IMG", type=str, help="A linux initrd image path" ) parser.add_argument( "-k", "--kernel-pattern", "--kernelpattern", metavar="PATTERN", type=str, help="A pattern for generating kernel paths", ) parser.add_argument("--label", metavar="LABEL", type=str, help="Host profile label") parser.add_argument( "-l", "--linux", metavar="IMG", type=str, help="A linux kernel image path" ) parser.add_argument( "-L", "--root-lv", "--rootlv", metavar="LV", type=str, help="An LVM2 root logical volume", ) parser.add_argument( "--lvm-opts", "--lvmopts", metavar="OPTS", type=str, help="A template option string for LVM2 devices", ) parser.add_argument( "-m", "--machine-id", "--machineid", metavar="MACHINE_ID", type=str, help="The machine_id value to use", ) parser.add_argument( "-M", "--mount", metavar="MOUNT", action="append", help="File system mount specification", ) parser.add_argument( "-n", "--name", metavar="OSNAME", type=str, help="The name of a Boom OsProfile" ) parser.add_argument( "--name-prefixes", "--nameprefixes", help="Add a prefix to report field names", action="store_true", ) parser.add_argument( "--no-headings", "--noheadings", action="store_true", help="Suppress output of report headings", ) parser.add_argument( "--no-dev", "--nodev", action="store_true", help="Disable checks for a valid root device", ) parser.add_argument( "--no-fstab", "--nofstab", action="store_true", help="Ignore mounts and swap devices defined in fstab", ) parser.add_argument( "--optional-keys", metavar="KEYS", type=str, help="Optional keys allows by this operating system profile", ) parser.add_argument( "-o", "--options", metavar="FIELDS", type=str, help="Specify which fields to display", ) parser.add_argument( "--os-version", "--osversion", metavar="OSVERSION", help="A Boom OsProfile version", type=str, ) parser.add_argument( "-O", "--sort", metavar="SORTFIELDS", type=str, help="Specify which fields to sort by", ) parser.add_argument( "-I", "--os-version-id", "--osversionid", help="A Boom OsProfile version ID", metavar="OSVERSIONID", type=str, ) parser.add_argument( "--os-options", "--osoptions", metavar="OPTIONS", help="A Boom OsProfile options template", type=str, ) parser.add_argument( "--os-release", "--osrelease", metavar="OSRELEASE", help="Path to an os-release file", type=str, ) parser.add_argument( "-p", "--profile", metavar="OS_ID", type=str, help="A boom operating system profile identifier", ) parser.add_argument( "-r", "--root-device", "--rootdevice", metavar="ROOT", help="The root device for a boot entry", type=str, ) parser.add_argument( "-R", "--initramfs-pattern", "--initramfspattern", type=str, help="A pattern for generating initramfs paths", metavar="PATTERN", ) parser.add_argument( "--rows", action="store_true", help="Output report columnes as rows" ) parser.add_argument( "--separator", metavar="SEP", type=str, help="Report field separator" ) parser.add_argument( "-s", "--short-name", "--shortname", help="A Boom OsProfile short name", metavar="OSSHORTNAME", type=str, ) parser.add_argument( "--swap", metavar="SWAP", action="append", help="Swap device specification", ) parser.add_argument( "-t", "--title", metavar="TITLE", type=str, help="The title of a boom boot entry", ) parser.add_argument( "-u", "--uname-pattern", "--unamepattern", help="A Boom OsProfile uname pattern", metavar="PATTERN", type=str, ) parser.add_argument("-V", "--verbose", help="Enable verbose output", action="count") parser.add_argument( "-v", "--version", metavar="VERSION", type=str, help="The kernel version of a boom boot entry", ) if len(args) < 3: parser.print_usage() print("Too few arguments: %s" % " ".join(args[1:])) return 1 cmd_types = [cmdtype[0] for cmdtype in _boom_command_types] cmd_verbs = _get_command_verbs() type_arg = args[1] if len(args) > 1 else "" cmd_arg = args[2] if len(args) > 2 else "" if type_arg not in cmd_types or cmd_arg not in cmd_verbs: parser.print_usage() print("Unknown command: %s %s" % (type_arg, cmd_arg)) return 1 try: cmd_args = parser.parse_args(args=args[1:]) except SystemExit as e: return e.code try: set_debug(cmd_args.debug) except ValueError as e: print(e) return 1 setup_logging(cmd_args) cmd_type = _match_cmd_type(cmd_args.type) if not cmd_type: print("Unknown command type: %s" % cmd_args.type) return 1 if cmd_args.boot_dir or BOOM_BOOT_PATH_ENV in environ: boot_path = cmd_args.boot_dir or environ[BOOM_BOOT_PATH_ENV] if not isabs(boot_path): boot_path = join(getcwd(), boot_path) set_boot_path(boot_path) set_boom_config_path("boom.conf") if cmd_args.config: set_boom_config_path(cmd_args.config) try: bc = load_boom_config() except ValueError as e: _log_error("Could not load boom configuration: %s" % e) if not path_exists(get_boom_path()): _log_error("Configuration directory '%s' not found." % get_boom_path()) return 1 if not path_exists(get_boom_config_path()): _log_error("Configuration file '%s' not found." % get_boom_config_path()) return 1 if not path_exists(boom_profiles_path()): _log_error( "OS profile configuration path '%s' not found." % boom_profiles_path() ) return 1 if not path_exists(boom_host_profiles_path()): _log_error( "Host profile configuration path '%s' not found." % boom_host_profiles_path() ) return 1 if not path_exists(boom_entries_path()): _log_error( "Boot loader entries directory '%s' not found." % boom_entries_path() ) return 1 if cmd_type[0] == CACHE_TYPE and not bc.cache_enable: _log_error("Boot image cache disabled (config.cache_enable=False)") return 1 # Parse an LV name from root_lv and re-write the root_device if found if cmd_args.root_lv: try: root_lv = _canonicalize_lv_name(cmd_args.root_lv) except ValueError as e: print(e) print("Invalid logical volume name: '%s'" % cmd_args.root_lv) return 1 root_device = DEV_PATTERN % root_lv if cmd_args.root_device and cmd_args.root_device != root_device: print( "Options --root-lv %s and --root-device %s do not match." % (root_lv, root_device) ) return 1 cmd_args.root_device = root_device cmd_args.root_lv = root_lv # Try parsing an LV name from root_device and rewrite root_lv if found elif cmd_args.root_device: try: root_lv = _canonicalize_lv_name(cmd_args.root_device) cmd_args.root_lv = root_lv except ValueError: # No valid VG name pass type_cmds = cmd_type[1] command = _match_command(cmd_args.command, type_cmds) if not command: print("Unknown command: %s %s" % (cmd_type[0], cmd_args.command)) return 1 if cmd_args.backup and not bc.cache_enable: print("--backup specified but cache disabled" " (config.cache_enable=False)") return 1 elif cmd_args.backup: load_cache() select = Selection.from_cmd_args(cmd_args) opts = _report_opts_from_args(cmd_args) identifier = _id_from_arg(cmd_args, cmd_type[0], command[0]) status = 1 if cmd_args.debug: status = command[1](cmd_args, select, opts, identifier) else: try: status = command[1](cmd_args, select, opts, identifier) except Exception as e: _log_error("Command failed: %s" % e) if bc.cache_enable and bc.cache_auto_clean: try: clean_cache() except Exception as e: _log_error("Could not clean boot image cache: %s" % e) shutdown_logging() return status __all__ = [ # BootEntry manipulation "create_entry", "delete_entries", "clone_entry", "edit_entry", "list_entries", "print_entries", # OsProfile manipulation "create_profile", "delete_profiles", "clone_profile", "edit_profile", "list_profiles", "print_profiles", "os_options_from_cmdline", # HostProfile manipulation "create_host", "delete_hosts", "clone_host", "edit_host", "list_hosts", "print_hosts", # Cache manipulation "print_cache", "print_cache_images", "I_NONE", "I_CACHE", "I_BACKUP", ] # vim: set et ts=4 sw=4 :