Source code for boom.hostprofile

# Copyright (C) 2017 Red Hat, Inc., Bryn M. Reeves <bmr@redhat.com>
#
# hostprofile.py - Boom host profiles
#
# 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.hostprofile``  module defines the `HostProfile` class that
represents a host system profile. A `HostProfile` defines the identity
of a host and includes template values that override the corresponding
``OsProfile`` defaults for the respective host.

Functions are provided to read and write host system profiles from
an on-disk store, and to retrieve ``HostProfile`` instances using
various selection criteria.

The ``HostProfile`` class includes named properties for each profile
attribute ("profile key"). In addition, the class serves as a container
type, allowing attributes to be accessed via dictionary-style indexing.
This simplifies iteration over a profile's key / value pairs and allows
straightforward access to all members in scripts and the Python shell.

The keys used to access ``HostProfile`` members (and their corresponding
property names) are identical to those used by the ``OsProfile`` class.
"""
from __future__ import print_function

from hashlib import sha1
from os.path import join as path_join
import logging
import string

from boom import *
from boom.osprofile import *

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

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

#: Global host profile list
_host_profiles = []
_host_profiles_by_id = {}
_host_profiles_by_host_id = {}

#: Whether profiles have been read from disk
_host_profiles_loaded = False

#: Boom profiles directory name.
BOOM_HOST_PROFILES = "hosts"

#: File name format for Boom profiles.
BOOM_HOST_PROFILE_FORMAT = "%s-%s.host"

#: The file mode with which to create Boom profiles.
BOOM_HOST_PROFILE_MODE = 0o644

# Constants for Boom profile keys
#: Constant for the Boom host identifier profile key.
BOOM_HOST_ID = "BOOM_HOST_ID"
#: Constant for the Boom host name profile key.
BOOM_HOST_NAME = "BOOM_HOST_NAME"
#: Constant for the Boom host add options key.
BOOM_HOST_ADD_OPTS = "BOOM_HOST_ADD_OPTS"
#: Constant for the Boom host del options key.
BOOM_HOST_DEL_OPTS = "BOOM_HOST_DEL_OPTS"
#: Constant for the Boom host label key.
BOOM_HOST_LABEL = "BOOM_HOST_LABEL"

#: Constant for shared machine_id key
BOOM_ENTRY_MACHINE_ID = "BOOM_ENTRY_MACHINE_ID"

#: Ordered list of possible host profile keys, partitioned into
#: mandatory keys, optional host profile keys, keys mapping to
#: embedded ``OsProfile`` identity data, and ``OsProfile`` pattern
#: keys that may be overridden in the ``HostProfile``.
HOST_PROFILE_KEYS = [
    # HostProfile identifier
    BOOM_HOST_ID,
    # Machine hostname
    BOOM_HOST_NAME,
    # Binding to label, machine_id and OsProfile
    BOOM_ENTRY_MACHINE_ID,
    BOOM_OS_ID,
    # Optional host profile keys
    BOOM_HOST_LABEL,
    BOOM_HOST_ADD_OPTS,
    BOOM_HOST_DEL_OPTS,
    # Keys 7-10 OS identity keys mapped to the embedded OsProfile.
    BOOM_OS_NAME,
    BOOM_OS_SHORT_NAME,
    BOOM_OS_VERSION,
    BOOM_OS_VERSION_ID,
    # Keys 11-15 (OsProfile patterns) may be overridden in the host profile.
    BOOM_OS_UNAME_PATTERN,
    BOOM_OS_KERNEL_PATTERN,
    BOOM_OS_INITRAMFS_PATTERN,
    BOOM_OS_ROOT_OPTS_LVM2,
    BOOM_OS_ROOT_OPTS_BTRFS,
    BOOM_OS_OPTIONS,
]

#: A map of Boom host profile keys to human readable key names suitable
#: for use in formatted output. These key names are used to format a
#: ``HostProfile`` object as a human readable string.
HOST_KEY_NAMES = {
    # Keys unique to HostProfile
    BOOM_HOST_ID: "Host ID",
    BOOM_HOST_NAME: "Host name",
    BOOM_HOST_LABEL: "Host label",
    BOOM_HOST_ADD_OPTS: "Add options",
    BOOM_HOST_DEL_OPTS: "Del options",
    # Keys shared with BootEntry
    BOOM_ENTRY_MACHINE_ID: "Machine ID",
    # Keys incorporated from OsProfile
    BOOM_OS_ID: OS_KEY_NAMES[BOOM_OS_ID],
    BOOM_OS_NAME: OS_KEY_NAMES[BOOM_OS_NAME],
    BOOM_OS_SHORT_NAME: OS_KEY_NAMES[BOOM_OS_SHORT_NAME],
    BOOM_OS_VERSION: OS_KEY_NAMES[BOOM_OS_VERSION],
    BOOM_OS_VERSION_ID: OS_KEY_NAMES[BOOM_OS_VERSION_ID],
    BOOM_OS_UNAME_PATTERN: OS_KEY_NAMES[BOOM_OS_UNAME_PATTERN],
    BOOM_OS_KERNEL_PATTERN: OS_KEY_NAMES[BOOM_OS_KERNEL_PATTERN],
    BOOM_OS_INITRAMFS_PATTERN: OS_KEY_NAMES[BOOM_OS_INITRAMFS_PATTERN],
    BOOM_OS_ROOT_OPTS_LVM2: OS_KEY_NAMES[BOOM_OS_ROOT_OPTS_LVM2],
    BOOM_OS_ROOT_OPTS_BTRFS: OS_KEY_NAMES[BOOM_OS_ROOT_OPTS_BTRFS],
    BOOM_OS_OPTIONS: OS_KEY_NAMES[BOOM_OS_OPTIONS],
}

#: Boom host profile keys that must exist in a valid profile.
HOST_REQUIRED_KEYS = HOST_PROFILE_KEYS[0:4]

#: Boom optional host profile configuration keys.
HOST_OPTIONAL_KEYS = HOST_PROFILE_KEYS[4:]


def _host_exists(host_id):
    """Test whether the specified ``host_id`` already exists.

    Used during ``HostProfile`` initialisation to test if the new
    ``host_id`` is already known (and to avoid passing through
    find_profiles(), which may trigger recursive profile loading).

    :param host_id: the host identifier to check for

    :returns: ``True`` if the identifier is known or ``False``
              otherwise.
    :rtype: bool
    """
    global _host_profiles_by_host_id
    if not _host_profiles_by_host_id:
        return False
    if host_id in _host_profiles_by_host_id:
        return True
    return False


[docs] def boom_host_profiles_path(): """Return the path to the boom host profiles directory. :returns: The boom host profiles path. :rtype: str """ return path_join(get_boom_path(), BOOM_HOST_PROFILES)
[docs] def host_profiles_loaded(): """Test whether profiles have been loaded from disk. :rtype: bool :returns: ``True`` if profiles are loaded in memory or ``False`` otherwise """ return _host_profiles_loaded
[docs] def drop_host_profiles(): """Drop all in-memory host profiles.""" global _host_profiles, _host_profiles_by_id, _host_profiles_by_host_id global _host_profiles_loaded _host_profiles = [] _host_profiles_by_id = {} _host_profiles_by_host_id = {} _host_profiles_loaded = False
[docs] def load_host_profiles(): """Load HostProfile data from disk. Load the set of host profiles found at the path ``boom.hostprofile.boom_profiles_path()`` into the global host profile list. This function may be called to explicitly load, or reload the set of profiles on-disk. Profiles are also loaded implicitly if an API function or method that requires access to profiles is invoked (for example, ``boom.bootloader.load_entries()``. :returns: None """ global _host_profiles_loaded drop_host_profiles() profiles_path = boom_host_profiles_path() load_profiles_for_class(HostProfile, "Host", profiles_path, "host") _host_profiles_loaded = True _log_debug("Loaded %d host profiles" % len(_host_profiles))
[docs] def write_host_profiles(force=False): """Write all HostProfile data to disk. Write the current list of host profiles to the directory located at ``boom.osprofile.boom_profiles_path()``. :rtype: None """ global _host_profiles _log_debug("Writing host profiles to %s" % boom_host_profiles_path()) for hp in _host_profiles: try: hp.write_profile(force) except Exception as e: _log_warn( "Failed to write HostProfile(machine_id='%s'): %s" % (hp.disp_machine_id, e) )
def min_host_id_width(): """Calculate the minimum unique width for host_id values. Calculate the minimum width to ensure uniqueness when displaying host_id values. :returns: the minimum host_id width. :rtype: int """ return min_id_width(7, _host_profiles, "host_id") def min_machine_id_width(): """Calculate the minimum unique width for host_id values. Calculate the minimum width to ensure uniqueness when displaying host_id values. :returns: the minimum host_id width. :rtype: int """ return min_id_width(7, _host_profiles, "machine_id")
[docs] def select_host_profile(s, hp): """Test the supplied host profile against selection criteria. Test the supplied ``HostProfile`` against the selection criteria in ``s`` and return ``True`` if it passes, or ``False`` otherwise. :param s: The selection criteria :param hp: The ``HostProfile`` to test :rtype: bool :returns: True if ``hp`` passes selection or ``False`` otherwise. """ if s.host_id and not hp.host_id.startswith(s.host_id): return False if s.machine_id and hp.machine_id != s.machine_id: return False if s.host_name and hp.host_name != s.host_name: return False if s.host_label and hp.label != s.host_label: return False if s.host_short_name and hp.short_name != s.host_short_name: return False if s.host_add_opts and hp.add_opts != s.host_add_opts: return False if s.host_del_opts and hp.del_opts != s.host_del_opts: return False if s.os_id and not hp.os_id.startswith(s.os_id): return False if s.os_name and hp.os_name != s.os_name: return False if s.os_short_name and hp.os_short_name != s.os_short_name: return False if s.os_version and hp.os_version != s.os_version: return False if s.os_version_id and hp.os_version_id != s.os_version_id: return False if s.os_uname_pattern and hp.uname_pattern != s.os_uname_pattern: return False if s.os_kernel_pattern and hp.kernel_pattern != s.os_kernel_pattern: return False if s.os_initramfs_pattern and hp.initramfs_pattern != s.os_initramfs_pattern: return False if s.os_options and hp.options != s.os_options: return False return True
[docs] def find_host_profiles(selection=None, match_fn=select_host_profile): """Find host profiles matching selection criteria. Return a list of ``HostProfile`` objects matching the specified criteria. Matching proceeds as the logical 'and' of all criteria. Criteria that are unset (``None``) are ignored. If the optional ``match_fn`` parameter is specified, the match criteria parameters are ignored and each ``HostProfile`` is tested in turn by calling ``match_fn``. If the matching function returns ``True`` the ``HostProfile`` will be included in the results. If no ``HostProfile`` matches the specified criteria the empty list is returned. Host profiles will be automatically loaded from disk if they are not already in memory. :param selection: A ``Selection`` object specifying the match criteria for the operation. :param match_fn: An optional match function to test profiles. :returns: a list of ``HostProfile`` objects. :rtype: list """ # Use null search criteria if unspecified selection = selection if selection else Selection() selection.check_valid_selection(host=True) if not host_profiles_loaded(): load_host_profiles() matches = [] _log_debug_profile("Finding host profiles for %s" % repr(selection)) for hp in _host_profiles: if match_fn(selection, hp): matches.append(hp) _log_debug_profile("Found %d host profiles" % len(matches)) matches.sort(key=lambda h: h.host_name) return matches
[docs] def get_host_profile_by_id(machine_id, label=""): """Find a HostProfile by its machine_id. Return the HostProfile object corresponding to ``machine_id``, or ``None`` if it is not found. :rtype: HostProfile :returns: An HostProfile matching machine_id or None if no match was found. """ global _host_profiles, _host_profiles_by_id, _host_profiles_by_host_id if not host_profiles_loaded(): load_host_profiles() if machine_id in _host_profiles_by_id: if label in _host_profiles_by_id[machine_id]: return _host_profiles_by_id[machine_id][label] return None
[docs] def match_host_profile(entry): """Attempt to match a BootEntry to a corresponding HostProfile. Attempt to find a loaded ``HostProfile`` object with the a ``machine_id`` that matches the supplied ``BootEntry``. Checking terminates on the first matching ``HostProfile``. :param entry: A ``BootEntry`` object with no attached ``HostProfile``. :returns: The corresponding ``HostProfile`` for the supplied ``BootEntry`` or ``None`` if no match is found. :rtype: ``BootEntry`` or ``NoneType``. """ global _host_profiles, _host_profiles_loaded if not host_profiles_loaded(): load_host_profiles() _log_debug( "Attempting to match profile for BootEntry(title='%s', " "version='%s') with machine_id='%s'" % (entry.title, entry.version, entry.machine_id) ) # Attempt to match by uname pattern for hp in _host_profiles: if hp.machine_id == entry.machine_id: _log_debug( "Matched BootEntry(version='%s', boot_id='%s') " "to HostProfile(name='%s', machine_id='%s')" % (entry.version, entry.disp_boot_id, hp.host_name, hp.machine_id) ) return hp return None
[docs] class HostProfile(BoomProfile): """Class HostProfile implements Boom host system profiles. Objects of type HostProfile define a host identiry, and optional fields or ``BootParams`` modifications to be applied to the specified host. Host profiles may modify any non-identity ``OsProfile`` key, either adding to or replacing the value defined by an embedded ``OsProfile`` instance. """ _profile_data = None _unwritten = False _comments = None _profile_keys = HOST_PROFILE_KEYS _required_keys = HOST_REQUIRED_KEYS _identity_key = BOOM_HOST_ID _osp = None
[docs] def _key_data(self, key): if key in self._profile_data: return self._profile_data[key] if key in self.osp._profile_data: return self.osp._profile_data[key] return None
[docs] def _have_key(self, key): """Test for presence of a Host or Os profile key.""" return key in self._profile_data or key in self.osp._profile_data
[docs] def __str__(self): """Format this HostProfile as a human readable string. Profile attributes are printed as "Name: value, " pairs, with like attributes grouped together onto lines. :returns: A human readable string representation of this HostProfile. :rtype: string """ # FIXME HostProfile breaks breaks = [ BOOM_HOST_ID, BOOM_HOST_NAME, BOOM_OS_ID, BOOM_ENTRY_MACHINE_ID, BOOM_HOST_LABEL, BOOM_OS_VERSION, BOOM_OS_UNAME_PATTERN, BOOM_HOST_ADD_OPTS, BOOM_HOST_DEL_OPTS, BOOM_OS_INITRAMFS_PATTERN, BOOM_OS_ROOT_OPTS_LVM2, BOOM_OS_ROOT_OPTS_BTRFS, BOOM_OS_OPTIONS, ] fields = [f for f in HOST_PROFILE_KEYS if self._have_key(f)] hp_str = "" tail = "" for f in fields: hp_str += '%s: "%s"' % (HOST_KEY_NAMES[f], self._key_data(f)) tail = ",\n" if f in breaks else ", " hp_str += tail hp_str = hp_str.rstrip(tail) return hp_str
[docs] def __repr__(self): """Format this HostProfile as a machine readable string. Return a machine-readable representation of this ``HostProfile`` object. The string is formatted as a call to the ``HostProfile`` constructor, with values passed as a dictionary to the ``profile_data`` keyword argument. :returns: a string representation of this ``HostProfile``. :rtype: string """ hp_str = "HostProfile(profile_data={" fields = [f for f in HOST_PROFILE_KEYS if self._have_key(f)] for f in fields: hp_str += '%s:"%s", ' % (f, self._key_data(f)) hp_str = hp_str.rstrip(", ") return hp_str + "})"
[docs] def __setitem__(self, key, value): """Set the specified ``HostProfile`` key to the given value. :param key: the ``HostProfile`` key to be set. :param value: the value to set for the specified key. """ # FIXME: duplicated from OsProfile.__setitem__ -> factor # osprofile.check_format_key_value(key, value) # and include isstr() key name validation etc. # Map hp key names to a list of format keys which must not # appear in that key's value: e.g. %{kernel} in the kernel # pattern profile key. bad_key_map = { BOOM_OS_KERNEL_PATTERN: [FMT_KERNEL], BOOM_OS_INITRAMFS_PATTERN: [FMT_INITRAMFS], BOOM_OS_ROOT_OPTS_LVM2: [FMT_ROOT_OPTS], BOOM_OS_ROOT_OPTS_BTRFS: [FMT_ROOT_OPTS], } def _check_format_key_value(key, value, bad_keys): for bad_key in bad_keys: if bad_key in value: raise ValueError( "HostProfile.%s cannot contain %s" % (key, key_from_key_name(bad_key)) ) if not isinstance(key, str): raise TypeError("HostProfile key must be a string.") if key not in HOST_PROFILE_KEYS: raise ValueError("Invalid HostProfile key: %s" % key) if key in bad_key_map: _check_format_key_value(key, value, bad_key_map[key]) self._profile_data[key] = value
[docs] def _generate_id(self): """Generate a new host identifier. Generate a new sha1 profile identifier for this profile, using the name, machine_id, and os_id and store it in _profile_data. :returns: None """ hashdata = self.machine_id + self.label digest = sha1(hashdata.encode("utf-8"), usedforsecurity=False).hexdigest() self._profile_data[BOOM_HOST_ID] = digest
def __set_os_profile(self): """Set this ``HostProfile``'s ``osp`` member to the corresponding profile for the set ``os_id``. """ os_id = self._profile_data[BOOM_OS_ID] osps = find_profiles(Selection(os_id=os_id)) if not osps: raise ValueError("OsProfile not found: %s" % os_id) if len(osps) > 1: raise ValueError("OsProfile identifier '%s' is ambiguous" % os_id) self.osp = osps[0]
[docs] def _append_profile(self): """Append a HostProfile to the global profile list""" global _host_profiles, _host_profiles_by_id, _host_profiles_by_host_id if _host_exists(self.host_id): raise ValueError("Profile already exists (host_id=%s)" % self.disp_host_id) _host_profiles.append(self) machine_id = self.machine_id if machine_id not in _host_profiles_by_id: _host_profiles_by_id[machine_id] = {} _host_profiles_by_id[machine_id][self.label] = self _host_profiles_by_host_id[self.host_id] = self
[docs] def _from_data(self, host_data, dirty=True): """Initialise a ``HostProfile`` from in-memory data. Initialise a new ``HostProfile`` object using the profile data in the `host_data` dictionary. This method should not be called directly: to build a new `Hostprofile`` object from in-memory data, use the class initialiser with the ``host_data`` argument. :returns: None """ err_str = "Invalid profile data (missing %s)" for key in HOST_REQUIRED_KEYS: if key == BOOM_HOST_ID: continue if key not in host_data: raise ValueError(err_str % key) self._profile_data = dict(host_data) if BOOM_HOST_ID not in self._profile_data: self._generate_id() self.__set_os_profile() if dirty: self._dirty() self._append_profile()
[docs] def __init__( self, 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="", del_opts="", options=None, profile_file=None, profile_data=None, ): """Initialise a new ``HostProfile`` object. If neither ``profile_file`` nor ``profile_data`` is given, all of ``machine_id``, ``name``, and ``os_id`` must be given. These values form the host profile identity and are used to generate the profile unique identifier. :param host_name: The hostname of this system :param os_id: An OS identifier specifying the ``OsProfile`` to use with this host profile. :param profile_data: An optional dictionary mapping from ``BOOM_*`` keys to profile values. :param profile_file: An optional path to a file from which profile data should be loaded. The file should be in Boom host profile format, with ``BOOM_*`` key=value pairs. :returns: A new ``HostProfile`` object. :rtype: class HostProfile """ global _host_profiles self._profile_data = {} # Initialise BoomProfile base class super(HostProfile, self).__init__( HOST_PROFILE_KEYS, HOST_REQUIRED_KEYS, BOOM_HOST_ID ) if profile_data and profile_file: raise ValueError( "Only one of 'profile_data' or 'profile_file' " "may be specified." ) if profile_data: self._from_data(profile_data) return if profile_file: self._from_file(profile_file) return self._dirty() required_args = [machine_id, host_name, os_id] if any([not val for val in required_args]): raise ValueError( "Invalid host profile arguments: machine_id, " "host_name, and os_id are mandatory." ) osps = find_profiles(Selection(os_id=os_id)) if not osps: raise ValueError("No matching profile found for os_id=%s" % os_id) if len(osps) > 1: raise ValueError("OsProfile ID is ambiguous: %s" % os_id) os_id = osps[0].os_id self._profile_data[BOOM_ENTRY_MACHINE_ID] = machine_id self._profile_data[BOOM_HOST_NAME] = host_name self._profile_data[BOOM_OS_ID] = os_id self._profile_data[BOOM_HOST_LABEL] = label # Only set keys that have a value in the host profile data dict if kernel_pattern: self._profile_data[BOOM_OS_KERNEL_PATTERN] = kernel_pattern if initramfs_pattern: self._profile_data[BOOM_OS_INITRAMFS_PATTERN] = initramfs_pattern if root_opts_lvm2: self._profile_data[BOOM_OS_ROOT_OPTS_LVM2] = root_opts_lvm2 if root_opts_btrfs: self._profile_data[BOOM_OS_ROOT_OPTS_BTRFS] = root_opts_btrfs if add_opts: self._profile_data[BOOM_HOST_ADD_OPTS] = add_opts if del_opts: self._profile_data[BOOM_HOST_DEL_OPTS] = del_opts if options: self._profile_data[BOOM_OS_OPTIONS] = options self.__set_os_profile() self._generate_id() _host_profiles.append(self)
# We use properties for the HostProfile attributes: this is to # allow the values to be stored in a dictionary. Although # properties are quite verbose this reduces the code volume # and complexity needed to marshal and unmarshal the various # file formats used, as well as conversion to and from string # representations of HostProfile objects. # Keys obtained from os-release data form the profile's identity: # the corresponding attributes are read-only. # HostProfile properties: # # Profile identity properties (ro): # host_id # disp_os_id # # Profile identity properties (rw): # host_name # machine_id # os_id # # Properties mapped to OsProfile (ro) # os_name # os_short_name # os_version # os_version_id # uname_pattern # # Properties overridden or mapped to OsProfile (rw) # kernel_pattern # initramfs_pattern # root_opts_lvm2 # root_opts_btrfs # options # # HostProfile specific properties (rw) # label # add_opts # del_opts @property def disp_os_id(self): """The display os_id of this profile. Return the shortest prefix of this OsProfile's os_id that is unique within the current set of loaded profiles. :getter: return this OsProfile's os_id. :type: str """ return self.osp.disp_os_id @property def host_id(self): if BOOM_HOST_ID not in self._profile_data: self._generate_id() return self._profile_data[BOOM_HOST_ID] @property def disp_host_id(self): """The display host_id of this profile Return the shortest prefix of this HostProfile's os_id that is unique within the current set of loaded profiles. :getter: return this HostProfile's display host_id. :type: str """ return self.host_id[: min_host_id_width()] @property def disp_machine_id(self): """The machine_id of this host profile. Return the shortest prefix of this HostProfile's os_id that is unique within the current set of loaded profiles. :getter: return this HostProfile's display host_id. :type: str """ return self.machine_id[: min_machine_id_width()] @property def machine_id(self): """The machine_id of this host profile. Return the shortest prefix of this HostProfile's os_id that is unique within the current set of loaded profiles. :getter: return this ``HostProfile``'s display host_id. :setter: change this ``HostProfile``'s ``machine_id``. This will change the ``host_id``. :type: str """ return self._profile_data[BOOM_ENTRY_MACHINE_ID] @machine_id.setter def machine_id(self, value): if value == self._profile_data[BOOM_ENTRY_MACHINE_ID]: return self._profile_data[BOOM_ENTRY_MACHINE_ID] = value self._dirty() self._generate_id() @property def os_id(self): """The ``os_id`` of this profile. :getter: returns the ``os_id`` as a string. :type: string """ return self.osp.os_id @os_id.setter def os_id(self, value): if value == self._profile_data[BOOM_OS_ID]: return self._profile_data[BOOM_OS_ID] = value self.__set_os_profile() self._dirty() self._generate_id() @property def osp(self): """The ``OsProfile`` used by this ``HostProfile``. :getter: returns the ``OsProfile`` object used by this ``HostProfile``. :setter: stores a new ``OsProfile`` for use by this ``HostProfile`` and updates the stored ``os_id`` value in the host profile. """ return self._osp @osp.setter def osp(self, osp): if self._osp and osp.os_id == self._osp.os_id: return self._osp = osp self._profile_data[BOOM_OS_ID] = osp.os_id self._dirty() self._generate_id() @property def host_name(self): """The ``host_name`` of this profile. Normally set to the hostname of the system corresponding to this ``HostProfile``. :getter: returns the ``host_name`` as a string. :type: string """ return self._profile_data[BOOM_HOST_NAME] @host_name.setter def host_name(self, value): if value == self._profile_data[BOOM_HOST_NAME]: return self._profile_data[BOOM_HOST_NAME] = value self._dirty() self._generate_id() @property def short_name(self): """The ``short_name`` of this profile. If ``HostProfile.host_name`` appears to contain a DNS-style name, return only the host portion. :getter: returns the ``short_name`` as a string. :type: string """ host_name = self._profile_data[BOOM_HOST_NAME] return host_name.split(".")[0] if "." in host_name else host_name # # Properties mapped to OsProfile # @property def os_name(self): """The ``os_name`` of this profile. :getter: returns the ``os_name`` as a string. :type: string """ return self.osp.os_name @property def os_short_name(self): """The ``os_short_name`` of this profile. :getter: returns the ``os_short_name`` as a string. :type: string """ return self.osp.os_short_name @property def os_version(self): """The ``os_version`` of this profile. :getter: returns the ``os_version`` as a string. :type: string """ return self.osp.os_version @property def os_version_id(self): """The ``os_version_id`` of this profile. :getter: returns the ``os_version_id`` as a string. :type: string """ return self.osp.os_version_id @property def uname_pattern(self): """The current ``uname_pattern`` setting of this profile. :getter: returns the ``uname_pattern`` as a string. :setter: stores a new ``uname_pattern`` setting. :type: string """ if BOOM_OS_UNAME_PATTERN in self._profile_data: return self._profile_data[BOOM_OS_UNAME_PATTERN] return self.osp.uname_pattern # # Properties overridden or mapped to OsProfile # @property def kernel_pattern(self): """The current ``kernel_pattern`` setting of this profile. :getter: returns the ``kernel_pattern`` as a string. :setter: stores a new ``kernel_pattern`` setting. :type: string """ if BOOM_OS_KERNEL_PATTERN in self._profile_data: return self._profile_data[BOOM_OS_KERNEL_PATTERN] return self.osp.kernel_pattern @kernel_pattern.setter def kernel_pattern(self, value): kernel_key = key_from_key_name(FMT_KERNEL) if kernel_key in value: raise ValueError("HostProfile.kernel cannot contain %s" % kernel_key) self._profile_data[BOOM_OS_KERNEL_PATTERN] = value self._dirty() @property def initramfs_pattern(self): """The current ``initramfs_pattern`` setting of this profile. :getter: returns the ``initramfs_pattern`` as a string. :setter: store a new ``initramfs_pattern`` setting. :type: string """ if BOOM_OS_INITRAMFS_PATTERN in self._profile_data: return self._profile_data[BOOM_OS_INITRAMFS_PATTERN] return self.osp.initramfs_pattern @initramfs_pattern.setter def initramfs_pattern(self, value): initramfs_key = key_from_key_name(FMT_INITRAMFS) if initramfs_key in value: raise ValueError("HostProfile.initramfs cannot contain %s" % initramfs_key) self._profile_data[BOOM_OS_INITRAMFS_PATTERN] = value self._dirty() @property def root_opts_lvm2(self): """The current LVM2 root options setting of this profile. :getter: returns the ``root_opts_lvm2`` value as a string. :setter: store a new ``root_opts_lvm2`` value. :type: string """ if BOOM_OS_ROOT_OPTS_LVM2 in self._profile_data: return self._profile_data[BOOM_OS_ROOT_OPTS_LVM2] return self.osp.root_opts_lvm2 @root_opts_lvm2.setter def root_opts_lvm2(self, value): root_opts_key = key_from_key_name(FMT_ROOT_OPTS) if root_opts_key in value: raise ValueError( "HostProfile.root_opts_lvm2 cannot contain " "%s" % root_opts_key ) self._profile_data[BOOM_OS_ROOT_OPTS_LVM2] = value self._dirty() @property def root_opts_btrfs(self): """The current BTRFS root options setting of this profile. :getter: returns the ``root_opts_btrfs`` value as a string. :setter: store a new ``root_opts_btrfs`` value. :type: string """ if BOOM_OS_ROOT_OPTS_BTRFS in self._profile_data: return self._profile_data[BOOM_OS_ROOT_OPTS_BTRFS] return self.osp.root_opts_btrfs @root_opts_btrfs.setter def root_opts_btrfs(self, value): root_opts_key = key_from_key_name(FMT_ROOT_OPTS) if root_opts_key in value: raise ValueError( "HostProfile.root_opts_btrfs cannot contain %s" % root_opts_key ) self._profile_data[BOOM_OS_ROOT_OPTS_BTRFS] = value self._dirty() @property def options(self): """The current kernel command line options setting for this profile. :getter: returns the ``options`` value as a string. :setter: store a new ``options`` value. :type: string """ if BOOM_OS_OPTIONS in self._profile_data: return self._profile_data[BOOM_OS_OPTIONS] return self.osp.options @options.setter def options(self, value): self._profile_data[BOOM_OS_OPTIONS] = value self._dirty() @property def title(self): """The current title template for this profile. :getter: returns the ``title`` value as a string. :setter: store a new ``title`` value. :type: string """ if BOOM_OS_TITLE not in self._profile_data: return None return self._profile_data[BOOM_OS_TITLE] @title.setter def title(self, value): if not value: # It is valid to set an empty title in a HostProfile as long # as the OsProfile defines one. if not self.osp or not self.osp.title: raise ValueError("Entry title cannot be empty") self._profile_data[BOOM_OS_TITLE] = value self._dirty() @property def optional_keys(self): if not self.osp or not self.osp.optional_keys: return "" return self.osp.optional_keys # # HostProfile specific properties # @property def add_opts(self): if BOOM_HOST_ADD_OPTS in self._profile_data: return self._profile_data[BOOM_HOST_ADD_OPTS] return "" @add_opts.setter def add_opts(self, opts): self._profile_data[BOOM_HOST_ADD_OPTS] = opts self._dirty() @property def del_opts(self): if BOOM_HOST_DEL_OPTS in self._profile_data: return self._profile_data[BOOM_HOST_DEL_OPTS] return "" @del_opts.setter def del_opts(self, opts): self._profile_data[BOOM_HOST_DEL_OPTS] = opts self._dirty() @property def label(self): if BOOM_HOST_LABEL in self._profile_data: return self._profile_data[BOOM_HOST_LABEL] return "" @label.setter def label(self, value): valid_chars = string.ascii_letters + string.digits + "_- " if BOOM_HOST_LABEL in self._profile_data: if self._profile_data[BOOM_HOST_LABEL] == value: return for c in value: if c not in valid_chars: raise ValueError("Invalid host label character: '%s'" % c) self._profile_data[BOOM_HOST_LABEL] = value self._dirty() self._generate_id()
[docs] def _profile_path(self): """Return the path to this profile's on-disk data. Return the full path to this HostProfile in the Boom profiles directory (or the location to which it will be written, if it has not yet been written). :rtype: str :returns: The absolute path for this HostProfile's file """ if self.label: label = self.label if " " in label: label = label.replace(" ", "_") names = (self.short_name, label) name_fmt = "%s-%s" else: names = self.short_name name_fmt = "%s" profile_name = name_fmt % names profile_id = (self.host_id, profile_name) profile_path_name = BOOM_HOST_PROFILE_FORMAT % profile_id return path_join(boom_host_profiles_path(), profile_path_name)
[docs] def write_profile(self, force=False): """Write out profile data to disk. Write out this ``HostProfile``'s data to a file in Boom format to the paths specified by the current configuration. Currently the ``machine_id`` and ``name`` keys are used to construct the file name. If the value of ``force`` is ``False`` and the ``HostProfile`` is not currently marked as dirty (either new, or modified since the last load operation) the write will be skipped. :param force: Force this profile to be written to disk even if the entry is unmodified. :raises: ``OsError`` if the temporary entry file cannot be renamed, or if setting file permissions on the new entry file fails. """ path = boom_host_profiles_path() mode = BOOM_HOST_PROFILE_MODE self._write_profile(self.host_id, path, mode, force=force)
[docs] def delete_profile(self): """Delete on-disk data for this profile. Remove the on-disk profile corresponding to this ``HostProfile`` object. This will permanently erase the current file (although the current data may be re-written at any time by calling ``write_profile()`` before the object is disposed of). :rtype: ``NoneType`` :raises: ``OsError`` if an error occurs removing the file or ``ValueError`` if the profile does not exist. """ global _host_profiles, _host_profiles_by_id, _host_profiles_by_host_id self._delete_profile(self.host_id) machine_id = self.machine_id host_id = self.host_id if _host_profiles and self in _host_profiles: _host_profiles.remove(self) if _host_profiles_by_id and machine_id in _host_profiles_by_id: _host_profiles_by_id.pop(machine_id) if _host_profiles_by_host_id and host_id in _host_profiles_by_host_id: _host_profiles_by_host_id.pop(host_id)
__all__ = [ # Host profiles "HostProfile", "drop_host_profiles", "load_host_profiles", "write_host_profiles", "host_profiles_loaded", "find_host_profiles", "select_host_profile", "get_host_profile_by_id", "match_host_profile", "select_host_profile", # Host profile keys "BOOM_HOST_ID", "BOOM_HOST_NAME", "BOOM_HOST_ADD_OPTS", "BOOM_HOST_DEL_OPTS", "BOOM_HOST_LABEL", "HOST_PROFILE_KEYS", "HOST_REQUIRED_KEYS", "HOST_OPTIONAL_KEYS", # Path configuration "boom_host_profiles_path", ] # vim: set et ts=4 sw=4 :