# 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 boom import *
from boom.osprofile import *
from boom.report import *
from boom.bootloader import *
from boom.legacy import *
from boom.config import *

import sys
from os import environ, uname
from os.path import basename
from argparse import ArgumentParser
import logging

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

# 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

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

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

            :returns: a new BoomReportObj.
            :returntype: ``BoomReportObj``
        """
        self.be = boot_entry
        self.osp = os_profile


#: BootEntry report object type
BR_ENTRY = 1
#: OsProfile report object type
BR_PROFILE = 2
#: BootParams report object type
BR_PARAMS = 4

#: 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)
]

#
# 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.name)),
    BoomFieldType(
        BR_PROFILE, "osshortname", "OsShortName", "OS short name", 12,
        REP_STR, lambda f, d: f.report_str(d.short_name)),
    BoomFieldType(
        BR_PROFILE, "osversion", "OsVersion", "OS version", 10,
        REP_STR, lambda f, d: f.report_str(d.version)),
    BoomFieldType(
        BR_PROFILE, "osversion_id", "VersionID", "Version identifier", 10,
        REP_STR, lambda f, d: f.report_str(d.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", "Profile path", "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"

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

#: 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", "Machine ID", "Machine identifier", 12,
        REP_SHA, lambda f, d: f.report_sha(d.machine_id)),
    BoomFieldType(
        BR_ENTRY, "entrypath", "Entry path", "On-disk entry path", 12,
        REP_STR, lambda f, d: f.report_str(d._entry_path))
]

#: 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")


def _subvol_from_arg(subvol):
    """Parse a BTRFS subvolume from a string argument.

        Parse a BTRFS subvolume path or identifier from a command line
        argument string. Numeric values are assumed to be a subvolume ID
        and values beginning with a '/' character are assumed to be a
        subvolume path.

        :param subvol: A subvolume path or ID string
        :returns: (path, id) tuple or (None, None) if neither is found
        :returntype: (str, str)
    """
    if not subvol:
        return (None, None)
    subvol = _parse_btrfs_subvol(subvol)
    if subvol.startswith('/'):
        btrfs_subvol_path = subvol
        btrfs_subvol_id = None
    else:
        btrfs_subvol_path = None
        btrfs_subvol_id = subvol
    return (btrfs_subvol_path, btrfs_subvol_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):
        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:
        write_legacy_loader(selection=Selection(), loader=config.legacy_format)


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

#
# BootEntry manipulation
#

def create_entry(title, version, machine_id, root_device, lvm_root_lv=None,
                 btrfs_subvol_path=None, btrfs_subvol_id=None, osprofile=None,
                 write=True, allow_no_dev=False):
    """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 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 osprofile: The ``OsProfile`` for this entry.
        :param allow_no_dev: Accept a non-existent or invalid root dev.
        :returns: a ``BootEntry`` object corresponding to the new entry.
        :returntype: ``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:
        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 osprofile:
        raise ValueError("Cannot create entry without OsProfile.")

    btrfs = any([btrfs_subvol_path, btrfs_subvol_id])

    bp = BootParams(version, root_device, lvm_root_lv=lvm_root_lv,
                    btrfs_subvol_path=btrfs_subvol_path,
                    btrfs_subvol_id=btrfs_subvol_id)

    be = BootEntry(title=title, machine_id=machine_id,
                   osprofile=osprofile, boot_params=bp,
                   allow_no_dev=allow_no_dev)

    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()
        __write_legacy()

    return be


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.
        :returntype: ``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


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, osprofile=None, write=True):
    """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 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 osprofile: The ``OsProfile`` for this entry.
        :returns: a ``BootEntry`` object corresponding to the new entry.
        :returntype: ``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:
        raise ValueError("clone requires boot_id")
        return 1

    all_args = (title, version, machine_id, root_device, lvm_root_lv,
                btrfs_subvol_path, btrfs_subvol_id, osprofile)

    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, osprofile")
        return 1

    bes = find_entries(selection)
    if len(bes) > 1:
        raise ValueError("clone criteria must match exactly one entry")
        return 1

    be = bes[0]

    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
    root_device = root_device if root_device else be.bp.root_device
    lvm_root_lv = lvm_root_lv if lvm_root_lv else be.bp.lvm_root_lv
    btrfs_subvol_path = (btrfs_subvol_path if btrfs_subvol_path
                         else be.bp.btrfs_subvol_path)
    btrfs_subvol_id = (btrfs_subvol_id if btrfs_subvol_id
                       else be.bp.btrfs_subvol_id)
    osprofile = osprofile if osprofile else be._osp

    bp = BootParams(version, root_device, lvm_root_lv=lvm_root_lv,
                    btrfs_subvol_path=btrfs_subvol_path,
                    btrfs_subvol_id=btrfs_subvol_id)

    clone_be = BootEntry(title=title, machine_id=machine_id,
                         osprofile=osprofile, boot_params=bp)
    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

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, osprofile=None, write=True):
    """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
        :returns: The modified ``BootEntry``
        :returntype: ``BootEntry``
    """
   # Discard all selection criteria but boot_id.
    selection = Selection(boot_id=selection.boot_id)

    osp = None
    if osprofile:
        os_id = osprofile
        osps = find_profiles(Selection(os_id=os_id))
        if not osps:
            raise ValueError("No matching profile found: %s" % os_id)
        if len(osps) > 1:
            raise ValueError("OS profile identifier '%s' is ambiguous" % os_id)
            return 1
        osp = osps.pop()

    bes = find_entries(selection=selection)
    if not bes:
        raise ValueError("No matching entry found for boot ID %s" %
                         selection.disp_boot_id)
        return 1

    be = bes.pop()
    # Boot ID will change: clean up old file.
    be.delete_entry()
    be._osp = osp or be._osp
    be.title = title or be.title
    be.version = version or be.version
    be.machine_id = machine_id or be.machine_id
    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

    if write:
        be.write_entry()
        __write_legacy()

    return be


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.
        :returntype: list
    """
    bes = find_entries(selection=selection)

    return bes


