#! /usr/bin/python2.6
#
# CDDL HEADER START
#
# The contents of this file are subject to the terms of the
# Common Development and Distribution License (the "License").
# You may not use this file except in compliance with the License.
#
# You can obtain a copy of the license at usr/src/OPENSOLARIS.LICENSE
# or http://www.opensolaris.org/os/licensing.
# See the License for the specific language governing permissions
# and limitations under the License.
#
# When distributing Covered Code, include this CDDL HEADER in each
# file and include the License file at usr/src/OPENSOLARIS.LICENSE.
# If applicable, add the following below this CDDL HEADER, with the
# fields enclosed by brackets "[]" replaced with your own identifying
# information: Portions Copyright [yyyy] [name of copyright owner]
#
# CDDL HEADER END
#
# Copyright (c) 2011, 2012, Oracle and/or its affiliates. All rights reserved.
#
"""
Abstractions for boot configuration management. A BootConfig object aggregates
the boot configuration for a system. Implementations are provided for active
systems (systems that boot from disk devices) as well as for installation image
creation (i.e. for network-based installation images, optical disc installation
images, and USB-based installation images).
BootConfig provides access, through abstractions, to the system boot loader,
general system boot configuration variables, and the set of bootable instances
that the boot loader provides a means to boot.
"""
import collections
import re
import unittest
from bootmgmt import (BootmgmtNotSupportedError, BootmgmtArgumentError,
BootmgmtMissingInfoError, BootmgmtError,
BootmgmtUnsupportedOperationError,
BootmgmtUnsupportedPlatformError,
BootmgmtConfigReadError)
from bootmgmt import bootinfo, bootloader
from bootmgmt.bootutil import (LoggerMixin, get_current_arch_string,
bootfs_split, parse_bool,
get_zpool_default_bootfs, scrub_kargs)
from bootmgmt.pysol import (libzfs_init, libzfs_fini, zpool_open, zpool_close,
zpool_get_physpath, zpool_set_prop, zpool_get_prop,
ZPOOL_PROP_BOOTFS, libzfs_error_description)
class BootConfig(LoggerMixin):
"""Abstract base class for boot configuration classes"""
# Valid flags to pass to __init__:
(BCF_CREATE,
BCF_ONESHOT,
BCF_AUTOGEN,
BCF_MIGRATE,
BCF_NO_LOAD_BOOT_INSTANCES,
BCF_LOAD_SILENT_FAILURE) = range(6)
BOOT_CLASS_DISK = 'disk'
BOOT_CLASS_NET = 'net'
BOOT_CLASS_ODD = 'odd'
TOKEN_SYSTEMROOT = 'systemroot'
OUTPUT_TYPE_FILE = 'file'
OUTPUT_TYPE_NETCONFIG = 'file-netconfig'
OUTPUT_TYPE_BIOS_ELTORITO = 'bios-eltorito-image'
OUTPUT_TYPE_UEFI_ELTORITO = 'uefi-eltorito-image'
OUTPUT_TYPE_BIOS_NBP = 'nbp-platform-0x0000'
OUTPUT_TYPE_UEFI64_NBP = 'nbp-platform-0x0007'
# Tuple indices for each tuple in the list output by the install() method
(IDX_FILETYPE,
IDX_TEMPNAME,
IDX_INSTANCE,
IDX_DESTNAME,
IDX_USER,
IDX_GROUP,
IDX_MODE) = range(7)
@property
def dirty(self):
return self._dirty
@dirty.setter
def dirty(self, val):
if self._dirty != val:
self._debug('dirty => %s', val)
self._dirty = val
def __init__(self, flags, **kwargs):
"""
Initialize the basic set of attributes for all BootConfig classes
-----------------------------------------------------------------
Argument| Valid Value(s)
--------+--------------------------------------------------------
flags | <Tuple of flags that modify the behavior of this
| BootConfig object> [tuple]
|
| Allowed values in the tuple:
| ----------------------------
| BootConfig.BCF_CREATE: Create a new boot
| configuration. If one already
| exists, it will be discarded.
| If this flag is not present and
| no boot configuration exists,
| an exception
| (BootmgmtConfigReadError)
| will be raised.
| BootConfig.BCF_ONESHOT:If True, only the final boot
| configuration files will be
| created when the
| commit_boot_config() is called.
| This is useful for one-shot
| creation of boot configurations
| for installation media.
| BootConfig.BCF_AUTOGEN:The set of boot instances will be
| automatically generated by
| scanning the system. The list of
| boot instances found by the scan
| will be available after
| the BootConfig object is created.
| This set of boot instances can
| then be tailored before calling
| commit_boot_config(). Note that
| the set of BootInstance objects
| added to this object's boot_instances
| are only guaranteed to include
| Solaris boot instances.
| BootConfig.BCF_MIGRATE:If supported, the existing boot
| configuration will be migrated
| from an older form to a newer
| form (i.e. conversion from legacy
| GRUB's menu.lst to GRUB2's
| configuration format). The conversion
| is performed at commit_boot_config()
| time. Cannot be used with the
| BCF_CREATE flag.
| BootConfig.BCF_NO_LOAD_BOOT_INSTANCES: Omits boot instances
| from the set of configuration data
| loaded.
| BootConfig.BCF_LOAD_SILENT_FAILURE: Does not raise an
| exception when loading configuration
| data fails. Using this allows a
| partial set of configuration data to
| be read.
|
platform| [optional] The target system architecture / firmware tuple for
(keyword| which this boot configuration will be written. Useful
arg) | for the creation of boot configuration files for network
| boot instances, or when a boot configuration is created
| on a system of a different architecture / firmware than
| the target. If this is not supplied, the assumption is
| that the boot configuration will be for the current
| system. Supported tuple values are:
| { ('sparc', 'obp'),
| ('x86', 'bios'),
| ('x86', 'uefi64') }
| A firmware value of None means that the boot
| configuration will be written for all supported
| firmware types for the specified architecture. Note
| that not all BootConfig subclasses support this
| wildcard platform firmware specification.
| If a particular BootConfig subclass does not support
| manipulation of a different platform's boot
| configuration, a BootmgmtNotSupportedError will be
| raised.
loader\ |
class | The class corresponding to the BootLoader that the
| BootConfig's creator requests wants to be this
| BootConfig's BootLoader. If this BootLoader cannot be
| used, a BootmgmtInvalidBootloaderError is raised.
-----------------------------------------------------------------
"""
self.boot_instances = []
self.boot_class = kwargs.get('boot_class')
self.boot_fstype = kwargs.get('boot_fstype')
self._platform = kwargs.get('platform')
if (self._platform is not None and
(not isinstance(self._platform, (tuple, list)) or
len(self._platform) != 2)):
raise BootmgmtArgumentError('Invalid platform value specified. '
'Expected 2-tuple or 2-item list')
self._dirty = False
self._flags = flags
self.boot_loader = self._get_boot_loader(**kwargs)
if (BootConfig.BCF_CREATE in self._flags and
BootConfig.BCF_MIGRATE in self._flags):
raise BootmgmtUnsupportedOperationError(
'Migration cannot be combined with creation')
if BootConfig.BCF_CREATE in self._flags:
self._new_boot_config(**kwargs)
elif (self.boot_loader and
(bootloader.BootLoader.ARTIFACT_BOOTLOADER_CONFIGFILES not in
self.boot_loader.artifacts)):
self._debug('CONFIGFILE artifact not present. Will not attempt '
'to load boot loader configuration. Creating new boot '
'configuration instead. artifacts were %s.',
str(self.boot_loader.artifacts))
self._new_boot_config(**kwargs)
else:
try:
# _load_boot_config raises BootmgmtConfigReadError
self._load_boot_config(**kwargs)
except BootmgmtConfigReadError:
if BootConfig.BCF_LOAD_SILENT_FAILURE not in self._flags:
raise
if BootConfig.BCF_NO_LOAD_BOOT_INSTANCES in self._flags:
self.delete_boot_instance(lambda x: True)
else:
self.dirty = False
if BootConfig.BCF_AUTOGEN in self._flags:
self._autogenerate_config(**kwargs)
def _autogenerate_config(self, **kwargs):
from bootmgmt.backend.autogen import BootInstanceAutogenFactory
inst_list = BootInstanceAutogenFactory.autogen(self)
if len(inst_list) > 0:
self.boot_instances += inst_list
self.dirty = True
def _load_boot_config(self, **kwargs):
"""[Implemented by child classes] Loads the boot configuration (with
guidance from kwargs)."""
pass
def _new_boot_config(self, **kwargs):
"""[Implemented by child classes] Initializes this instance with
a new boot configuration (with guidance from kwargs)."""
pass
def _get_boot_loader(self, **kwargs):
"""Initializes this instance's boot_loader"""
loader = bootloader.BootLoader.get(bootconfig=self, **kwargs)
self._debug('loader = %s', loader)
return loader
@property
def platform_requested(self):
"""Returns the requested platform string for this BootConfig
"""
if self._platform:
return self._platform[0]
@property
def firmware_requested(self):
"""Returns the requested firmware string for this BootConfig
"""
if self._platform:
return self._platform[1]
def get_root(self):
"Return the root directory where this BootConfig is be stored"
return None
def add_boot_instance(self, boot_instances, where=-1):
"""
Adds one or more new boot instances to this boot configuration.
boot_instance is a list of BootInstance references or a single
BootInstance reference.
The where argument determines where in the ordered list of boot
instances this boot instance entry will be placed. The default
is -1, which means the end of the list of boot instances. If
where is a callable object, it will be used to iterate through
the boot instances list. If it returns a negative value, the
boot_instance will be inserted BEFORE that item; If it returns
a positive value, the boot_instance will be inserted AFTER that
item; If it returns 0, no insertion will occur at that point
in the iteration. The arguments to the function are
(<current_boot_instance_in_the_iteration>, boot_instance).
"""
if isinstance(boot_instances, collections.Iterable):
for instance in boot_instances:
self._add_one_boot_instance(instance, where)
# If where is not -1, make sure we increment it so that
# subsequent boot instances are placed in the boot_instances
# array in the proper order
if where > -1:
where = where + 1
else:
self._add_one_boot_instance(boot_instances, where)
def _add_one_boot_instance(self, boot_instance, where=-1):
prevDefault = None
if boot_instance.default is True:
# Find previous default (if any)
defaults = [x for x in self.boot_instances
if x.default is True]
if len(defaults) > 0:
prevDefault = defaults[0]
self._debug("Previous default was:\n%s", prevDefault)
if type(where) is int:
# where is an index if it's > 0
where = where if where >= 0 else len(self.boot_instances)
self.boot_instances[where:where] = [boot_instance]
boot_instance._bootconfig = self
self.dirty = True
elif callable(where):
for index, inst in enumerate(self.boot_instances):
whereval = where(inst, boot_instance)
if whereval < 0:
self.boot_instances[index:index] = [boot_instance]
boot_instance._bootconfig = self
self.dirty = True
break
elif whereval > 0:
self.boot_instances[index + 1: index + 1] = [boot_instance]
boot_instance._bootconfig = self
self.dirty = True
break
else:
raise BootmgmtArgumentError('The where parameter is malformed')
if not prevDefault is None:
prevDefault.default = False
def delete_boot_instance(self, filter_func, all=True):
"""
Deletes one or more boot instances from the boot configuration.
filter_func is a function that takes a single argument (a
BootInstance). If it returns True, that BootInstance is removed
from the boot configuration.
The 'all' parameter is True if all matching boot instances are
to be deleted. If False, only the first instance for which
filter_func returns True is deleted.
For example:
delete_boot_instance(lambda x: x.title == 'snv_158')
will delete all boot instances whose titles match 'snv_158'.
"""
if all is True:
oldlen = len(self.boot_instances)
self.boot_instances = filter(lambda x: not filter_func(x),
self.boot_instances)
newlen = len(self.boot_instances)
if oldlen != newlen:
self.dirty = True
return (oldlen - newlen)
else:
for idx, inst in enumerate(self.boot_instances):
if filter_func(inst) is True:
del self.boot_instances[idx]
self.dirty = True
return 1
return 0
def modify_boot_instance(self, filter_func, mod_func):
"""
Applies mod_func to all boot instances for which filter_func
returns True.
This is shorthand for:
for bi in filter(filter_func, bootconfig_obj.boot_instances):
mod_func(bi)
(The (single) argument to filter_func and mod_func is a
BootInstance ref).
"""
for inst in filter(filter_func, self.boot_instances):
mod_func(inst)
def commit_boot_config(self, temp_dir=None, boot_devices=None,
force=False):
"""Writes the boot configuration (including boot instances and boot
loader settings) to stable storage.
If this object was created with the BootConfig.BC_ONESHOT flag,
then only the final boot configuration file(s) will be created
(i.e. the Legacy GRUB menu.lst, GRUB2 configuration file, or
SPARC menu.lst)-- any other state files that store boot
configuration information will not be written (customizations to
this boot configuration may not persist). This may prevent
incremental modifications to the boot configuration from
being possible (depending on the boot loader in use). BC_ONESHOT
should only be used when creating a boot configuration that is
not intended to change (i.e. when creating a boot configuration
for use with an install image, not a running system).
boot_devices is a list of strings, each of which is the path
to a character-special device where the boot loader should be
installed. This argument should be omitted when the consumer
desires to write the boot configuration files to a temporary
directory.
If temp_dir is not None, then the set of files that constitute
the boot configuration is written to a temporary directory (and
not to the official location(s)). (temp_dir must exist or an
IOError will be raised). When files are written to temp_dir,
commit_boot_config() returns a list of 7-tuples of the following
form:
(<type>, <srcpath>, <object>, <destpath>, <uid>, <gid>, <mode>)
Each of these tuples describes a boot configuration file that
was written, along with the required system-relative path where
it must be copied. This enables a consumer to install the
file(s) into the correct place(s) on a system or install image
without having to hard-code knowledge of the filenames)).
For example:
[('file', '/tmp/bc.Jzx1cZa/boot/solaris/bootenv.rc',
<bootmgmt.bootconfig.SolarisDiskBootInstance object at ...>,
'%(systemroot)s/boot/solaris/bootenv.rc', 'root', 'sys', 0644)]
The <type> value in the tuple identifies the type of file in
the tuple. This is useful for conveying platform-specific
attributes of a particular file. For example, <type> could be
'eltorito' to identify an eltorito boot image, which a consumer
would recognize and then use to supply the argument to
`mkisofs -b`. See child class definitions for additional <type>
values. Only <type>='file' is defined at the BootConfig level.
The <object> element, if not None, is a reference to a
BootInstance object. It provides clarification for tokens that
are embedded in the destination path. In the above example, the
`systemroot' token refers to the root path of the Solaris BE
identified by the SolarisDiskBootInstance object in the tuple.
In this example, including the object allows the consumer to
resolve the root path location.
Note the use of Python string-formatting tokens, for which a
consumer must supply definitions. The following list of tokens
are defined at the BootConfig level; child classes may define
additional tokens:
----------------------------------------------------------------
Token Name | Meaning
------------------+---------------------------------------------
systemroot | The path to a boot instance's mounted root
| filesystem. [string]
----------------------------------------------------------------
"""
if not self.boot_loader:
raise BootmgmtError('A boot loader could not be established '
'for this boot configuration (this is usually '
'due to missing boot loader files). '
'The system cannot be guaranteed to be '
'bootable.')
if temp_dir is None:
if force or self.dirty is True:
for inst in self.boot_instances:
if (not inst.boot_vars is None and
(force or inst.boot_vars.dirty is True)):
inst.boot_vars.write(inst=inst)
if force or self.dirty is True or self.boot_loader.dirty is True:
self.boot_loader.install(boot_devices, force)
return None
else:
tuple_list = []
# If a temp dir is specified, ignore the dirty flag and just write
# all the config to the temporary directory
for inst in self.boot_instances:
if not inst.boot_vars is None:
bv_tuples = inst.boot_vars.write(inst, temp_dir)
if bv_tuples:
tuple_list.extend(bv_tuples)
if not self.boot_loader is None:
bl_tuples = self.boot_loader.install(temp_dir)
if bl_tuples:
tuple_list.extend(bl_tuples)
return tuple_list
def __str__(self):
s = 'State: ' + ('dirty' if self.dirty else 'clean') + '\n'
s += 'Class: ' + (self.boot_class
if not self.boot_class is None else 'none') + '\n'
s += 'FSType: ' + ((self.boot_fstype
if not self.boot_fstype is None else 'unknown') +
'\n')
s += 'Boot instances: ' + str(len(self.boot_instances)) + '\n'
for idx, inst in enumerate(self.boot_instances):
s += '===[ Instance ' + str(idx) + ' ]===\n'
s += '\t' + str(inst).replace('\n', '\n\t') + '\n'
s += '===[ End Boot Instances ]===\n'
if not self.boot_loader is None:
s += 'Boot loader:\n'
s += str(self.boot_loader) + '\n'
else:
s += 'No boot loader'
return s
class DiskBootConfig(BootConfig):
"""A class for managing the boot configuration stored on hard disk-like
storage devices. Handles Solaris boot configurations stored in ZFS root
pools and on devices with a UFS root filesystem"""
# Key names for keyword arguments to the constructor
ARG_ZFS_RPNAME = 'rpname' # ZFS root pool name
ARG_ZFS_TLDPATH = 'tldpath' # ZFS top-level dataset mounted path
ARG_ZFS_SYSROOT_PATH = 'zfspath' # ZFS system root mounted path
ARG_ZFS_SET_BOOTFS_ON_COMMIT = 'set_bootfs_on_commit'
ARG_UFS_ROOT = 'ufsroot' # UFS root mounted path
# Tokens returned from commit_boot_config()
TOKEN_ZFS_RPOOL_TOP_DATASET = 'rpool_top_dataset'
def __init__(self, flags, **kwargs):
# DiskBootConfig does not support platforms other than native
platform = kwargs.get('platform', None)
if not platform is None:
raise BootmgmtNotSupportedError(self.__class__.__name__ + ' does '
'not support cross-platform operations')
fstype = None
if (DiskBootConfig.ARG_ZFS_RPNAME in kwargs and
DiskBootConfig.ARG_ZFS_TLDPATH in kwargs):
fstype = 'zfs'
self.sysroot = kwargs.get(DiskBootConfig.ARG_ZFS_SYSROOT_PATH, '/')
self.zfstop = kwargs[DiskBootConfig.ARG_ZFS_TLDPATH]
self.zfsrp = kwargs[DiskBootConfig.ARG_ZFS_RPNAME]
self.set_bootfs = kwargs.get(
DiskBootConfig.ARG_ZFS_SET_BOOTFS_ON_COMMIT,
False)
elif DiskBootConfig.ARG_UFS_ROOT in kwargs:
fstype = 'ufs'
self.sysroot = kwargs[DiskBootConfig.ARG_UFS_ROOT]
if fstype is None:
raise BootmgmtNotSupportedError('The filesystem type supplied '
'to the DiskBootConfig constructor was not recognized')
super(self.__class__, self).__init__(flags,
boot_class=BootConfig.BOOT_CLASS_DISK,
boot_fstype=fstype, **kwargs)
def get_root(self):
return self.sysroot
def _load_boot_config(self, **kwargs):
"""Loads the boot configuration"""
if self.boot_loader:
self.boot_loader.load_config()
fstype = getattr(self, 'boot_fstype', None)
if (fstype is None or fstype != 'zfs' or
getattr(self, 'zfsrp', None) is None):
self._debug('(skipping bootfs check)')
return
# If, after loading the boot loader's config, we do not have a
# default bootfs, check the root pool's bootfs property and use
# it to find a boot instance. In the event there are multiple
# boot instances with the same bootfs, choose the first one.
default_instances = [inst for inst in self.boot_instances
if inst.default is True]
if len(default_instances) > 0:
self._debug('This BootConfig already has a default boot instance')
return
# Get the default bootfs for this root pool:
lzfsh = libzfs_init()
pool_default_bootfs = get_zpool_default_bootfs(lzfsh, self.zfsrp)
libzfs_fini(lzfsh)
# If there is no default or we couldn't get it, bail
if pool_default_bootfs is None:
return
default_instances = [inst for inst in self.boot_instances
if getattr(inst, 'bootfs', None) ==
pool_default_bootfs]
if len(default_instances) == 0:
default_instances = [inst for inst in self.boot_instances
if getattr(inst, 'rpool', None) == self.zfsrp]
# If there is at least one boot instance with a bootfs that matches,
# set the first as default
if len(default_instances) > 0:
default_instances[0].default = True
def _new_boot_config(self, **kwargs):
"""Initializes this instance with a new boot configuration"""
if self.boot_loader:
self.boot_loader.new_config()
def _set_default_bootfs(self):
if len(self.boot_instances) == 0:
return # Nothing to do
# Look for the default boot instance. If there is none, use the first
# one.
default_inst = None
for inst in self.boot_instances:
if inst.default is True:
default_inst = inst
break
if default_inst is None:
self._debug('No default boot instance found-- using first one')
default_inst = self.boot_instances[0]
default_bootfs = getattr(default_inst, 'bootfs', None)
if default_bootfs is None:
self._debug('Could not find a boot instance with a bootfs '
'attribute; Not setting root pool bootfs property')
return # Nothing to do!
# Use libzfs to set the bootfs property
lzfsh = libzfs_init()
rootpool = default_bootfs.split('/')[0]
zph = zpool_open(lzfsh, rootpool)
pool_default_bootfs = zpool_get_prop(lzfsh, zph, ZPOOL_PROP_BOOTFS)
if pool_default_bootfs != default_bootfs:
setprop_rv = zpool_set_prop(zph, 'bootfs', default_bootfs)
if setprop_rv == 0:
self._debug('pool %s bootfs was %s now %s', (rootpool,
pool_default_bootfs, default_bootfs))
else:
setprop_rv = 0
self._debug('pool %s bootfs is already set to %s',
(rootpool, default_bootfs))
zpool_close(zph)
libzfs_fini(lzfsh)
if setprop_rv != 0:
raise BootmgmtError(libzfs_error_description(lzfsh))
def commit_boot_config(self, temp_dir=None, boot_devices=None,
force=False):
tuples = super(DiskBootConfig, self).commit_boot_config(temp_dir,
boot_devices,
force)
if temp_dir is None and self.set_bootfs is True:
self._set_default_bootfs()
return tuples
class ODDBootConfig(BootConfig):
"""A class for managing the boot configuration stored on optical disk
storage devices. Handles Solaris boot configurations stored on DVD
media"""
TOKEN_ODD_ROOT = 'oddimage_root'
def __init__(self, flags, **kwargs):
""
# Weed out unsupported flags:
if BootConfig.BCF_MIGRATE in flags:
raise BootmgmtUnsupportedOperationError(self.__class__.__name__ +
': Migration is not \
supported')
# Save the image's root directory:
self.odd_image_root = kwargs.get('oddimage_root', None)
if self.odd_image_root is None:
raise BootmgmtArgumentError('Missing oddimage_root argument')
super(self.__class__, self).__init__(flags,
boot_class=BootConfig.BOOT_CLASS_ODD,
**kwargs)
def get_root(self):
return self.odd_image_root
class NetBootConfig(BootConfig):
"""A class for managing the boot configuration stored on a network boot
server.
"""
# NetBootConfigs must not use ProxyBootLoader -- it should only be
# associated with a single boot loader class. There is no 'upgrade'
# issue that would require ProxyBootConfig
EXCLUSIVE_LOADER = True
TOKEN_TFTP_ROOT = 'tftproot'
def __init__(self, flags, **kwargs):
""
# Weed out unsupported flags:
if BootConfig.BCF_MIGRATE in flags:
raise BootmgmtUnsupportedOperationError(self.__class__.__name__ +
': Migration is not \
supported')
# Save the image's root directory:
self.net_osimage_root = kwargs.get('osimage_root', None)
if self.net_osimage_root is None:
raise BootmgmtArgumentError('Missing net_osimage_root argument')
# Save the data directory:
self.net_data_root = kwargs.get('data_root', None)
if self.net_data_root is None:
raise BootmgmtArgumentError('Missing net_data_root argument')
# Save the (optional) tftproot-relative directory for config files
# / boot loader files
self.net_tftproot_subdir = kwargs.get('tftproot_subdir', '/')
# Now look for the client id:
self.client_id = kwargs.get('client_id', None)
if self.client_id:
client_id_valid = re.match(
"^(([0-9A-Fa-f]){12}|([0-9A-Fa-f]{2}:){5}[0-9A-Fa-f]{2})$",
self.client_id)
if client_id_valid is None:
raise BootmgmtArgumentError('Bad client_id value: %s' %
self.client_id)
if ':' in self.client_id:
self.client_id = self.client_id.replace(':', '')
super(self.__class__, self).__init__(flags,
boot_class=BootConfig.BOOT_CLASS_NET,
**kwargs)
def _load_boot_config(self, **kwargs):
"""Loads the boot configuration"""
if self.boot_loader:
self.boot_loader.load_config()
def _new_boot_config(self, **kwargs):
"""Initializes this instance with a new boot configuration"""
if self.boot_loader:
self.boot_loader.new_config()
def get_root(self):
return self.net_osimage_root
def commit_boot_config(self, temp_dir=None, boot_devices=None,
force=False):
"""
"""
# NetBootConfig is a bit different from the other BootConfig classes
# in that we need the tuple list to be returned even though we're
# maintaining the configuration files so that they can be loaded
# and modified in the future. As a result, we need to pass a "temp"
# dir to commit_boot_config(), but in this case, it's not temporary--
# it's the location from which we can load the config in the future.
return super(NetBootConfig, self).commit_boot_config(
self.net_data_root, boot_devices, force)
def pxe_suffix(self):
"""Returns a string that should be used as a client-specific suffix
for configuration filenames, or the empty string if this configuration
is not client-specific."""
if self.client_id:
return '.01' + self.client_id.upper()
else:
return ''
###############################################################################
class BootInstance(LoggerMixin):
"""BootInstance is the core abstraction for a bootable instance of an
operating system. BootConfig objects aggregate zero or more
BootInstance objects. A BootInstance should always be associated
with a BootConfig object (so that if a consumer modifies a
BootInstance, the BootConfig is notified that its state has
changed so it can write updated configuration information at
BootConfig.commit_boot_config()-time). This association is made
when a BootInstance is passed to BootConfig.add_boot_instance()."""
# TARGET should be set to None if this class can be used with any
# system target, otherwise should be set to the tuple
# (target, firmwarelist) (i.e. "('x86', ['bios'])"). If firmwarelist is
# None, all firmware types are allowed to use this BootInstance.
TARGET = None
# Valid attributes ('default' is a special case)
_attributes = {'title': None,
'transient': False,
'identfile': None,
'platform' : None}
def __init__(self, rootpath, **kwargs):
copysrc = kwargs.get('copysrc')
if copysrc:
# The copy implementation is a bit tricky. First, we need
# to initialize the current instance's _attributes member,
# because we override __setattr__ and it needs access to
# _attributes to determine what attribute-setting causes
# this instance to mark the associated bootconfig as dirty.
self.__dict__['_attributes'] = dict(copysrc._attributes)
# Next, initialize a dictionary with all attributes and their
# either current or default values, so the copy has all
# attributes of the original
wholedict = copysrc.default_attributes()
wholedict.update(copysrc.serialization_dict())
self.__dict__.update(wholedict)
# Finally, init the private state associated with this BootInstance
self._default = copysrc._default
# The copied BootInstance does NOT get its _bootconfig initialized
self._bootconfig = None
self.rootpath = rootpath
self.linked_boot_instance = None
self.init_from_rootpath(rootpath)
return
# If the child class added its own set of attributes, just append to
# it; overwise, set it to the default set from this class
(self.__dict__.setdefault('_attributes', {})
.update(BootInstance._attributes))
self._bootconfig = None
self._default = False
self.boot_vars = None
self.rootpath = None
self.linked_boot_instance = None
# Only add attributes to this instance if they were not overridden
# by the caller's keyword arguments
for key, value in self._attributes.items():
# If the key wasn't already set in this instance,
# init it to a default value here
if not key in kwargs and not key in self.__dict__:
self._debug('DEFAULT: Setting %s="%s"', (key, value))
self.__setattr__(key, value)
# If the user passed in keyword args, add them as attributes
for key, value in kwargs.items():
# If the key wasn't already set in this instance,
# init it to a default value here
if key in self.__dict__:
continue
self._debug('KWARGS: Setting %s="%s"', (key, value))
self.__setattr__(key, value)
# init_from_rootpath must be called after all instance variables
# have been set
if rootpath is not None:
self.init_from_rootpath(rootpath)
@property
def default(self):
return self._default
def default_attributes(self):
"""Returns a dictionary of default attributes
"""
defattr = dict(BootInstance._attributes)
defattr['default'] = False
return defattr
def serialization_dict(self):
"""Returns a dictionary of current attributes and their values.
Child classes should extend this method if there are attributes
they need to preserve that are not in the class's _attributes dict.
"""
retdict = {}
for key in self.default_attributes():
value = getattr(self, key, None)
if value:
retdict[key] = value
# default is a special case, and is not covered by default_attributes:
retdict['default'] = self.default
return retdict
def copy(self):
"""Returns a copy of the current BootInstance instance."""
return self.__class__(self.rootpath, copysrc=self)
def update(self, other):
"""Updates the attributes in this BootInstances with the values from
other. The 'default' property IS NOT updated."""
for key in self._attributes.keys():
try:
newval = getattr(other, key)
if newval != getattr(self, key):
setattr(self, key, newval)
except AttributeError as attrerr:
self._debug('Error while updating: %s; continuing', attrerr)
def compare (self, other, cmp_default=True):
"""Returns True if self is the same as other (attributes and
class names are compared)"""
if self.__class__ != other.__class__:
return False
keys = self._attributes.keys()
if len(set(keys) - set(other._attributes.keys())) != 0:
return False
for key in keys:
try:
if getattr(self, key) != getattr(other, key):
return False
except AttributeError as attrerr:
self._debug('Unexpected AttributeError (%s) when comparing '
'boot instances ([%s] vs. [%s])', (attrerr, self, other))
return False
if cmp_default and self.default != other.default:
return False
return True
def init_from_rootpath(self, rootpath):
"""Initialize the boot instance with information from the root path
given. This can be done after the object has been constructed, so
that consumers do not have to complete the cumbersome task of
mounting each BootInstance that's created."""
self.rootpath = rootpath
try:
self.boot_vars = self._get_boot_vars(rootpath)
except BootmgmtError as bmerr:
self._debug('Error loading bootvars: %s', bmerr)
self.boot_vars = None
def _set_default(self, value):
"""By setting this BootInstance to be the default, the previous default
must be disabled. This property setter function does just that by
using the _bootconfig reference to find the list of boot instances,
locating the previous default, and setting it to False. Note that
only setting the default to True triggers this search (otherwise, we
could end up in an infinite recursive loop)."""
if not value is False and not value is True:
raise BootmgmtArgumentError('default must be either True or False')
if self._default == value: # Nothing to do
return
if value is False:
self._default = False
else:
self._default = True
if not self._bootconfig is None:
for inst in self._bootconfig.boot_instances:
if not inst is self and inst.default is True:
self._debug('Found previous default -- clearing it')
# The following assignment's side-effect marks the
# BootConfig dirty
inst.default = False
return
if not self._bootconfig is None:
# If we got here, then either the value is False or it was True
# and no previous default was found, so mark the BootConfig dirty
self._bootconfig.dirty = True
def _get_boot_vars(self, rootpath):
"""Initializes this instance's boot_vars"""
return bootinfo.BootVariables.get(sysroot=rootpath,
arch=self.platform_requested)
@property
def platform_requested(self):
platform = self.platform
if isinstance(platform, basestring):
return platform
elif (isinstance(platform, tuple) and len(platform) == 2 and
isinstance(platform[0], basestring)):
return platform[0]
return None
def _get_attrdict_by_platform(self, classname, platform):
"""Returns the platform-specific default-attribute dict (if it exists)
otherwise returns the _attributes dict (if it exists), otherwise
returns {}
"""
attrname = '_attributes'
if isinstance(platform, basestring):
attrname = platform + attrname
elif (isinstance(platform, tuple) and len(platform) == 2 and
isinstance(platform[0], basestring)):
attrname = platform[0] + attrname
elif platform:
raise BootmgmtArgumentError('Invalid platform given: Must be '
'a string or a 2-tuple of (string, string|None)')
if attrname not in classname.__dict__:
if not platform:
attrname = get_current_arch_string() + '_attributes'
if attrname not in classname.__dict__:
attrname = '_attributes'
return classname.__dict__.get(attrname, {})
def __setattr__(self, key, value):
"""Intercept the set attribute method to enforce setting of a
particular set of boot instance attributes (the union of those
defined in child classes and this class). 'default' is treated
specially (see _set_default())"""
if key == 'default':
self._debug('key="%s" value="%s"', (key, value))
return self._set_default(value)
self.__dict__[key] = value
if key in self._attributes and not self._bootconfig is None:
self._debug('key="%s" value="%s"', (key, value))
self._bootconfig.dirty = True
def __delattr__(self, key):
"""Delete an attribute from this BootInstance. 'default' is treated
specially, as with __setattr__."""
if key == 'default':
self._debug('delete key="default"')
self._set_default(False) # Same effect as setting it to False
return
if not key in self.__dict__:
raise AttributeError(str(key))
del self.__dict__[key]
if key in self._attributes and not self._bootconfig is None:
self._debug('delete key="%s"', key)
if not self._bootconfig is None:
self._bootconfig.dirty = True
def __str__(self):
s = ''
if self.default is True: # only print default if it's True
s += 'default = True\n'
for item in self._attributes.keys():
s += str(item) + ' = '
s += str(getattr(self, item, '<Not Defined>')) + '\n'
if self.boot_vars and len(self.boot_vars) > 0:
s += '===[ %d Boot variables ]===\n' % len(self.boot_vars)
s += '\n'.join(map(str, self.boot_vars))
s += '\n===[ End Boot Variables ]==='
else:
s += '===[ No Boot Variables ]==='
return s
def __eq__(self, other):
# XXX Ugh
for key in self._attributes:
if key not in other._attributes:
return False
if getattr(self, key) != getattr(other, key):
return False
return True
class ChainDiskBootInstance(BootInstance):
"""A boot instance of a chainloaded operating system"""
TARGET = ('x86', ['bios'])
_attributes = {'chaininfo' : None,
'chainstart' : '0',
'chaincount' : '1',
'forceactive': False}
def __init__(self, rootpath=None, **kwargs):
"""rootpath is not supported, so should remain `None'"""
copysrc = kwargs.get('copysrc')
if not copysrc:
kwargs = self._sanitize_kwargs(kwargs)
(self.__dict__.setdefault('_attributes', {})
.update(ChainDiskBootInstance._attributes))
super(ChainDiskBootInstance, self).__init__(None, **kwargs)
if copysrc:
# Treat the copy of chaininfo specially-- must be done after
# the super().__init__ to set up the bootconfig reference!
self.chaininfo = tuple(copysrc.chaininfo)
def default_attributes(self):
"""Returns a dictionary of default attributes
"""
superdict = super(ChainDiskBootInstance, self).default_attributes()
superdict.update(ChainDiskBootInstance._attributes)
return superdict
def serialization_dict(self):
"""Returns a dictionary of attribute values suitable for rebuilding
this instance. chaininfo is converted into a comma-delimited string.
"""
superdict = super(ChainDiskBootInstance, self).serialization_dict()
if self.chaininfo and type(self.chaininfo) is tuple:
tuple_strings = []
# Chaininfo is a nested tuple, but for serialization, we flatten
# it.
for item in self.chaininfo:
if type(item) is tuple:
for subitem in item:
tuple_strings.append(str(subitem))
else:
tuple_strings.append(str(item))
superdict['chaininfo'] = ', '.join(tuple_strings)
return superdict
def _sanitize_kwargs(self, kwargs):
"""Checks the values passed in kwargs and fixes them to the proper
types. Returns a new dictionary with the modified values.
"""
kwargs = dict(kwargs)
chaininfo = kwargs.get('chaininfo')
if chaininfo and isinstance(chaininfo, basestring):
# Split string into a list:
chaininfo = chaininfo.split(',')
disk = int(chaininfo[0])
kwargs['chaininfo'] = (disk,)
part = None
if len(chaininfo) == 2:
try: # Convert each element to an int, if possible
part = int(chaininfo[1])
except ValueError:
part = chaininfo[1]
elif len(chaininfo) > 2:
part = []
for idx, item in enumerate(chaininfo[1:]):
part.append(item.strip())
try: # Convert each element to an int, if possible
part[idx] = int(item)
except ValueError:
pass
part = tuple(part)
if part is not None:
kwargs['chaininfo'] = (disk, part)
forceactive = kwargs.get('forceactive')
if forceactive and isinstance(forceactive, basestring):
self._debug('Trying to parse a bool: %s' % forceactive)
kwargs['forceactive'] = parse_bool(forceactive)
return kwargs
class SolarisBootInstance(BootInstance):
"""Abstraction for a Solaris Boot Instance. Supported attributes are:
- kernel [string] [optional] [x86-only]
- boot_archive [string] [optional] [x86-only]
- kargs [string] [optional] [x86-only]: Kernel argument string
- signature [string] [optional] [x86-only]: The "boot
signature" of this
boot instance.
- Public Methods:
- expanded_kargs(): [x86-only] Expands all macros in kargs
and returns the expanded kernel argument string.
"""
x86_attributes = {
'kernel': '/platform/i86pc/kernel/amd64/unix',
'boot_archive': '/platform/i86pc/amd64/boot_archive',
'kargs': None,
'signature': None,
'splashimage': None,
'foreground': None,
'background': None
}
def __init__(self, rootpath, **kwargs):
copysrc = kwargs.get('copysrc')
if not copysrc:
# If the child class added its own set of attributes, just append to
# it; overwise, set it to the default set from this class
attrdict = self._get_attrdict_by_platform(SolarisBootInstance,
kwargs.get('platform'))
self._debug('Applying dict %s to _attributes', attrdict)
self.__dict__.setdefault('_attributes', {}).update(attrdict)
# If kargs are specified, scrub them for any args that may be
# boot loader implementation-dependent strings so that boot
# instances loaded from different boot loaders' config files can
# be directly compared
if kwargs.get('kargs', None):
kwargs = dict(kwargs)
kwargs['kargs'] = scrub_kargs(kwargs['kargs'], self).strip()
# If the args are empty after the scrub, just set them to None
# to make comparisons sane.
if kwargs['kargs'] == '':
kwargs['kargs'] = None
super(SolarisBootInstance, self).__init__(rootpath, **kwargs)
def default_attributes(self):
"""Returns a dictionary of default attributes
"""
superdict = super(SolarisBootInstance, self).default_attributes()
attrdict = self._get_attrdict_by_platform(SolarisBootInstance,
getattr(self, 'platform', None))
superdict.update(attrdict)
return superdict
def expanded_kargs(self):
"""Expands all macros found in self.kargs and returns the string
with all macros expanded. Currently returns the kargs unmodified."""
if get_current_arch_string() != 'x86':
raise BootmgmtUnsupportedPlatformError('expanded_kargs() not '
'supported on the %s platform' % get_current_arch_string())
return self.kargs
class SolarisDiskBootInstance(SolarisBootInstance):
"""Abstraction for a Disk-based Solaris Boot Instance. Additional
attributes supported are:
- fstype [string] [required]: One of: [ 'ufs', 'zfs' ]
- If fstype == 'zfs':
* bootfs [string] [overrides the value of 'rpool']
* rpool [string]
Public methods implementations:
- expanded_kargs(): [x86-only] Expands all macros in kargs
and returns the expanded kernel argument
string (currently only $ZFS-BOOTFS
expansion).
"""
_attributes = {'fstype': 'zfs',
'bootfs': None,
'rpool' : None}
def __init__(self, rootpath, **kwargs):
copysrc = kwargs.get('copysrc')
if not copysrc:
# If the child class added its own set of attributes, just append to
# it; overwise, set it to the default set from this class
(self.__dict__.setdefault('_attributes', {})
.update(SolarisDiskBootInstance._attributes))
super(SolarisDiskBootInstance, self).__init__(rootpath, **kwargs)
if copysrc:
return
if self.fstype == 'zfs':
if not 'bootfs' in kwargs and not 'rpool' in kwargs:
raise BootmgmtMissingInfoError('missing bootfs or rpool arg')
if 'bootfs' in kwargs:
# Make sure bootfs appears to be well-formed
bootfs_spec = kwargs['bootfs'].split('/', 2)
if len(bootfs_spec) != 3 or bootfs_spec[1] != 'ROOT':
raise BootmgmtArgumentError('Invalid bootfs: %s' %
kwargs['bootfs'])
# If title is STILL None, try an alternate (the last component of the
# bootfs):
if self.title is None:
self.title = self._default_title()
def _default_title(self):
"""Returns the default title string from the bootfs, if any, or None
"""
try:
return bootfs_split(self.bootfs)[1]
except:
return None
def default_attributes(self):
"""Returns a dictionary of default attributes
"""
superdict = super(SolarisDiskBootInstance, self).default_attributes()
superdict.update(SolarisDiskBootInstance._attributes)
superdict['title'] = self._default_title()
return superdict
def init_from_rootpath(self, rootpath):
# Invoke the parent's init_from_rootpath first
super(SolarisDiskBootInstance, self).init_from_rootpath(rootpath)
# self.title is guaranteed to have been initialized to something
if not self.title is None:
return # No need to get the title from /etc/release
try:
alt_title = self.bootfs.split('/', 2)[2]
except:
alt_title = None
self.title = solaris_release_name(rootpath, alt_title)
def expanded_kargs(self):
"""Expands all macros found in self.kargs and returns the string
with all macros expanded. Currently, only $ZFS-BOOTFS is a
supported macro."""
if get_current_arch_string() != 'x86':
raise BootmgmtUnsupportedPlatformError('expanded_kargs() not '
'supported on the %s platform' % get_current_arch_string())
kargs = super(SolarisDiskBootInstance, self).expanded_kargs()
# If kargs is the empty string (or is None) or if the well-known
# legacy GRUB $ZFS-BOOTFS token does NOT exist in the kargs and
# this is a ZFS boot instance, we need to create the entire
# zfs bootfs argument from scratch.
if (not kargs or not '$ZFS-BOOTFS' in kargs) and self.fstype == 'zfs':
if not kargs:
kargs = ''
return kargs + ' -B ' + self._expand_zfs_bootfs()
elif kargs is None:
return None
# ZFS-BOOTFS expands to a string that contains two elements:
# The zfs-bootfs element and the bootpath element. Retrieve
# Them both from libzfs here, then perform the substitution.
return kargs.replace("$ZFS-BOOTFS", self._expand_zfs_bootfs())
def _expand_zfs_bootfs(self):
"""Use libzfs (via ctypes) to get the zpool properties and return a
string that consists of the kernel arguments needed to specify the
zfs root pool and bootfs associated with this BootInstance
"""
lzfsh = libzfs_init()
if getattr(self, 'bootfs', None) is not None:
zph = zpool_open(lzfsh, self.bootfs.split('/')[0])
elif getattr(self, 'rpool', None) is not None:
zph = zpool_open(lzfsh, self.rpool)
else:
libzfs_fini(lzfsh)
self._debug('bootfs AND rpool not set in instance')
return ''
physpaths = zpool_get_physpath(lzfsh, zph)
if self.bootfs is None:
bootfs = zpool_get_prop(lzfsh, zph, ZPOOL_PROP_BOOTFS)
else:
bootfs = self.bootfs
zpool_close(zph)
libzfs_fini(lzfsh)
# XXX: We're just using the first physpath for now
return ('zfs-bootfs=' + bootfs + ',bootpath="' + physpaths[0] + '"')
class SolarisNetBootInstance(SolarisBootInstance):
"""Abstraction for a Network-based Solaris Boot Instance.
"""
def __init__(self, rootpath, **kwargs):
copysrc = kwargs.get('copysrc')
super(SolarisNetBootInstance, self).__init__(rootpath, **kwargs)
if copysrc:
return
# If title is STILL None, make one up.
if self.title is None:
self.title = 'Solaris Network Boot Instance'
class SolarisODDBootInstance(SolarisBootInstance):
"""Abstraction for an optical-disc-based Solaris Boot Instance
"""
def __init__(self, rootpath, **kwargs):
super(SolarisODDBootInstance, self).__init__(rootpath, **kwargs)
# No further initialization is needed if there is a copysrc
def init_from_rootpath(self, rootpath):
# Invoke the parent's init_from_rootpath first
super(SolarisODDBootInstance, self).init_from_rootpath(rootpath)
# self.title is guaranteed to have been initialized to something
if not self.title is None:
return
self.title = solaris_release_name(rootpath, None)
def solaris_release_name(rootpath, alt_title):
# On a disk-based instance, the title can be derived from the
# first line of the /etc/release file, if it exists
title = alt_title if not alt_title is None else 'Oracle Solaris'
try:
with open(rootpath + '/etc/release') as etcrelease:
title = etcrelease.readline().strip()
except IOError:
pass
return title
###############################################################################
#################################### TESTS ##################################
###############################################################################
class TestBootConfig(unittest.TestCase):
def setUp(self):
pass
def tearDown(self):
print 'Done with ' + str(self)
def test_flags(self):
pass
def testSuite():
return unittest.TestLoader().loadTestsFromTestCase(TestBootConfig)
if __name__ == '__main__':
unittest.main()