def print_entries(selection=None, output_fields=None, opts=None,
                  sort_keys=None):
    """Print boot loader entries matching selection criteria.

        Format a set of ``boom.bootloader.BootEntry`` objects matching
        the given criteria, and output them as a report to the file
        given in ``out_file``, or ``sys.stdout`` if ``out_file`` is
        unset.

        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
        :returns: the ``boot_id`` of the new entry
        :returntype: str
    """
    opts = opts if opts is not None else BoomReportOpts()

    if not output_fields:
        output_fields = _default_entry_fields
    elif output_fields.startswith('+'):
        output_fields = _default_entry_fields + ',' + output_fields[1:]

    bes = find_entries(selection=selection)

    br = BoomReport(_report_obj_types, _entry_fields + _profile_fields +
                    _params_fields, output_fields, opts, sort_keys, None)
    for be in bes:
        br.report_object(BoomReportObj(be, be._osp))

    return br.report_output()

#
# OsProfile manipulation
#

def _os_profile_from_file(os_release, uname_pattern,
                          kernel_pattern, initramfs_pattern,
                          root_opts_lvm2, root_opts_btrfs,
                          options):
    """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 kernel_pattern: A replacement kernel_pattern value
        :param initramfs_pattern: A replacement initramfs_pattern value
        :param root_opts_lvm2: Replacement LVM2 root options
        :param root_opts_btrfs: Replacement BTRFS root options
        :param options: Replacement options string template
        :returns: A new OsProfile
        :returntype: OsProfile
    """
    osp = OsProfile.from_os_release_file(os_release)
    if uname_pattern:
        osp.uname_pattern = uname_pattern
    if kernel_pattern:
        osp.kernel_pattern = kernel_pattern
    if initramfs_pattern:
        osp.initramfs_pattern = initramfs_pattern
    if root_opts_lvm2:
        osp.root_opts_lvm2 = root_opts_lvm2
    if root_opts_btrfs:
        osp.root_opts_btrfs = root_opts_btrfs
    if options:
        osp.options = options

    if find_profiles(selection=Selection(os_id=osp.os_id)):
        raise ValueError("Profile already exists (os_id=%s)" % osp.disp_os_id)

    osp.write_profile()
    return osp


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,
                   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
    
        :returns: an ``OsProfile`` object for the new profile
        :returntype: ``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 profile_file:
        return _os_profile_from_file(profile_file, uname_pattern,
                                     kernel_pattern, initramfs_pattern,
                                     root_opts_lvm2, root_opts_btrfs,
                                     options)

    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.")

    if not profile_data:
        profile_data = {}

    # 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
    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

    osp = OsProfile(name, short_name, version, version_id,
                    profile_data=profile_data)

    if find_profiles(selection=Selection(os_id=osp.os_id)):
        raise ValueError("Profile already exists (os_id=%s)" % osp.disp_os_id)

    osp.write_profile()
    return osp

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.
        :returntype: ``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


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.

        :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 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.
        :returntype: ``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")
        return 1

    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')
        return 1

    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")
        return 1

    osp = osps.pop()

    # Clone unset keys
    name = name if name else osp.name
    short_name = short_name if short_name else osp.short_name
    version = version if version else osp.version
    version_id = version_id if version_id else osp.version_id
    uname_pattern = uname_pattern if uname_pattern else osp.uname_pattern
    kernel_pattern = kernel_pattern if kernel_pattern else osp.kernel_pattern
    root_opts_lvm2 = root_opts_lvm2 if root_opts_lvm2 else osp.root_opts_lvm2
    root_opts_btrfs = (root_opts_btrfs if root_opts_btrfs
                         else osp.root_opts_btrfs)
    options = options if options else osp.options

    pd = {}
    if name:
        pd[BOOM_OS_NAME] = name
    if short_name:
        pd[BOOM_OS_SHORT_NAME] = short_name
    if version:
        pd[BOOM_OS_VERSION] = version
    if version_id:
        pd[BOOM_OS_VERSION_ID] = version_id
    if uname_pattern:
        pd[BOOM_OS_UNAME_PATTERN] = uname_pattern
    if kernel_pattern:
        pd[BOOM_OS_KERNEL_PATTERN] = kernel_pattern
    if initramfs_pattern:
        pd[BOOM_OS_INITRAMFS_PATTERN] = initramfs_pattern
    if root_opts_lvm2:
        pd[BOOM_OS_ROOT_OPTS_LVM2] = root_opts_lvm2
    if root_opts_btrfs:
        pd[BOOM_OS_ROOT_OPTS_BTRFS] = root_opts_btrfs
    if options:
        pd[BOOM_OS_OPTIONS] = options

    clone_osp = OsProfile(None, None, None, None, profile_data=pd)

    if find_profiles(Selection(os_id=clone_osp.os_id)):
        raise ValueError("Profile already exists (os_id=%s)." %
                         clone_osp.disp_os_id)

    clone_osp.write_profile()

    return clone_osp


def edit_profile(selection=None, uname_pattern=None, kernel_pattern=None,
                 initramfs_pattern=None, root_opts_lvm2=None,
                 root_opts_btrfs=None, options=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``
        :returntype: ``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)
        return 1

    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.write_profile()
    return osp


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.
        :returntype: list
    """
    osps = find_profiles(selection=selection)

    return osps

def print_profiles(selection=None, opts=None, output_fields=None,
                   sort_keys=None):
    """Print operating system profiles 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
        :returns: the number of matching profiles output.
        :returntype: int
    """
    opts = opts if opts else BoomReportOpts()

    if not output_fields:
        output_fields = _default_profile_fields
    elif output_fields.startswith('+'):
        output_fields = _default_profile_fields + ',' + output_fields[1:]

    osps = find_profiles(selection=selection)

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

    for osp in osps:
        br.report_object(BoomReportObj(None, osp))

    return br.report_output()


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 fmt: The name of a legacy boot loader format
    """
    (name, decorator, path)  = find_legacy_loader(loader, None)
    bes = find_entries(selection=selection)
    for be in bes:
        gbe = Grub1BootEntry(entry_data=be._entry_data)
        print(gbe)

#
# boom command line tool
#

def _apply_profile_overrides(boot_entry, cmd_args):
    if cmd_args.linux:
        boot_entry.linux = cmd_args.linux
        modified = True

    if cmd_args.initrd:
        boot_entry.initrd = cmd_args.initrd
        modified = True


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 not check_bootloader():
        _log_warn("Boom configuration not found in grub.cfg")
        _log_warn("Run 'grub2-mkconfig > /boot/grub2/grub.cfg' to enable")

    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.title:
        print("create requires --title")
        return 1
    else:
        title = cmd_args.title

    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
    (btrfs_subvol_path, btrfs_subvol_id) = _subvol_from_arg(subvol)

    no_dev = cmd_args.no_dev

    # FIXME: default to host OsProfile
    if not cmd_args.profile:
        # Attempt to find a matching OsProfile by version string
        osp = match_os_profile_by_version(version)
        if not osp:
            print("create requires --profile")
            return 1
        os_id = osp.os_id
    else:
        os_id = cmd_args.profile

    osps = find_profiles(Selection(os_id=os_id))

    if not osps:
        print("OsProfile not found: %s" % os_id)
        return 1

    if len(osps) > 1:
        print("OsProfile ID '%s' is ambiguous" % os_id)
        return 1

    osp = osps[0]

    try:
        be = create_entry(title, version, machine_id,
                          root_device, lvm_root_lv=lvm_root_lv,
                          btrfs_subvol_path=btrfs_subvol_path,
                          btrfs_subvol_id=btrfs_subvol_id, osprofile=osp,
                          write=False, allow_no_dev=no_dev)
    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)

    try:
        be.write_entry()
        __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))


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.
    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"))


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()``
    """
    if identifier is not None:
        select = Selection(boot_id=identifier)

    title = cmd_args.title
    version = cmd_args.version
    machine_id = cmd_args.machine_id
    root_device = cmd_args.root_device
    lvm_root_lv = cmd_args.root_lv
    subvol = cmd_args.btrfs_subvolume
    (btrfs_subvol_path, btrfs_subvol_id) = _subvol_from_arg(subvol)

    # Discard all selection criteria but boot_id.
    select = Selection(boot_id=select.boot_id)

    osp = None
    if cmd_args.profile:
        osps = find_profiles(Selection(os_id=cmd_args.profile))
        if not osps:
            print("OsProfile not found: %s" % cmd_args.profile)
            return 1
        if len(osps) > 1:
            print("OS profile identifier '%s' is ambiguous" %
                  cmd_args.profile)
            return 1
        osp = osps[0]

    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=btrfs_subvol_path,
                         btrfs_subvol_id=btrfs_subvol_id, osprofile=osp)
    except ValueError as e:
        print(e)
        return 1

    _apply_profile_overrides(be, cmd_args)

    try:
        be.write_entry()
        __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()``
    """
    if identifier is not None:
        select = Selection(boot_id=identifier)

    try:
        bes = find_entries(select)
    except ValueError as e:
        print(e)
        return 1
    first = True
    for be in bes:
        ws = "" if first else "\n"
        be_str = _str_indent(str(be), 2)
        print("%sBoot Entry (boot_id=%s)\n%s" % (ws, be.disp_boot_id, be_str))
        first = False
    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()``
    """
    if identifier is not None:
        select = Selection(boot_id=identifier)

    if cmd_args.options:
        fields = cmd_args.options
    elif cmd_args.verbose:
        fields = _verbose_entry_fields
    else:
        fields = None

    try:
        print_entries(selection=select, output_fields=fields,
                      opts=opts, sort_keys=cmd_args.sort)
    except ValueError as e:
        print(e)
        return 1


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()``
    """
    if identifier is not None:
        select = Selection(boot_id=identifier)

    title = cmd_args.title
    version = cmd_args.version
    machine_id = cmd_args.machine_id
    root_device = cmd_args.root_device
    lvm_root_lv = cmd_args.root_lv
    subvol = cmd_args.btrfs_subvolume
    (btrfs_subvol_path, btrfs_subvol_id) = _subvol_from_arg(subvol)

    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=btrfs_subvol_path,
                        btrfs_subvol_id=btrfs_subvol_id)
    except ValueError as e:
        print(e)
        return 1

    _apply_profile_overrides(be, cmd_args)

    try:
        be.write_entry()
        __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.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 not cmd_args.uname_pattern:
        print("profile create requires --uname-pattern")
        return 1

    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=cmd_args.os_options, 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.
    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 ""))


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

    if identifier is not None:
        select = Selection(os_id=identifier)

    # Discard all selection criteria but os_id.
    select = Selection(os_id=select.os_id)

    try:
        be = 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 _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()``
    """
    if identifier is not None:
        select = Selection(os_id=identifier)

    try:
        osps = find_profiles(select)
    except ValueError as e:
        print(e)
        return 1
    first = True
    for osp in osps:
        ws = "" if first else "\n"
        osp_str = _str_indent(str(osp), 2)
        print("%sOS Profile (os_id=%s)\n%s" % (ws, osp.disp_os_id, osp_str))
        first = False
    return 0


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()``
    """
    if identifier is not None:
        select = Selection(os_id=identifier)

    if cmd_args.options:
        fields = cmd_args.options
    elif cmd_args.verbose:
        fields = _verbose_profile_fields
    else:
        fields = None

    try:
        print_profiles(selection=select, output_fields=fields,
                       opts=opts, sort_keys=cmd_args.sort)
    except ValueError as e:
        print(e)
        return 1


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()``
    """
    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

    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)
    except ValueError as e:
        print(e)
        return 1

    print("Edited profile:")
    print(_str_indent(str(osp), 2))
    return 0


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)


boom_usage = """%(prog}s [type] <command> [options]\n\n"
                [entry] create <title> <version> [--osprofile=os_id] [...]
                [entry] delete [title|version|boot_id|os_id]
                [entry] clone --boot-id ID
                [entry] list [title|version|boot_id|os_id|root_device|machine_id]\n\n
                [entry] edit [...]
                profile create <name> <shortname> <version> <versionid> [...]
                profile delete [...]
                profile list [...]
                profile edit [...]
                legacy write [...]
                legacy delete [...]
             """

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"
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_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),
    (LEGACY_TYPE, _boom_legacy_commands)
]


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("-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("--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("-H", "--from-host", "--fromhost",
                        help="Take os-release values from the running host",
                        action="store_true")
    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("-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("-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("-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("-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 ouput",
                        action="count")
    parser.add_argument("-v", "--version", metavar="VERSION", type=str,
                        help="The kernel version of a boom "
                        "boot entry")
    cmd_args = parser.parse_args()

    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 cmd_args.boot_dir or BOOM_BOOT_PATH_ENV in environ:
        boot_path = cmd_args.boot_dir or environ[BOOM_BOOT_PATH_ENV]
        set_boot_path(boot_path)
        set_boom_config_path("boom.conf")

    if cmd_args.config:
        set_boom_config_path(cmd_args.config)

    load_boom_config(path=cmd_args.config)

    # 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

    if not cmd_type:
        print("Unknown command type: %s" % cmd_args.type)
        return 1

    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

    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)

    shutdown_logging()
    sys.exit(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',
]

# vim: set et ts=4 sw=4 :
