#! /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.
#
"""
GRUB2 boot loader backend for pybootmgmt
"""
# pylint: disable=C0302
import ConfigParser
import errno
import fcntl
import filecmp
import grp
import libbe
import libbe_py
import os
import pkg.pkgsubprocess as subprocess
import pwd
import shutil
import stat
import syslog
import sys
import tempfile
import time
from bootmgmt import (BootmgmtError, BootmgmtArgumentError,
BootmgmtConfigReadError, BootmgmtConfigWriteError,
BootmgmtNotSupportedError,
BootmgmtUnsupportedOperationError,
BootmgmtUnsupportedPropertyError,
BootmgmtUnsupportedPlatformError,
BootmgmtInterfaceCodingError)
from bootmgmt.pysol import (libzfs_init, libzfs_fini, zpool_open, zpool_close,
zpool_get_prop, ZPOOL_PROP_GUID, is_gpt_disk,
mnttab_open, getmntany, mnttab_close)
from bootmgmt.bootloader import BootLoader, BootLoaderInstallError
from bootmgmt.bootconfig import (BootConfig, DiskBootConfig, ODDBootConfig,
NetBootConfig, SolarisDiskBootInstance)
from bootmgmt.bootutil import (get_current_arch_string, LoggerMixin,
bootfs_split, parse_bool,
find_efi_system_partition,
get_zpool_default_bootfs,
is_zfs_encrypted_dataset,
get_bootfs_list_from_libbe)
from bootmgmt.bootinfo import SystemFirmware
EFISYS_PARTID = 0xEF
EFISYS_GUID = 'C12A7328-F81F-11D2-BA4B-00A0C93EC93B'
def timestamp():
"""Returns a timestamp string with all significant digits
"""
return '%f' % time.time()
#############################################################################
# menu.conf interface classes
#############################################################################
# pylint: disable=R0903
class MenuConfigEntry(LoggerMixin):
"""MenuConfigEntry represents one menu entry (section) from a
MenuConfigParser. Each entry is linked with the MenuConfigParser that
created it so that when it is modified, it can inform the parent
that state has been modified.
"""
def __init__(self, mcp, section, options=None):
"""Initialize the MenuConfigEntry
"""
super(MenuConfigEntry, self).__init__()
if not mcp:
raise BootmgmtArgumentError('Bad arguments to %s.__init__' %
self.__class__.__name__)
if not section or not type(section) is tuple or len(section) != 2:
raise BootmgmtArgumentError('Bad section argument to %s.__init__' %
self.__class__.__name__)
self._mcp = mcp
# section is the section name tuple:
# (<class type>, <0-based index>)
self.section = section
# options is a dictionary of all options in the section
if options:
self._options = self._stringify_options(options)
else:
self._options = {}
if 'modified' not in self._options:
self._options['modified'] = timestamp()
def update_options(self, newdict):
"""Update the options dict in this instance with the newdict passed in.
Detects if there are changes, and if there are, marks the parent
MenuConfigParser as dirty.
"""
dirty = False
del_keylist = []
for key in self._options.keys():
if not newdict.has_key(key):
del_keylist.append(key)
if del_keylist:
for key in del_keylist:
del self._options[key]
dirty = True
for key, value in newdict.items():
if (not dirty and
(key not in self._options or self._options[key] != value)):
dirty = True
self._options[key] = value
if dirty:
self._mcp.dirty = True
def _stringify_options(self, options):
"""Returns a dict with the options passed in, stringified.
"""
options = dict(options) # Do not modify the original dict
for key, value in options.items():
if not isinstance(value, basestring):
self._debug('Stringifying %s', key)
options[key] = str(value)
return options
@property
def options(self):
"""Return a copy to the options dict so no one can modify it.
Ideally, it would be an immutable dict.
"""
return dict(self._options)
@property
def last_modified(self):
"""Returns a floating point timestamp of the last modified time, or 0
if no timestamp exists.
"""
ts_prop = self._options.get('modified')
if ts_prop:
try:
return float(ts_prop)
except ValueError:
self._debug('Error while converting %s to float', ts_prop)
return 0
@property
def section_string(self):
"""Return a string that represents the section name
"""
if len(self.section) == 2:
return (('%s' + MenuConfigParser.SECTION_SEPARATOR + '%d') %
self.section)
else:
return None
def match_section_by_string(self, sect_string):
"""Match the section using the string passed.
"""
split_section = sect_string.split(MenuConfigParser.SECTION_SEPARATOR)
if len(split_section) == 2:
classname = split_section[0]
try:
idx = int(split_section[1])
if self.section == (classname, idx):
return True
except ValueError:
self._debug('Error converting index to int for %s',
sect_string)
return False
def __str__(self):
"""Returns the string representation of this menu configuration entry
"""
str_rep = ''
str_rep = '[%s]' % self.section_string
for key, value in self._options.items():
str_rep += '\n%s=%s' % (key, value)
return str_rep
# pylint: disable=W0212
@staticmethod
def _cmp_sections(first, other):
"""Compare two MenuConfigEntry instances based on their section names.
"""
# Compare the section tuples directly (the integer comparison
# for the third tuple element happens automatically):
if first.section < other.section:
return -1
elif first.section > other.section:
return 1
return 0
def __cmp__(self, other):
"""Compares two MenuConfigEntry instances. the entry with the
most-recent modified timestamp wins, otherwise, the entry with the
lexically-largest section tuple wins.
"""
try:
self_modified = self._options['modified']
other_modified = other.options['modified']
if float(self_modified) < float(other_modified):
cmpval = -1
elif float(self_modified) == float(other_modified):
cmpval = MenuConfigEntry._cmp_sections(self, other)
else:
cmpval = 1
return cmpval
except (ValueError, KeyError):
pass # Can't compare using the modified timestamps, so revert
# to testing the section names
return MenuConfigEntry._cmp_sections(self, other)
# pylint: enable=W0212
# pylint: enable=R0903
# pylint: disable=R0902
class MenuConfigParser(LoggerMixin):
"""The MenuConfigParser class provides an interface to the menu.conf
file. The menu.conf file contains a set of sections, each (except for two)
of which describes a single Solaris boot entry in the
automatically-generated boot-loader menu. The syntax of the menu.conf is
that accepted by the ConfigParser class. The sections and properties
include:
[meta]
order = <comma-separated list of section names>
[global]
<Boot Loader Properties>
Zero or More of:
[<classname>:<index>]
title=<Menu entry title> [Required]
default={True|False} # If this is the default boot entry
transient={True|False} # If this is a transient entry
Other properties, depending on the <classname>:
bootfs=<bootfs value> [SolarisDiskBootInstance] [required]
kargs=<kernel arguments> [SolarisBootInstance] [optional]
kernel=<path to kernel file> [SolarisBootInstance] [optional]
boot_archive=<path to boot archive> [SolarisBootInstance] [optional]
"""
MENU_CONF_PREAMBLE = ('#\n# This file is modified without notice.\n'
'# DO NOT EDIT THIS FILE DIRECTLY. CHANGES MAY NOT '
'BE PRESERVED.\n'
'# Changes to this file can be made indirectly via '
'bootadm(1M) subcommands.\n#\n')
SECTION_SEPARATOR = '|'
def __init__(self, filename='/rpool/boot/grub/menu.conf', new_conf=False):
"""Creates or loads the menu configuration from the specified file.
If the file does not exist, an empty menu configuration is created,
but is not written to disk (see the write() method). If new_conf
is True, the file's contents are not loaded; instead, an empty menu
configuration is created (but not written yet).
The following Exceptions may be generated:
BootmgmtConfigReadError -- If there's a problem reading config file.
"""
super(MenuConfigParser, self).__init__()
self._filename = filename
self.last_written_path = None
self.entry_list = []
self._dirty = False
self._cfgparser = ConfigParser.RawConfigParser()
self._meta_options = {}
self.global_options = {}
if not new_conf:
self._load(filename)
def _load(self, filename):
"""Attempt to load the file specified. If it does not exist,
proceed with an empty SafeConfigParser.
The following Exceptions may be generated:
BootmgmtConfigReadError -- If there's a problem reading config file.
"""
if not filename:
self._debug('No filename specified -- not loading menu '
'configuration')
return
self._debug('Loading menu.conf from %s', filename)
try:
self._cfgparser.readfp(open(filename))
except IOError as ioerr:
if ioerr.errno == errno.ENOENT:
self._debug('[IGNORED] %s not found', filename)
return
raise BootmgmtConfigReadError('Error trying to read the '
'menu configuration file ', ioerr)
self._load_sections()
self.entry_list.sort()
def _load_sections(self):
"""Creates MenuConfigEntry objects from the ConfigParser's section
list and adds them to the entry list. Also loads the meta and
global sections, if found.
"""
# Now create an MenuConfigEntry object for each section
for sect in self._cfgparser.sections():
# Parse the section name into a tuple:
section_split = sect.split(MenuConfigParser.SECTION_SEPARATOR)
new_entry = None
# Handle 'normal' menu entries
if len(section_split) == 2:
try:
section_split = (section_split[0].strip(),
int(section_split[1]))
new_entry = MenuConfigEntry(self, section_split,
self._cfgparser.items(sect))
except ValueError: # Not an int, ignore entry
self._debug('Malformed section name: "%s"-- ignoring',
sect)
elif sect == 'meta': # Handle [meta] section
self._meta_options = dict(self._cfgparser.items('meta'))
elif sect == 'global': # Handle [global] section
self.global_options = dict(self._cfgparser.items('global'))
# Globals are sanity-checked by MenuConfigParser consumers
if new_entry:
self._debug('Adding entry to list:\n%s', new_entry)
self.entry_list.append(new_entry)
# pylint: disable=E0102,E0202,E1101
@property
def dirty(self):
"""Returns the state of the internal dirty variable.
"""
return self._dirty
@dirty.setter
def dirty(self, val):
"""Sets the state of the internal dirty variable. Emits a debug
message if the state of the internal dirty variable changed.
"""
if self._dirty != val:
self._debug('dirty => %s', val)
self._dirty = val
# pylint: enable=E0102,E0202,E1101
@property
def filename(self):
"""Returns the filename of the menu.conf that this object was created
with.
"""
return self._filename
# pylint: disable=R0913
def add_entry(self, section_name, propdict=None):
"""Add an entry to the menu configuration file.
The section_name parameter is used to derive the section
name.
If the propdict is not None, it contains
the properties used to initialize the new menu entry.
"""
new_section = self.gen_new_section(section_name)
newent = MenuConfigEntry(self, new_section, propdict)
self.entry_list.append(newent)
self.dirty = True
self._debug('Added new entry:\n%s', newent)
return newent
# pylint: enable=R0913
def delete_entry(self, entry):
"""Delete an entry by matching the object address to one in the list.
"""
self.delete_entries(cmp_fn=(lambda x: entry == x), match_all=False)
def delete_entries(self, section_name=None, cmp_fn=None, match_all=True):
"""Deletes one or more menu configuration entries. Entries are found
based by either matching the section_name argument (if cmp_fn is None)
or by invoking cmp_fn for each entry in the entry list. If cmp_fn
returns True, that entry is deleted. cmp_fn takes 1 argument--
the entry instance to consider for deletion).
If match_all is True, then all entries that match are deleted;
otherwise, only the first match is deleted.
Returns a count of the number of deleted entries.
"""
if match_all:
orig_len = len(self.entry_list)
if cmp_fn:
self.entry_list = [x for x in self.entry_list if not cmp_fn(x)]
elif section_name:
self.entry_list = [x for x in self.entry_list
if section_name != x.section_name]
count = orig_len - len(self.entry_list)
self._debug('Deleted %d entr(y|ies)', count)
if count > 0:
self.dirty = True
else:
count = 0
for idx, entry in enumerate(self.entry_list):
if ((section_name and section_name == entry.section[0]) or
(cmp_fn and cmp_fn(section_name, entry))):
self._debug('Deleting entry %s', str(entry.section))
del self.entry_list[idx]
self.dirty = True
count = 1
break
return count
def _get_meta_cds_list(self, propname):
"""CDS = comma-delimited string
"""
stringlist = self._meta_options.get(propname)
if stringlist:
list_of_strings = stringlist.split(',')
list_of_strings = [x.strip() for x in list_of_strings]
return list_of_strings
return None
def _set_meta_cds(self, propname, stringlist):
"""Set the propname property to a comma-separated string composed
of strings from stringlist.
"""
if stringlist:
newvalue = ', '.join(stringlist)
if (not propname in self._meta_options or
self._meta_options[propname] != newvalue):
self._debug('meta:%s: was %s now %s', (propname,
self._meta_options.get(propname, ''), newvalue))
self._meta_options[propname] = newvalue
self.dirty = True
def get_order(self):
"""Get a list of section headers that specify the order of menu
entries, if one exists.
"""
return self._get_meta_cds_list('order')
def set_order(self, order_list):
"""Set the list of section headers that specifies the order of menu
entries.
"""
self._set_meta_cds('order', order_list)
def clear_order(self):
"""Delete the order property from the meta section and mark this
instance dirty.
"""
if 'order' in self._meta_options:
del self._meta_options['order']
self.dirty = True
def gen_new_section(self, section_name):
"""Generates a new section title based on the existing sections and
the name passed in. Note that until the section is a part of a
MenuConfigEntry object and that object is added to this
MenuConfigParser's entry list, it's possible for this function to
generate multiple identical section names.
"""
# Now find the largest used index for the section_name
used_indices = [x.section[1] for x in self.entry_list
if x.section[0] == section_name]
if not used_indices: # No other entries match, so index=0
section_tuple = (section_name, 0)
else: # Search for and return a tuple with a free index
unused_indices = list(set(range(len(used_indices) + 1)) -
set(used_indices))
if not unused_indices:
raise BootmgmtError('Could not find a free section index!')
section_tuple = (section_name, unused_indices[0])
self._debug('Generated section: "%s"', str(section_tuple))
return section_tuple
def _synch_to_config_parser(self):
"""Generate a new ConfigParser using the data from the MenuConfigEntry
list (section and option data) and the meta/global dictionaries.
"""
new_parser = ConfigParser.SafeConfigParser()
if self._meta_options:
new_parser.add_section('meta')
for key, value in self._meta_options.items():
new_parser.set('meta', key, value)
if self.global_options:
new_parser.add_section('global')
for key, value in self.global_options.items():
new_parser.set('global', key, value)
for entry in self.entry_list:
string_list = [str(x) for x in entry.section]
new_section = MenuConfigParser.SECTION_SEPARATOR.join(string_list)
try:
new_parser.add_section(new_section)
except ConfigParser.DuplicateSectionError:
self._debug('Duplicate section error for "%s"', new_section)
else:
for key, value in entry.options.items():
if not isinstance(value, basestring):
new_parser.set(new_section, key, str(value))
else:
new_parser.set(new_section, key, value)
return new_parser
def write(self, alt_file=None, force=False):
"""Writes the menu configuration data to the menu configuration file.
If no changes have been made since the configuration was loaded,
write() will return without writing the file (unless force is True).
If alt_file is not None, the menu configuration will be written there.
Exceptions that may be generated:
BootmgmtConfigWriteError -- If the menu config file cannot be created
or if there's a problem writing to it.
"""
if not self._dirty and not force:
self._debug('avoiding write')
return
elif not self._dirty and force:
self._debug('forcing write')
# First synch the MenuConfigParser state to a new ConfigParser:
self._cfgparser = self._synch_to_config_parser()
# Use the alternate file, if specified; otherwise use the cached
# filename supplied to the constructor
write_file = alt_file if alt_file else self._filename
# We cannot proceed without a filename
if not write_file:
raise BootmgmtArgumentError('menu configuration filename not '
'specified')
try:
oldumask = os.umask(022)
outfile = open(write_file, 'w')
os.umask(oldumask)
outfile.write(MenuConfigParser.MENU_CONF_PREAMBLE)
self._cfgparser.write(outfile)
self.last_written_path = write_file
self.dirty = False
except IOError as ioerr:
raise BootmgmtConfigWriteError('Error writing menu '
'configuration file ', ioerr)
# pylint: enable=R0902
class GRUB2BootLoader(BootLoader):
"""Implementation of the BootLoader interface for the GRUB2 boot
loader.
"""
WEIGHT = 2
SUPPORTED_PLATFORMS = {'x86': ('bios', 'uefi64')}
SUPPORTED_PROPS = [
BootLoader.PROP_CONSOLE,
BootLoader.PROP_SERIAL_PARAMS,
BootLoader.PROP_TIMEOUT,
BootLoader.PROP_QUIET,
BootLoader.PROP_SPLASH,
BootLoader.PROP_FORECOLOR,
BootLoader.PROP_BACKCOLOR,
BootLoader.PROP_IDENT_FILE,
BootLoader.PROP_THEME
]
# These prefixes MUST NOT start with '/', or os.path.join() will not
# behave as we want below.
GRUB_PLATFORM_PREFIXES = {'bios' : 'usr/lib/grub2/bios',
'uefi64': 'usr/lib/grub2/uefi64'}
GRUB_BOOT_SUBDIRS = {'bios' : 'i386-pc',
'uefi64': 'x86_64-efi'}
GRUB_DATA_PATHS = {'bios' : os.path.join(GRUB_PLATFORM_PREFIXES['bios'],
'lib/grub/' + GRUB_BOOT_SUBDIRS['bios']),
'uefi64': os.path.join(GRUB_PLATFORM_PREFIXES['uefi64'],
'lib/grub/' + GRUB_BOOT_SUBDIRS['uefi64'])}
GRUB_TARGET_STRINGS = {
'bios' : GRUB_BOOT_SUBDIRS['bios'].replace('-', '_'),
'uefi64': GRUB_BOOT_SUBDIRS['uefi64'].replace('-', '_')
}
# List of modules that should be baked into the GRUB 2 image (i.e.
# available in rescue mode). Each key in PRELOADED_MODULES *must* be
# a list of strings (module names).
PRELOADED_MODULES_COMMON = ['minicmd', 'reboot']
PRELOADED_MODULES = {'bios' : PRELOADED_MODULES_COMMON,
'uefi64': PRELOADED_MODULES_COMMON}
# These may look like duplicates of the BOOT_SUBDIRS, but they're separate
# because they're actualy arguments to the grub-mkimage command.
MKIMAGE_FORMATS = {'bios' : 'i386-pc',
'uefi64': 'x86_64-efi'}
VIDEO_BACKENDS = {'bios' : 'vbe',
'uefi64': 'efi_gop'}
GFX_MODELIST = "1024x768x32;1024x768x16;800x600x16;640x480x16;" \
"640x480x15;640x480x32"
GRUB_INSTALL = 'sbin/grub-install'
GRUB_MKCONFIG = 'sbin/grub-mkconfig'
GRUB_MKIMAGE = 'bin/grub-mkimage'
# This exit code is returned by grub-install and grub-mkimage if it is
# incompatible with the GRUB2 modules on which it was asked to operate.
# In the case of ODDBootConfig, this is a fatal error. For DiskBootConfig,
# we log this fact and then proceed with deferred boot loader activation.
EXIT_CODE_INCOMPATIBLE = 5
DEFERRED_ACTIVATION_FILENAME = '.boot_loader_update_required'
# GRUB_OTHERS stores lists of 3-tuples (source, destination, perms)
# (source is relative to the data root (i.e. the root directory of a boot
# instance) and destination is relative to the config data root (i.e. the
# zfs top level dataset for a root pool). The destination is relative to
# the grub configuration directory (boot/grub) and perms is a 3-tuple of
# (user, group, mode). If perms is None, GRUB_OTHERS_PERM_DEFAULTS is used.
GRUB_OTHERS_PERM_DEFAULTS = ('root', 'root', 0644)
BOOT_GRUB_SUBDIR = 'boot/grub'
THEME_SUBDIR = 'themes'
BOOT_GRUB_THEME_SUBDIR = BOOT_GRUB_SUBDIR + '/' + THEME_SUBDIR
THEME_DEFAULT = 'solaris'
THEME_TEMPLATE = os.path.join(GRUB_PLATFORM_PREFIXES['bios'],
'share/grub/themes/%s/theme.txt')
THEME_DEFAULT_PATH = THEME_TEMPLATE % THEME_DEFAULT
EXTN_TO_MODULE = {'jpg': 'jpeg', 'jpeg': 'jpeg',
'png': 'png',
'tga': 'tga'}
FONT_EXTN_LIST = [ 'pf2' ]
GRUB_OTHERS = [(os.path.join(GRUB_PLATFORM_PREFIXES['bios'],
'share/grub/unicode.pf2'),
'unicode.pf2', None),
('boot/grub/splash.jpg',
'splash.jpg', None)]
GRUB_NBP_PATHS = {'bios' : 'boot/grub/pxegrub2',
'uefi64' : 'boot/grub/grub2netx64.efi'}
CORE_IMG_BASENAME = 'core.img'
# Paths are relative to the zfs top level dataset:
CUSTOM_CFG_BASENAME = 'custom.cfg'
GRUB_CFG_BASENAME = 'grub.cfg'
GRUB_CFG_PERMS = 0644
MENU_CONF_BASENAME = 'menu.conf'
MENU_CONF_PERMS = 0644
GRUB_DEFS_PATH_FMT = 'grub2_defs.%s'
EL_TORITO_IMAGENAMES = {'bios' : 'boot/bios.img',
'uefi64': 'boot/uefi.img'}
# Due to a limitation in Solaris's mkfs -F pcfs, we cannot make a
# fat filesystem with nofdisk of size < 4M.
UEFI_FS_MIN_SIZE = 4 * 1024 * 1024 # FAT filesystem min size
DEFAULT_GRUB_SPLASH = 'splash.jpg' # relative to the config dir
DEFAULT_TIMEOUT = 30 # 30 seconds is the default timeout
DEFAULT_FORECOLOR = '343434'
DEFAULT_BACKCOLOR = 'F7FBFF'
@classmethod
def probe(cls, **kwargs):
"""Probe for GRUB2 on a system.
"""
bootconfig = kwargs.get('bootconfig')
if (bootconfig is None or bootconfig.boot_class is None):
return None
if (bootconfig.boot_class != BootConfig.BOOT_CLASS_NET and
get_current_arch_string() != 'x86'):
cls._debug('GRUB2 boot loader not supported on this '
'platform')
return None
if (bootconfig.boot_class == BootConfig.BOOT_CLASS_DISK and
bootconfig.boot_fstype is not None):
return GRUB2BootLoader._probe_disk(**kwargs)
elif bootconfig.boot_class == BootConfig.BOOT_CLASS_ODD:
return GRUB2BootLoader._probe_odd(**kwargs)
elif bootconfig.boot_class == BootConfig.BOOT_CLASS_NET:
try:
cls._validate_platform_and_firmware(bootconfig,
kwargs.get('fwtype'))
except BootmgmtError as bmerr:
cls._debug('[PROBE ABORTED DUE TO EXCEPTION] %s', bmerr)
return None
return GRUB2BootLoader._probe_net(**kwargs)
else:
raise BootmgmtUnsupportedOperationError('Boot class %s not '
'supported' % bootconfig.boot_class)
@classmethod
def _probe_odd(cls, **kwargs):
"""This GRUB2 probe function searches the ODD's root, looking for
the GRUB2 core modules"""
bootconfig = kwargs.get('bootconfig')
root = bootconfig.get_root()
cls._debug('odd_image_root=%s', root)
odd_data_probe_list = [os.path.join(
cls.GRUB_DATA_PATHS['bios'],
'cdboot.img'),
os.path.join(
cls.GRUB_DATA_PATHS['bios'],
'kernel.img'),
os.path.join(
cls.GRUB_DATA_PATHS['uefi64'],
'kernel.img')]
try:
cls._probe_generic(root, odd_data_probe_list)
except BootmgmtNotSupportedError:
return None
return GRUB2BootLoader(**kwargs)
@classmethod
def _probe_net(cls, **kwargs):
"""This GRUB2 probe function searches the net install image root,
looking for the GRUB2 NBP programs"""
bootconfig = kwargs.get('bootconfig')
artifacts = GRUB2BootLoader._probe_artifacts_net(bootconfig)
if not artifacts:
return None
kwargs['artifacts'] = artifacts
return GRUB2BootLoader(**kwargs)
@classmethod
def _probe_disk(cls, **kwargs):
"""This GRUB2 probe function invokes probe_artifacts to look for
the menu.conf file at the ZFS top-level dataset (if this is a ZFS-based
DiskBootConfig), as well as the GRUB2 kernel and utilities.
"""
bootconfig = kwargs.get('bootconfig')
artifacts = cls._probe_artifacts_disk(bootconfig)
if not artifacts:
return None
kwargs['artifacts'] = artifacts
return GRUB2BootLoader(**kwargs)
@classmethod
def _probe_generic(cls, dataroot, datafiles):
"""Generic probe function for GRUB2.
"""
# Both the data root location must be specified
if dataroot is None:
raise BootmgmtNotSupportedError('dataroot is None')
try:
for datafile in datafiles:
fpath = os.path.join(dataroot, datafile)
cls._debug('Probing for %s', fpath)
open(fpath).close()
except IOError as ioerr:
raise BootmgmtNotSupportedError('IOError when checking for '
'datafiles', ioerr)
@classmethod
def _probe_artifacts_net(cls, bootconfig):
"""Looks for the menu.conf and the NBPs. There are no "tools"
needed for NetBootConfig. The images are preassembled.
"""
artifacts = []
# This is the OS image's root; we need to look for the config
# files in the net_data_root.
root = bootconfig.get_root()
cls._debug('net_image_root=%s, net_data_root=%s', (root,
bootconfig.net_data_root))
net_data_probe_list = [GRUB2BootLoader.GRUB_NBP_PATHS['bios'],
GRUB2BootLoader.GRUB_NBP_PATHS['uefi64']]
menu_conf = os.path.join(bootconfig.net_data_root,
GRUB2BootLoader.MENU_CONF_BASENAME +
bootconfig.pxe_suffix())
try:
stat_info = os.stat(menu_conf)
if stat.S_ISREG(stat_info.st_mode) and stat_info.st_size > 0:
artifacts += [BootLoader.ARTIFACT_BOOTLOADER_CONFIGFILES]
except OSError:
pass
try:
cls._probe_generic(root, net_data_probe_list)
artifacts += [BootLoader.ARTIFACT_BOOTLOADER_IMAGES]
artifacts += [BootLoader.ARTIFACT_BOOTLOADER_TOOLS]
except BootmgmtNotSupportedError:
pass
return artifacts
@classmethod
def _probe_artifacts_disk(cls, bootconfig):
"""Probe for artifacts found when used with a DiskBootConfig
"""
# probe_artifacts can either be called at BootLoader factory.get
# time or at runtime. If it's the latter, we need to get the
# most-recent data root in which to look for the artifacts.
bootloader = getattr(bootconfig, 'boot_loader', None)
if bootloader:
# pylint: disable=W0212
dataroot = bootloader._get_boot_loader_data_root()
# pylint: enable=W0212
else:
dataroot = bootconfig.get_root()
if bootconfig.boot_fstype == 'ufs':
cfgdataroot = dataroot
elif bootconfig.boot_fstype == 'zfs':
cfgdataroot = bootconfig.zfstop
else:
cls._debug('[PROBE FAILURE] Unknown filesystem %s',
bootconfig.boot_fstype)
return []
if not os.path.isdir(dataroot):
cls._debug('dataroot is not a directory')
return []
cls._debug('dataroot=%s cfgdataroot=%s', (dataroot, cfgdataroot))
artifacts = []
# First look for the menu.conf file:
try:
menuconf = os.path.join(cfgdataroot,
GRUB2BootLoader.BOOT_GRUB_SUBDIR,
GRUB2BootLoader.MENU_CONF_BASENAME)
stat_info = os.stat(menuconf)
if stat.S_ISREG(stat_info.st_mode) and stat_info.st_size > 0:
artifacts += [BootLoader.ARTIFACT_BOOTLOADER_CONFIGFILES]
except (OSError, AttributeError) as excpt:
cls._debug('Error while looking for %s: %s', (menuconf, str(excpt)))
# List of essential data files need to pass the disk probe:
disk_data_probe_list = [os.path.join(
cls.GRUB_DATA_PATHS['bios'],
'diskboot.img'),
os.path.join(
cls.GRUB_DATA_PATHS['bios'],
'kernel.img'),
os.path.join(
cls.GRUB_DATA_PATHS['uefi64'],
'kernel.img')]
try:
cls._probe_generic(dataroot, disk_data_probe_list)
artifacts += [BootLoader.ARTIFACT_BOOTLOADER_IMAGES]
except BootmgmtNotSupportedError as excpt:
cls._debug('Error while looking for GRUB2 kernel/loader files: %s',
str(excpt))
# In addition to the loader files themselves, we need to ensure
# that we have access to the grub-install program (and its dependent
# executables) in the currently-running system (otherwise, we'll
# have no way to install GRUB2).
fwname = SystemFirmware.get(None).fw_name
if fwname == 'bios':
disk_execs_probe_list = [os.path.join(
cls.GRUB_PLATFORM_PREFIXES['bios'],
'sbin/grub-setup'),
os.path.join(
cls.GRUB_PLATFORM_PREFIXES['bios'],
'sbin/grub-probe'),
os.path.join(
cls.GRUB_PLATFORM_PREFIXES['bios'],
'sbin/grub-install'),
os.path.join(
cls.GRUB_PLATFORM_PREFIXES['bios'],
'bin/grub-mkimage')]
elif fwname == 'uefi64':
disk_execs_probe_list = [os.path.join(
cls.GRUB_PLATFORM_PREFIXES['uefi64'],
'sbin/grub-probe'),
os.path.join(
cls.GRUB_PLATFORM_PREFIXES['uefi64'],
'sbin/grub-install'),
os.path.join(
cls.GRUB_PLATFORM_PREFIXES['uefi64'],
'bin/grub-mkimage')]
else:
cls._debug('Unsupported firmware type: %s', fwname)
return []
missing_tools = False
for execname in disk_execs_probe_list:
try:
full_exec_path = os.path.join('/', execname)
if (not (os.path.exists(full_exec_path) and
os.access(full_exec_path, os.X_OK))):
cls._debug('%s not found or not executable',
full_exec_path)
missing_tools = True
break
except OSError as oserr:
cls._debug('Error while looking for GRUB2 tools: %s', oserr)
missing_tools = True
if not missing_tools:
artifacts += [BootLoader.ARTIFACT_BOOTLOADER_TOOLS]
return artifacts
@classmethod
def probe_artifacts(cls, bootconfig):
"""Returns a list of artifacts for this BootLoader, given a
bootconfig to use for directory information.
"""
# Only supported for disk-based BootConfigs.
if bootconfig.boot_class == BootConfig.BOOT_CLASS_DISK:
return cls._probe_artifacts_disk(bootconfig)
elif bootconfig.boot_class == BootConfig.BOOT_CLASS_NET:
return cls._probe_artifacts_net(bootconfig)
return []
@property
def config_dir(self):
if self._boot_config.boot_class == BootConfig.BOOT_CLASS_DISK:
return self._config_dir_disk()
elif self._boot_config.boot_class == BootConfig.BOOT_CLASS_NET:
# The config files are written to the root of the data dir.
return self._boot_config.net_data_root
else:
return '/' + GRUB2BootLoader.BOOT_GRUB_SUBDIR
def __init__(self, **kwargs):
"""Constructor for GRUB2 BootLoader
"""
self.pkg_names = []
self.name = 'GRUB2'
super(GRUB2BootLoader, self).__init__(**kwargs)
self.theme_os_path = None
self.theme_grub_path = None
self.theme_mods_set = None
self.theme_fonts_set = None
if self._boot_config.boot_class == BootConfig.BOOT_CLASS_DISK:
self._menu_conf_path = os.path.join(self.config_dir,
GRUB2BootLoader.MENU_CONF_BASENAME)
elif self._boot_config.boot_class == BootConfig.BOOT_CLASS_NET:
self._menu_conf_path = os.path.join(self._boot_config.net_data_root,
GRUB2BootLoader.MENU_CONF_BASENAME +
self._boot_config.pxe_suffix())
if self._boot_config.boot_class in [BootConfig.BOOT_CLASS_DISK,
BootConfig.BOOT_CLASS_NET]:
self._menu_org = GRUB2MenuOrganizer(self._boot_config,
self._menu_conf_path)
else:
self._menu_org = None
def _clear_theme_state(self):
"""Uninitialized all theme attributes (resets to default theme)
"""
self.theme_os_path = None
self.theme_grub_path = None
self.theme_mods_set = None
self.theme_fonts_set = None
def new_config(self):
"""Reset configuration information by starting with a new menu.conf.
"""
super(GRUB2BootLoader, self).new_config()
if self._boot_config.boot_class == BootConfig.BOOT_CLASS_NET:
self._bl_props[BootLoader.PROP_CONSOLE] = \
BootLoader.PROP_CONSOLE_TEXT
else:
self._bl_props[BootLoader.PROP_CONSOLE] = \
BootLoader.PROP_CONSOLE_GFX
if BootLoader.PROP_THEME in self._bl_props:
del self._bl_props[BootLoader.PROP_THEME]
self._clear_theme_state()
if self._boot_config.boot_class in [BootConfig.BOOT_CLASS_DISK,
BootConfig.BOOT_CLASS_NET]:
self._menu_org.new_config()
else:
# menu.conf not supported on non-disk/net bootconfigs
self._menu_org = None
def load_config(self, do_add=True):
"""Loads the configuration, based on the type of BootConfig.
"""
super(GRUB2BootLoader, self).load_config(do_add)
bootcfg = self._boot_config
inst_list = []
if bootcfg.boot_class in [BootConfig.BOOT_CLASS_DISK,
BootConfig.BOOT_CLASS_NET]:
self.dirty, inst_list = self._load_config_generic(do_add)
else:
raise BootmgmtUnsupportedOperationError('Stored configurations '
'are not supported on non-disk/net BootConfigs')
if inst_list:
return inst_list
return []
def _config_dir_disk(self):
"""Returns the directory where configuration files are stored.
"""
if self._boot_config.boot_fstype == 'zfs':
cfgroot = self._boot_config.zfstop
elif self._boot_config.boot_fstype == 'ufs':
cfgroot = self._boot_config.get_root()
else:
raise BootmgmtUnsupportedOperationError('Unknown filesystem: %s'
% self._boot_config.boot_fstype)
return os.path.join(cfgroot, GRUB2BootLoader.BOOT_GRUB_SUBDIR)
def _load_config_generic(self, do_add, menuconf_path=None):
"""Load the menu configuration file and read bootloader properties
and boot instances from it. Autogenerate boot instances from the BEs
on the system. Returns True if boot instances were autogenerated (and
therefore the MenuConfigParser was updated).
"""
self._menu_org.load(menuconf_path)
# merge in properties from the config file:
menu_conf_bl_props = self._menu_org.get_bootloader_props()
for key in menu_conf_bl_props:
try:
self.setprop(key, menu_conf_bl_props[key])
except BootmgmtUnsupportedPropertyError:
self._debug('Unsupported property found in menu config file: '
'%s=%s', (key, menu_conf_bl_props[key]))
boot_instances = self._menu_org.generate_boot_instances()
if do_add:
if boot_instances:
# Add the boot instances to the BootConfig instance:
self._boot_config.add_boot_instance(boot_instances)
return self._menu_org.dirty, boot_instances
def _prop_validate(self, key, value=None, validate_value=False):
"""Part of upgrade support: Correct splash image extension if
it's .xpm.gz.
"""
if key == BootLoader.PROP_SPLASH and isinstance(value, basestring):
if value.find('.xpm.gz') != -1:
return value.replace('.xpm.gz', '.jpg')
elif key == BootLoader.PROP_THEME:
self._clear_theme_state()
return super(GRUB2BootLoader, self)._prop_validate(key, value,
validate_value)
# BootLoader install() hooks
# For GRUB2, the menu.conf file is used to create the etc/default/grub
# (or temporary equivalent for non-Disk BootConfigs) file (from the global
# properties) and the rest of the menu.conf is used by the GRUB2
# autogeneration program in etc/grub.d/XX_solaris.
def _write_config(self, basepath):
"""Generates the grub.cfg file(s) for GRUB2. Different approaches are
taken depending on the type of BootConfig this BootLoader is associated
with. For DiskBootConfig, we update the
$PREFIX/etc/default/grub2_defs.$FW file and the menu.conf before
exec()ing grub-mkconfig to do the actual file generation. For
ODDBootConfig and NetBootConfig, we create the grub.cfg by hand (since
the autogeneration scripts put system-specific GRUB2 commands into the
generated configuration file.)
"""
if self._boot_config is None:
msg = ('Cannot _write_config(%s) - _boot_config is None' %
str(basepath))
self._debug(msg)
raise BootmgmtInterfaceCodingError(msg)
# Determine the type of boot configuration we're dealing with, then
# call the tailored _write_config_<bootconfig_type> method.
if self._boot_config.boot_class == BootConfig.BOOT_CLASS_DISK:
return self._write_config_disk(basepath)
elif self._boot_config.boot_class == BootConfig.BOOT_CLASS_ODD:
return self._write_config_odd(basepath)
elif self._boot_config.boot_class == BootConfig.BOOT_CLASS_NET:
return self._write_config_net(basepath)
else:
raise BootmgmtUnsupportedOperationError('Unsupported BootConfig '
'class: %s' % self._boot_config.__class__.__name__)
def _get_target_list(self):
"""Returns a list of firmware targets that this BootLoader must
handle at install()-time.
"""
targets = self.getprop(BootLoader.PROP_BOOT_TARGS)
if (targets and not isinstance(targets, basestring)
and len(targets) > 0):
return targets
else:
return [self.firmware.fw_name]
def _write_config_disk(self, basepath):
"""To generate the grub.cfg file, we invoke the grub-mkconfig
command. Before doing that, we need to create the grub2_defs.$FW
file, which consists of a number of environment variables whose
values are derived from the BootLoader properties. Once that file is
created, we must store the menu organizer's state (the menu.conf)
then we can invoke grub-mkconfig (since the Solaris autogenerator
plugin consumes the menu.conf file).
"""
menu_conf_tuple_list = self._write_menu_conf(basepath)
target_list = self._get_target_list()
target = target_list[0]
if len(target_list) > 1:
self._debug('%s is %s: ONLY using %s as target value',
(BootLoader.PROP_BOOT_TARGS, target_list, target))
# assemble_other_grub_files must be done before the grub.cfg is
# generated, as grub-mkconfig looks for specific files that
# assemble_other_grub_files copies.
other_tuple_list = self._assemble_other_grub_files(basepath)
grub_cfg_tuple_list = self._generate_grub_cfg(target, basepath)
if basepath:
menu_conf_tuple_list.extend(grub_cfg_tuple_list)
menu_conf_tuple_list.extend(other_tuple_list)
return menu_conf_tuple_list
else:
return None
def _write_menu_conf(self, basepath):
"""Write the menu.conf file to the system location if basepath is None.
Creates directories as needed.
May raise BootmgmtConfigWriteError if there was a problem creating
directories.
"""
if basepath:
# We need to return 2 tuples -- one for the menu.conf and one
# for the grub.cfg.
boot_class = self._boot_config.boot_class
# If final_path is None, do not generate a tuple for menu.conf
final_path = None
if boot_class == BootConfig.BOOT_CLASS_DISK:
dest = GRUB2BootLoader.BOOT_GRUB_SUBDIR + '/' + \
GRUB2BootLoader.MENU_CONF_BASENAME
menuconf_path = os.path.join(basepath,
GRUB2BootLoader.BOOT_GRUB_SUBDIR,
GRUB2BootLoader.MENU_CONF_BASENAME)
token = self._get_root_token()
if token:
final_path = '%(' + token + ')s/'
else:
final_path = '/'
final_path += dest
elif boot_class == BootConfig.BOOT_CLASS_NET:
menuconf_name = GRUB2BootLoader.MENU_CONF_BASENAME + \
self._boot_config.pxe_suffix()
# The menu.conf is stored in the root of the basepath
# provided, NOT in a /boot/grub subdirectory.
menuconf_path = os.path.join(basepath, menuconf_name)
final_path = os.path.join(self.config_dir, menuconf_name)
else:
raise BootmgmtConfigWriteError('Cannot find a place to write '
'%s for BootConfig class %s' %
(GRUB2BootLoader.MENU_CONF_BASENAME, boot_class))
tuple_list = []
tuple_list.extend([(BootConfig.OUTPUT_TYPE_FILE,
menuconf_path,
None,
final_path,
'root',
'root',
GRUB2BootLoader.MENU_CONF_PERMS)])
force = True # always force when a basepath is specified
else:
force = False
tuple_list = None
menuconf_path = self._menu_conf_path
self._make_basedir(menuconf_path)
self._debug('Writing menu.conf to %s', menuconf_path)
if self._boot_config and self._boot_config.boot_instances:
self._menu_org.synch_menu_config(self._boot_config.boot_instances,
self._bl_props)
self._menu_org.store(menuconf_path, force)
return tuple_list
# pylint: disable=R0913
def _generate_stub_cfg_file(self, target, cfg_basename, basepath,
contents=None, contents_only=False, on_zfs=False):
"""Generates a stub file that sources the parent dir's file of the
same name. This is suitable ONLY for DiskBootConfig and ODDBootConfig.
Returns a tuple if basepath is not None.
"""
subdir = GRUB2BootLoader.GRUB_BOOT_SUBDIRS[target]
if basepath:
cfgpath = basepath
subdir = GRUB2BootLoader.BOOT_GRUB_SUBDIR + '/' + subdir
else:
cfgpath = self.config_dir
outfilename = os.path.join(cfgpath, subdir, cfg_basename)
try:
self._make_basedir(outfilename)
outfile = file(outfilename, 'w')
except IOError as err:
raise BootmgmtConfigWriteError('Error during grub.cfg generation: '
"Couldn't create %s" % outfilename, err)
except BootmgmtConfigWriteError:
raise
outfile.write('# Automatically generated -- do not edit\n')
if contents:
outfile.write(contents)
if not contents_only:
outfile.write('source %s/boot/grub/%s\n' %
('/@' if on_zfs else '', cfg_basename))
outfile.close()
if basepath:
token = self._get_root_token()
if token:
final_path = '%(' + token + ')s/'
else:
final_path = '/'
final_path += '%s/%s' % (subdir, cfg_basename)
return (BootConfig.OUTPUT_TYPE_FILE,
outfilename,
None,
final_path,
'root',
'root',
GRUB2BootLoader.GRUB_CFG_PERMS)
# pylint: enable=R0913
@staticmethod
def _generate_grub2_load_video_func(target, def_dict):
"""Generate the GRUB2 script code for the video driver load
function.
"""
targname = GRUB2BootLoader.GRUB_TARGET_STRINGS[target]
vidbackend = def_dict.get('GRUB_VIDEO_BACKEND')
if vidbackend:
video_insmod = ('function load_video_%s {\n'
'\tinsmod %s\n'
'}') % (targname, vidbackend)
else:
video_insmod = 'function load_video_%s {\n\ttrue\n}'
return video_insmod
def _generate_stub_files(self, target, basepath=None, def_dict=None):
"""Generates the stub config files for the platform-specific config
directory.
"""
if (isinstance(self._boot_config, DiskBootConfig) and
getattr(self._boot_config, 'boot_fstype', '') == 'zfs'):
on_zfs = True
else:
on_zfs = False
targname = GRUB2BootLoader.GRUB_TARGET_STRINGS[target]
video_insmod = ''
if def_dict:
video_insmod = self._generate_grub2_load_video_func(target,
def_dict)
if target.startswith('uefi'):
multiboot = 'multiboot2'
module = 'module2'
else:
multiboot = 'multiboot'
module = 'module'
grubscript = ('set target=%s\n'
'set multiboot=%s\n'
'set module=%s\n'
'%s\n') % (targname, multiboot, module,
video_insmod)
stub_grub_cfg = self._generate_stub_cfg_file(target,
GRUB2BootLoader.GRUB_CFG_BASENAME, basepath, grubscript,
on_zfs=on_zfs)
custom_name = '%s/boot/grub/%s' % ('/@' if on_zfs else '',
GRUB2BootLoader.CUSTOM_CFG_BASENAME)
stub_custom_cfg = self._generate_stub_cfg_file(target,
GRUB2BootLoader.CUSTOM_CFG_BASENAME, basepath,
'if [ -s %s ]; then\n'
'\tsource %s\n'
'fi\n' % (custom_name, custom_name), True)
if basepath:
return [stub_grub_cfg, stub_custom_cfg]
def _generate_grub_cfg_file(self, target, basepath, defs_dict, outfile):
"""Create and populate the grub.cfg file"""
# Before we can invoke grub-mkconfig, we need to transform
# the boot loader properties into a grub defaults file,
# then set the BOOTMGMT_GRUB2_DEFAULTS environment
# variable to its path. (The grub-mkconfig program will source
# etc/default/grub, which will source the generated defaults
# file if the environment variable is set.)
grub_defs_path = None
try:
reset_env = False
if defs_dict:
# The resulting grub.cfg from _internal_mkconfig is designed
# to work on all targets
self._internal_mkconfig(target, defs_dict, outfile)
else:
grub_defs_path = self._generate_grub_defs(target, basepath)
os.environ['BOOTMGMT_GRUB2_DEFAULTS'] = grub_defs_path
reset_env = True
self._invoke_grub_mkconfig(target, outfile)
outfile.close()
except BootmgmtConfigWriteError:
try:
os.remove(outfile.name)
except OSError:
pass
raise
finally:
if reset_env:
del os.environ['BOOTMGMT_GRUB2_DEFAULTS']
if (basepath and grub_defs_path and
not getattr(self, 'preserve_defs', False)):
try:
os.remove(grub_defs_path)
except OSError:
pass
# Not a fatal error
# pylint: disable=R0912
def _generate_grub_cfg(self, target, basepath, defs_dict=None,
filesuffix=''):
"""Writes the grub.cfg file to the appropriate location. If
defs_dict is not None, internal grub-mkconfig is used.
"""
dest_path = ''
if basepath:
boot_class = self._boot_config.boot_class
if boot_class == BootConfig.BOOT_CLASS_NET:
base_grub_cfg = GRUB2BootLoader.GRUB_CFG_BASENAME + filesuffix
dest_path = os.path.join(
self._boot_config.net_tftproot_subdir, base_grub_cfg)
outfilename = os.path.join(basepath, base_grub_cfg)
else:
dest_path = '/' + GRUB2BootLoader.BOOT_GRUB_SUBDIR + '/' + \
GRUB2BootLoader.GRUB_CFG_BASENAME
outfilename = os.path.join(basepath, dest_path[1:])
else:
final_filename = os.path.join(self.config_dir,
GRUB2BootLoader.GRUB_CFG_BASENAME)
outfilename = final_filename + '.new'
try:
self._make_basedir(outfilename)
outfile = file(outfilename, 'w')
except IOError as err:
raise BootmgmtConfigWriteError('Error during grub.cfg generation: '
"Couldn't create %s" % outfilename, err)
except BootmgmtConfigWriteError:
raise
self._generate_grub_cfg_file(target, basepath, defs_dict, outfile)
if basepath:
token = self._get_root_token()
if token:
final_path = '%(' + token + ')s/'
else:
final_path = '/'
final_path += dest_path.lstrip('/')
return [(BootConfig.OUTPUT_TYPE_FILE,
outfilename,
None,
final_path,
'root',
'root',
GRUB2BootLoader.GRUB_CFG_PERMS)]
else:
self._generate_stub_files(target)
self._move_temp_to_grub_cfg(outfilename, final_filename)
# pylint: enable=R0912
def _get_root_token(self):
"""Returns the token to use when constructing tuples returned from
install().
"""
token = None
if isinstance(self._boot_config, DiskBootConfig):
fstype = getattr(self._boot_config, 'boot_fstype', None)
if fstype == 'zfs':
token = DiskBootConfig.TOKEN_ZFS_RPOOL_TOP_DATASET
elif fstype == 'ufs':
token = BootConfig.TOKEN_SYSTEMROOT
elif isinstance(self._boot_config, ODDBootConfig):
token = ODDBootConfig.TOKEN_ODD_ROOT
elif isinstance(self._boot_config, NetBootConfig):
token = NetBootConfig.TOKEN_TFTP_ROOT
else:
self._debug('Unknown BootConfig type -- not using a token')
return token
def _move_temp_to_grub_cfg(self, src, dest):
"""Moves the temporary grub.cfg.new to grub.cfg.
"""
try:
shutil.move(src, dest)
self._debug('grub.cfg moved into place as %s', dest)
except IOError as err:
try:
os.remove(src)
except OSError as oserr:
self._debug('Error while trying to remove %s: %s',
(src, oserr))
raise BootmgmtConfigWriteError("Couldn't move %s to %s" %
(src, dest), err)
# Move was successful, so now set the owner and mode properly:
try:
os.chmod(dest, GRUB2BootLoader.GRUB_CFG_PERMS)
os.chown(dest, pwd.getpwnam('root').pw_uid,
grp.getgrnam('root').gr_gid)
except OSError as oserr:
raise BootmgmtConfigWriteError("Couldn't set mode/perms on %s"
% dest, oserr)
# pylint: disable=R0912
def _invoke_grub_mkconfig(self, target, outfile):
"""Invokes grub-mkconfig and directs its output to the outfile.
"""
grub_mkconfig = [os.path.join('/',
GRUB2BootLoader.GRUB_PLATFORM_PREFIXES[target],
GRUB2BootLoader.GRUB_MKCONFIG),
'--prefix=%s' % os.path.join(self.config_dir,
GRUB2BootLoader.GRUB_BOOT_SUBDIRS[target])]
try:
self._execute_cmd(grub_mkconfig,
'Generating %s boot loader configuration files' % target,
'%s grub.cfg generation' % target, outfile)
finally:
outfile.close()
# pylint: enable=R0912
# pylint: disable=R0914
def _generate_grub_defs(self, target, basepath):
"""Generate the GRUB2 defaults file either in the specified
path or in the system location. Returns the path to the defaults
file. This is suitable only for DiskBootConfig.
May raise BootmgmtConfigWriteError if there was a problem creating the
defaults file (or its parent directory).
"""
if basepath:
dirpath = os.path.join(basepath, GRUB2BootLoader.BOOT_GRUB_SUBDIR)
else:
dirpath = self.config_dir
defs_path = os.path.join(dirpath,
GRUB2BootLoader.GRUB_DEFS_PATH_FMT % target)
self._make_basedir(defs_path)
try:
defs_file = open(defs_path, 'w')
except IOError as ioerr:
raise BootmgmtConfigWriteError('Error writing GRUB2 defaults',
ioerr)
for key, value in self._generate_grub_defs_dict(target).items():
print >> defs_file, '%s=\'%s\'' % (key, value)
defs_file.close()
return defs_path
# pylint: enable=R0914
def _gen_grub_defs_dict_cons_gfx(self):
"""Generate a dict with gfxterm defaults.
"""
defdict = {}
splash = self.getprop(BootLoader.PROP_SPLASH)
# If splash is None, the property does not exist, so use a
# default
if splash is None:
splash = GRUB2BootLoader.DEFAULT_GRUB_SPLASH
if len(splash) > 1:
if splash[0] == '/':
splash = splash[1:]
# We have to get the basename of the splashimage because
# we know it will be copied into the config_dir.
splash = os.path.join(self.config_dir, os.path.basename(splash))
defdict['GRUB_BACKGROUND'] = splash
fore = self.getprop(BootLoader.PROP_FORECOLOR)
# If fore is None, the property does not exist, so use a default
if fore is None:
fore = GRUB2BootLoader.DEFAULT_FORECOLOR
if len(fore) > 0:
forecolor = self._grub2_color(fore)
defdict['GRUB_FORECOLOR'] = forecolor
back = self.getprop(BootLoader.PROP_BACKCOLOR)
# If back is None, the property does not exist, so use a default
if back is None:
back = GRUB2BootLoader.DEFAULT_BACKCOLOR
if len(back) > 0:
backcolor = self._grub2_color(back)
# The background color must have a 0 alpha so the
# splash image comes through.
defdict['GRUB_BACKCOLOR'] = '%s,0' % backcolor
# If a theme is to be used, it was previously copied into the
# proper place in _assemble_other_grub_files(). If theme_grub_path
# is non-None, use it to set the GRUB_THEME_DIRECT property; if
# theme_os_path is set, use it to set the GRUB_THEME property.
if self.theme_grub_path:
defdict['GRUB_THEME_DIRECT'] = self.theme_grub_path
if self.theme_os_path:
defdict['GRUB_THEME'] = self.theme_os_path
return defdict
def _generate_grub_defs_dict_cons(self, target):
"""Generate a dictionary with grub defaults pertaining to the console.
"""
defdict = {}
# Regardless of the type of the GRUB2 console, always set the video
# backend; Solaris uses a framebuffer console by default and GRUB2
# may be called upon to set the graphics mode.
video_backend = GRUB2BootLoader.VIDEO_BACKENDS.get(target)
if video_backend:
defdict['GRUB_VIDEO_BACKEND'] = video_backend
# The gfxpayload modelist should be set regardless of the GRUB2
# console type also, since we may have Solaris instances that use
# framebuffer console that need this variable set in a specific
# part of the menu entry
defdict['GRUB_GFXPAYLOAD_MODE'] = GRUB2BootLoader.GFX_MODELIST
defdict['GRUB_GFXMODE'] = GRUB2BootLoader.GFX_MODELIST
cons = self.getprop(BootLoader.PROP_CONSOLE)
if cons:
if cons == BootLoader.PROP_CONSOLE_TEXT:
# 'console' is the default GRUB2 input terminal type. gfxterm
# is the default output terminal type if none are specified.
interm = None
outterm = 'console'
elif cons == BootLoader.PROP_CONSOLE_GFX:
interm = None
outterm = 'gfxterm'
defdict.update(self._gen_grub_defs_dict_cons_gfx())
elif cons == BootLoader.PROP_CONSOLE_SERIAL:
interm = 'serial'
outterm = 'serial'
serial_params = self.getprop(BootLoader.PROP_SERIAL_PARAMS)
serial_cmd = self._grub2_serial_cmd(serial_params)
defdict['GRUB_SERIAL_COMMAND'] = serial_cmd
else:
self._debug('Unknown console property: %s', cons)
interm = None
outterm = None
if interm:
defdict['GRUB_TERMINAL_INPUT'] = interm
if outterm:
defdict['GRUB_TERMINAL_OUTPUT'] = outterm
return defdict
def _generate_grub_defs_dict(self, target):
"""Generate a dictionary of grub default keys and their values.
"""
defdict = {}
defdict.update(self._generate_grub_defs_dict_cons(target))
timeout = self.getprop(BootLoader.PROP_TIMEOUT)
if timeout == '':
timeout = '0'
elif timeout is None:
timeout = GRUB2BootLoader.DEFAULT_TIMEOUT
defdict['GRUB_TIMEOUT'] = timeout
quiet = self.getprop(BootLoader.PROP_QUIET)
if (quiet and type(quiet) == bool) or parse_bool(quiet):
defdict['GRUB_HIDDEN_TIMEOUT'] = 'true' # Must be lower case
# Set the default entry based on the list of default boot instances
if self._boot_config and self._boot_config.boot_instances:
default_idxs = [idx for idx, x in
enumerate(self._boot_config.boot_instances)
if getattr(x, 'default', False) == True]
if default_idxs:
defdict['GRUB_DEFAULT'] = str(default_idxs[0])
return defdict
@staticmethod
def _grub2_color(color):
"""Convert a string of hex digits into a GRUB2 color specification.
"""
if len(color) != 6:
color = ('0' * (6 - len(color))) + color
red = color[0:2]
green = color[2:4]
blue = color[4:6]
return '0x%s,0x%s,0x%s' % (red, green, blue)
def _grub2_serial_cmd(self, params):
"""Convert the serial parameters property tuple into a GRUB2 serial
command. If params is None, the default serial command is used.
The form of the serial_params property is:
serial_params | A tuple containing (<portspec>,<speed>,<d>,
| <p>,<s>,<f>).
| <portspec> is currently defined to be a
| number (valid values depend on the platform,
| but '0' is ttya (com1) and '1' is ttyb (com2)).
| Serial console parameters (<d>=data bits,
| <p>=parity ('N','E','O'),<s>=stop bits ('0','1'),
| <f>=flow control ('H','S',None) for hardware,
| software, or none). The default is:
| ('0',None,None,None,None,None).
"""
parity_dict = {'N': 'no', 'E': 'even', 'O': 'odd'}
cmd = 'serial'
if params is None:
self._debug('Serial parameters are None: Using defaults')
return cmd + ' --unit 0'
if not params or type(params) != tuple:
self._debug('Invalid serial parameters: %s', str(params))
return cmd
if len(params) > 0 and params[0] is not None:
# If the port number is larger than 0xFF, assume it's an
# IO port number and use --port instead of --unit:
if int(params[0]) > 0xFF:
cmd += ' --port %s' % hex(int(params[0]))
else:
cmd += ' --unit %s' % params[0]
if len(params) > 1 and params[1] is not None:
cmd += ' --speed %s' % params[1]
if len(params) > 2 and params[2] is not None:
cmd += ' --word %s' % params[2]
if len(params) > 3 and params[3] is not None:
cmd += ' --parity %s' % parity_dict.get(params[3], 'no')
if len(params) > 4 and params[4] is not None:
cmd += ' --stop %s' % params[4]
return cmd
def _assemble_grub_theme_files(self, basepath, theme_file):
"""Copy all files associated with the theme whose theme
file is specified into the proper location. Returns a 3-tuple of
(1) tuples (if basepath is set) (or None if not), and (2)
the *GRUB path* to the theme_file (the GRUB path will be used
directly in the grub.cfg file) and (3) The OS path to the
theme_file.
"""
tuple_list = []
source_dir = os.path.dirname(os.path.join(
self._get_boot_loader_data_root(), theme_file))
theme_name = os.path.basename(source_dir)
theme_file_basename = os.path.basename(theme_file)
grub_path = '/' + GRUB2BootLoader.BOOT_GRUB_THEME_SUBDIR + '/' + \
theme_name + '/' + theme_file_basename
if (self._boot_config.boot_class == BootConfig.BOOT_CLASS_DISK and
self._boot_config.boot_fstype == 'zfs'):
grub_path_prefix = '/@'
grub_path = grub_path_prefix + grub_path
else:
grub_path_prefix = ''
self._debug('THEME: src=%s, theme=%s, theme_bn=%s, grub_path=%s ',
(source_dir, theme_name, theme_file_basename, grub_path))
if basepath:
dest_dir = os.path.join(basepath,
GRUB2BootLoader.BOOT_GRUB_THEME_SUBDIR)
dest_theme_dir = os.path.join(dest_dir, theme_name)
self._make_dirtree(dest_dir)
theme_fonts_set = set()
theme_mods_set = set()
def collect_tuples(dirnm, listing):
"dirnm is the source theme's directory"
for entry in listing:
source_file = os.path.join(dirnm, entry)
if not os.path.isfile(source_file):
continue
destcomponent = source_file.replace(source_dir + '/', '', 1)
root_rel_path = os.path.join('/',
GRUB2BootLoader.BOOT_GRUB_THEME_SUBDIR, theme_name,
destcomponent)
newtup = (BootConfig.OUTPUT_TYPE_FILE,
os.path.join(dest_theme_dir, destcomponent),
None,
'%(' + self._get_root_token() + ')s' +
root_rel_path,
'root', 'bin', 0644)
self._debug('THEME: New tuple: %s', str(newtup))
tuple_list.append(newtup)
extension = source_file.rsplit('.', 1)
if len(extension) > 1:
extn = extension[1]
modname = GRUB2BootLoader.EXTN_TO_MODULE.get(extn)
if modname:
theme_mods_set.add(modname)
elif extn in GRUB2BootLoader.FONT_EXTN_LIST:
fontpath = os.path.join(grub_path_prefix,
root_rel_path)
theme_fonts_set.add(fontpath)
return []
# We're doing a directory copy of an entire theme. If the
# destination exists, blow it away first, or the copytree will fail:
shutil.rmtree(dest_theme_dir, ignore_errors=True)
# Iterate through the source directory, using collect_tuples to
# collect the list of file tuples returned to the caller.
try:
self._debug('THEME: Copying %s -> %s', (source_dir,
dest_theme_dir))
shutil.copytree(source_dir, dest_theme_dir,
ignore=collect_tuples)
except (shutil.Error, OSError) as copyerr:
self._debug('Error during theme copy: %s', copyerr)
shutil.rmtree(dest_theme_dir, ignore_errors=True)
raise
# The OS path to the theme file is the destination path
os_path = os.path.join(dest_theme_dir, theme_file_basename)
self.theme_mods_set = theme_mods_set
self.theme_fonts_set = theme_fonts_set
return tuple_list, grub_path, os_path
if self._boot_config.boot_class != BootConfig.BOOT_CLASS_DISK:
return None, None, None
dest_dir = os.path.join(self._config_dir_disk(),
GRUB2BootLoader.THEME_SUBDIR)
self._make_dirtree(dest_dir)
dest_theme_dir = os.path.join(dest_dir, theme_name)
# If the source directory does not exist, but the destination DOES,
# check to see if the theme file exists, and if it does, just use the
# existing theme in the destination. This covers the case where
# pybootmgmt is used in conjunction with a data source that does not
# include the specified theme.
# The OS path to the theme file is the destination path
os_path = os.path.join(dest_theme_dir, theme_file_basename)
if os.path.exists(source_dir):
# We're doing a directory copy of an entire theme. If the
# destination exists, blow it away first, or the copytree will fail:
shutil.rmtree(dest_theme_dir, ignore_errors=True)
try:
self._debug('THEME: Copying %s -> %s', (source_dir,
dest_theme_dir))
shutil.copytree(source_dir, dest_theme_dir)
except (shutil.Error, OSError) as copyerr:
self._debug('Error during theme copy: %s', copyerr)
shutil.rmtree(dest_theme_dir, ignore_errors=True)
raise
elif not os.path.exists(os_path):
self._debug('Could not fall back to existing theme in %s',
os_path)
return None, None, None
else:
self._debug('Falling back to existing theme in %s',
os_path)
return None, grub_path, os_path
def _assemble_other_grub_files(self, basepath):
"""Copy other needed GRUB2 files into the proper location.
Return the appropriate tuples (if basepath is set).
"""
tuple_list = self._assemble_grub_files_generic(basepath,
GRUB2BootLoader.GRUB_OTHERS)
# Assemble the theme files, if necessary
constype = self.getprop(BootLoader.PROP_CONSOLE)
theme_tuples = None
if constype == BootLoader.PROP_CONSOLE_GFX:
default_theme = False
if BootLoader.PROP_THEME in self._bl_props:
themename = self.getprop(BootLoader.PROP_THEME)
# theme name alphanumeric validation was done in BootLoader
if themename:
theme_file = GRUB2BootLoader.THEME_TEMPLATE % themename
else:
theme_file = None
else: # No theme property, so default to enable themeing with
# the default theme.
theme_file = GRUB2BootLoader.THEME_DEFAULT_PATH
default_theme = True
try:
if theme_file:
theme_tuples, self.theme_grub_path, self.theme_os_path = \
self._assemble_grub_theme_files(basepath, theme_file)
except (IOError, OSError) as copyerr:
self._debug('Exception while assembling theme files: %s',
copyerr)
# If this is the default theme and we hit an error, quietly
# revert to non-themed graphical menus.
if not default_theme:
raise
if tuple_list:
if theme_tuples:
tuple_list.extend(theme_tuples)
return tuple_list
if theme_tuples:
return theme_tuples
def _assemble_grub_files_generic(self, basepath, file_tuples):
"""Using the list of 3-tuples passed in file_tuples, copy those files
to proper destination with the proper modes. This is suitable for
DiskBootConfig and ODDBootConfig only.
"""
tuple_list = []
for src, dest, perms in file_tuples:
src = os.path.join(self._get_boot_loader_data_root(), src)
if basepath:
destdir = os.path.join(basepath,
GRUB2BootLoader.BOOT_GRUB_SUBDIR)
else:
destdir = self.config_dir
fulldest = os.path.join(destdir, dest)
self._debug('src = %s, dest = %s', (src, fulldest))
self._make_basedir(fulldest)
# If perms is None in the 3-tuple
if not perms:
perms = GRUB2BootLoader.GRUB_OTHERS_PERM_DEFAULTS
try:
shutil.copy(src, fulldest)
except (IOError, shutil.Error) as copyerr:
self._debug('Could not copy %s for GRUB2: %s', (src, copyerr))
continue
# Skip the chmod/chown if we're writing to an intermediate
# location
if not basepath:
try:
os.chmod(fulldest, perms[2])
os.chown(fulldest, pwd.getpwnam(perms[0]).pw_uid,
grp.getgrnam(perms[1]).gr_gid)
except OSError as oserr:
raise BootmgmtConfigWriteError("Couldn't set mode/perms on"
" %s" % fulldest, oserr)
else:
token = self._get_root_token()
if token:
final_path = '%(' + token + ')s/'
else:
final_path = '/'
final_path += GRUB2BootLoader.BOOT_GRUB_SUBDIR + '/' + dest
tuple_list.append((BootConfig.OUTPUT_TYPE_FILE, fulldest, None,
final_path, perms[0], perms[1], perms[2]))
if tuple_list:
return tuple_list
def _internal_mkconfig(self, target, defs_dict, outfile):
"""A stripped-down barebones implementation of a GRUB2 menu generator.
First, a set of header script code is emitted (its contents depends on
the values in the defs_dict (a dict representation of the
$prefix/etc/default/grub file), followed by the menuentries that
correspond to the boot configuration's boot_instances list.
"""
outfile.write('# GRUB2 configuration file\n\n')
emitter = GRUB2MenuEntryEmitter(outfile, defs_dict)
self._internal_mkconfig_header(defs_dict, outfile)
for bootinst in self._boot_config.boot_instances:
emitter.write_menuentry(bootinst, target)
outfile.write('\n')
if 'NO_CUSTOM_CFG' not in defs_dict:
outfile.write('\n'
'if [ -f /boot/grub/custom.cfg ]; then\n'
'\tsource /boot/grub/custom.cfg\n'
'fi\n')
@staticmethod
def _grub_cfg_preamble(defs_dict):
"""Returns the list of lines in the script preamble stored in
the definitions dictionary passed in.
"""
preamble = defs_dict.get('__SCRIPT_PREAMBLE')
if preamble:
if isinstance(preamble, (list, tuple)):
return preamble
elif isinstance(preamble, basestring):
return preamble.split('\n')
return []
# pylint: disable=R0912
def _internal_mkconfig_header(self, defs_dict, outfile):
"""Output the grub.cfg header for this target based on the defs_dict.
"""
lines = []
# If there is a preamble specified in the defs_dict, add that first:
lines.extend(self._grub_cfg_preamble(defs_dict))
# First, output the script for initializing the console device(s):
# load_video_$target is defined in the stub grub.cfg files (or the
# grub.cfg directly for NetBootConfig).
# NOTE: We always load video drivers because Solaris may need it
# for framebuffer console.
lines.append('load_video_$target')
outdev = defs_dict.get('GRUB_TERMINAL_OUTPUT')
indev = defs_dict.get('GRUB_TERMINAL_INPUT')
initted_devs = []
if outdev:
outdevs = outdev.split()
# Initting gfxterm must be done after all other console devices
# are initted because the terminal_output statement is embedded
# in the gfxterm init script block.
if 'gfxterm' in outdevs:
outdevs.remove('gfxterm')
do_gfxterm = True
else:
do_gfxterm = False
for dev in outdevs:
conslines = self._mkcfg_consdev(dev, outdev, defs_dict)
lines.extend(conslines)
initted_devs += [dev]
if do_gfxterm:
conslines = self._mkcfg_consdev('gfxterm', outdev, defs_dict)
lines.extend(conslines)
initted_devs += 'gfxterm'
else:
lines.append('terminal_output %s' % outdev)
if indev:
for dev in indev.split():
if dev in initted_devs:
continue
conslines = self._mkcfg_consdev(dev, indev, defs_dict, False)
lines.extend(conslines)
initted_devs += dev
lines.append('terminal_input %s' % indev)
if lines[-1] != '':
lines.append('')
# Now add the timeout and quiet script:
quiet = defs_dict.get('GRUB_HIDDEN_TIMEOUT')
timeout = defs_dict.get('GRUB_TIMEOUT')
if quiet:
lines.append('if sleep --verbose --interruptible %s; then' %
timeout)
lines.append('\tset timeout=0')
lines.append('fi')
else:
lines.append('set timeout=%s' % timeout)
lines.append('')
# finally, the default:
default = defs_dict.get('GRUB_DEFAULT')
if default:
lines.append('set default="%s"' % default)
lines.append('')
for line in lines:
outfile.write(line)
outfile.write('\n')
def _mkcfg_consdev(self, dev, alldevs, defs_dict, outdev=True):
"""Return a list of lines that init the specified terminal device
"""
lines = []
if dev == 'gfxterm' and outdev:
# The caller knows that gfxterm must be the last console device
# processed (due to the use of terminal_output statements)
# We have several potential properties to handle here:
# GRUB_VIDEO_BACKEND, GRUB_BACKGROUND, GRUB_FORECOLOR, and
# GRUB_BACKCOLOR
modes = defs_dict.get('GRUB_GFXMODE')
backimg = defs_dict.get('GRUB_BACKGROUND')
forecolor = defs_dict.get('GRUB_FORECOLOR')
backcolor = defs_dict.get('GRUB_BACKCOLOR')
lines.append('insmod gfxterm')
if modes:
lines.append("set gfxmode='%s'" % modes)
loaded_mods = []
# Add GRUB theme dependencies here:
themefile = defs_dict.get('GRUB_THEME_DIRECT')
if themefile:
# Emit module load lines for all required modules:
if self.theme_mods_set:
for modname in self.theme_mods_set:
lines.append('insmod %s' % modname)
loaded_mods.append(modname)
if self.theme_fonts_set:
for fontpath in self.theme_fonts_set:
lines.append('if ! loadfont %s ; then' % fontpath)
lines.append('\tset theme_dep_load_failed=1')
lines.append('fi')
# If loading the default font fails, it's not fatal
lines.append('if [ x$theme_dep_load_failed = x ] ; then')
lines.append('\tloadfont /boot/grub/unicode.pf2')
lines.append('\tset theme=%s' % themefile)
lines.append('\tterminal_output %s' % alldevs)
lines.append('else')
indent = '\t'
need_fi = True
else:
indent = ''
need_fi = False
# Add the font-load conditional statement:
# XXX - Revisit the hard-coded font path here?
lines.append(indent + 'if loadfont /boot/grub/unicode.pf2 ; then')
lines.append(indent + '\tterminal_output %s' % alldevs)
# fore/back color must be set BEFORE the background image
if forecolor:
lines.append(indent + '\tforeground_color %s' % forecolor)
if backcolor:
lines.append(indent + '\tbackground_color %s' % backcolor)
if backimg:
extn = backimg.rsplit('.', 1)
if len(extn) > 1:
imgmod = GRUB2BootLoader.EXTN_TO_MODULE.get(extn[1])
if imgmod and imgmod not in loaded_mods:
lines.append(indent + '\tinsmod %s' % imgmod)
# Loading the background image requires us to first load
# modules needed to access the filesystem on which it's stored
lines.append(indent + '\tbackground_image -m stretch %s' %
backimg)
# Close the conditional
alldevs_minus_gfxterm = alldevs.replace('gfxterm', '').strip()
if alldevs_minus_gfxterm:
lines.append(indent + 'else')
lines.append(indent + '\tterminal_output %s' %
alldevs_minus_gfxterm)
lines.append(indent + 'fi')
if need_fi: # If we need a fi (if theme code was emitted above)
lines.append('fi')
elif dev == 'serial':
serialcmd = defs_dict.get('GRUB_SERIAL_COMMAND')
if serialcmd:
lines.append(serialcmd)
else:
self._debug('No serial command specified -- using default')
lines.append('serial') # default serial command
return lines
# pylint: enable=R0912
def _write_config_odd(self, basepath):
"""To generate the grub.cfg file for an ODDBootConfig, we create the
menu by hand (we do NOT use the grub-mkconfig program).
"""
if not basepath:
raise BootmgmtInterfaceCodingError('basepath must be specified for '
'ODDBootConfig')
target_list = self._get_target_list()
tuple_list = []
if target_list:
of_tuples = self._assemble_other_grub_files(basepath)
if of_tuples:
tuple_list.extend(of_tuples)
generated_grub_cfg = False
for target in target_list:
loader_tuples = self._create_grub2_image(target, basepath)
defs_dict = self._generate_grub_defs_dict(target)
if not generated_grub_cfg:
# It doesn't matter which target's defs_dict we use here
# because _generate_grub_cfg is guaranteed to generate a
# config file without direct target-specific code (there may
# be references to things defined in the target-specific stubs
# though)
gcfg_tuples = self._generate_grub_cfg(target, basepath,
defs_dict)
if gcfg_tuples:
tuple_list.extend(gcfg_tuples)
generated_grub_cfg = True
# The stub files are the small script files that do target-specific
# things, then source the main config file
stub_list = self._generate_stub_files(target, basepath,
def_dict=defs_dict)
if stub_list:
tuple_list.extend(stub_list)
if loader_tuples:
tuple_list.extend(loader_tuples)
return tuple_list
def _write_config_net(self, basepath):
"""To generate the grub.cfg file for a NetBootConfig, we create the
menu by hand (we do NOT use the grub-mkconfig program). Since we will
not use a graphical console while network booting, we do not need
to assemble other files (i.e. GRUB2 font files and splash screen image).
"""
if not basepath:
raise BootmgmtInterfaceCodingError('basepath must be specified for '
'NetBootConfig')
target_list = self._get_target_list()
tuple_list = []
menu_conf_tuple_list = self._write_menu_conf(basepath)
if menu_conf_tuple_list:
tuple_list.extend(menu_conf_tuple_list)
preamble = \
"""# begin preamble
regexp ".*/(.*)" $prefix -s 1:target
tr -s target - _ "$target"
if [ -z "$target" -o "$target" = "%(bios)s" ]; then
set multiboot=multiboot
set module=module
elif [ "$target" = "%(uefi64)s" ]; then
set multiboot=multiboot2
set module=module2
fi
""" % GRUB2BootLoader.GRUB_TARGET_STRINGS
target_dicts = {}
# Generate the target def dictionaries and the video driver load
# code so we can use them in the loop below.
for target in target_list:
target_dicts[target] = self._generate_grub_defs_dict(target)
preamble += self._generate_grub2_load_video_func(target,
target_dicts[target]) + "\n"
preamble += "# end preamble"
generated_grub_cfg = False
for target in target_list:
loader_tuples = self._create_grub2_image(target, basepath)
if loader_tuples:
tuple_list.extend(loader_tuples)
if not generated_grub_cfg:
# It doesn't matter which target's defs_dict we use here
# because _generate_grub_cfg is guaranteed to generate a
# config file without direct target-specific code.
defs_dict = target_dicts[target]
# Elide the final stanza that searches for the custom.cfg --
# we have no use for it in a network boot scenario.
defs_dict['NO_CUSTOM_CFG'] = True
# Add the script preamble, which establishes the platform
# on which the script is executing and sets some key variables
# that the generated grub configuration file uses.
defs_dict['__SCRIPT_PREAMBLE'] = preamble
gcfg_tuples = self._generate_grub_cfg(target, basepath,
defs_dict, self._boot_config.pxe_suffix())
if gcfg_tuples:
# The grub_cfg tuples must be marked specially, so that
# the consumer knows that they can be placed in the
# tftp boot loader search path
config_tuples = []
for tupl in gcfg_tuples:
lst = list(tupl)
if lst[BootConfig.IDX_FILETYPE] == \
BootConfig.OUTPUT_TYPE_FILE:
lst[BootConfig.IDX_FILETYPE] = \
BootConfig.OUTPUT_TYPE_NETCONFIG
config_tuples.append(tuple(lst))
tuple_list.extend(config_tuples)
generated_grub_cfg = True
return tuple_list
# Override install so we can update the UEFI boot variables with the
# device list passed in. Failure to set the boot variables is NOT a
# fatal error; it will, however, be logged to syslog.
def install(self, location, force=False, verbose_file=None, platdict=None):
"""
"""
tuple_list = super(GRUB2BootLoader, self).install(location, force,
verbose_file, platdict)
# No need to go further if we have tuples to return (no install
# was done to an actual device) or if location is empty
if tuple_list or not location:
return tuple_list
# If this is not a disk-based boot configuration, or if there were
# tuples returned from the main install() method (which implies we
# did not install onto actual physical devices) or if the system
# firmware is not UEFI, just return the tuple_list, if any.
if (self._boot_config.boot_class != BootConfig.BOOT_CLASS_DISK or
self.firmware is None or 'uefi' not in self.firmware.fw_name):
self._debug('No attempt will be made to set the boot order')
return None
try:
self.firmware.setprop(SystemFirmware.PROP_BOOT_DEVICE, location)
self._debug('UEFI boot variables successfully updated')
except BootmgmtError as bmerr:
self._debug('Error updating system boot device list: %s',
str(bmerr))
syslog.openlog('pybootmgmt-GRUB2', 0, syslog.LOG_DAEMON)
syslog.syslog(syslog.LOG_WARNING, 'Failed to set the UEFI boot '
'device to %s. Manual intervention may be required '
'to boot Solaris when the system restarts.' % str(location))
syslog.closelog()
return None
def _write_loader(self, devname, data_root, force, verbose_file, platdict):
"""Write the GRUB2 boot loader to disk. We support 4 scenarios:
(1) BIOS systems, installation onto a DOS-partitioned disk with
a Solaris partition -- boot loader is embedded in the Solaris
boot slice (usually slice 8, but really it's any slice that
includes cylinder 0). The devname passed in can be directly
passed to grub-install, as grub-setup has been modified to write
the loader to the appropriate place in the Solaris partition;
(2) BIOS systems, installation onto a GPT-partitioned disk. A
BIOS Boot Partition (BBP) is required. The devname passed in
refers to the partition that includes the ZFS, not the BBP, so
we need to figure out if we're on a GPT-partitioned disk, and
pass the whole-disk node (p0) to grub-install so *IT* can
locate the BBP and install GRUB2 there;
(3) UEFI systems, installation onto a DOS-partitioned disk. The
device node refers to a VTOC slice, so we need to get the device
node for the EFI System Partition, mount it (if it's not already
mounted), and invoke grub-install, telling it to install the EFI
boot application under EFI/ORACLE/ on the ESP;
(4) UEFI systems, installation onto a GPT-partitioned disk.
Similar to (3).
"""
if not isinstance(self._boot_config, DiskBootConfig):
self._debug('Ignoring _write_loader() for non-DiskBootConfig')
return
if not ((len(devname) > 2 and devname[-2] == 's' and
devname[-1].isdigit())
or
(len(devname) > 3 and devname[-3] == 's' and
devname[-2:].isdigit())):
raise BootLoaderInstallError('Device node is not a slice: ' +
devname)
target = self.firmware.fw_name
prefixdir = os.path.join(self.config_dir,
GRUB2BootLoader.GRUB_BOOT_SUBDIRS[target])
fulldatadir = os.path.join(data_root,
GRUB2BootLoader.GRUB_DATA_PATHS[target])
# The prefixdir MUST exist, otherwise grub-install will fail:
self._make_dirtree(prefixdir)
grub_inst_cmd = [os.path.join('/',
GRUB2BootLoader.GRUB_PLATFORM_PREFIXES[target],
GRUB2BootLoader.GRUB_INSTALL), '--no-floppy',
'--grub-directory=%s' % prefixdir,
'--pkglibdir=%s' % fulldatadir]
preloads = GRUB2BootLoader.PRELOADED_MODULES[target]
if preloads:
grub_inst_cmd.append('--modules=%s' % ' '.join(preloads))
if force:
grub_inst_cmd.append('--no-check-versioning')
if verbose_file:
grub_inst_cmd.append('--debug')
if (isinstance(platdict, dict) and platdict.get(BootLoader.PLATOPT_MBR)
and target == 'bios'):
grub_inst_cmd.append('--force-mbr')
base_devname = devname[:devname.rfind('s')]
wholedisk = base_devname + 'p0'
is_gpt = is_gpt_disk(wholedisk)
if target == 'bios':
if is_gpt:
instdev = wholedisk
else:
instdev = devname
elif target == 'uefi64':
# We need to derive the EFI System Partition from the device
# passed in, mount it (if it's not already mounted), and pass
# the mountpoint as the instdev
esp_dev = find_efi_system_partition(devname, is_gpt)
if not esp_dev:
raise BootLoaderInstallError('Could not determine the EFI '
'System Partition (ESP) for device %s' % devname)
esp_mountpt = None
esp_tempdir = None
try:
mnttab_open()
mntentdict = getmntany(mnt_special=esp_dev)
if mntentdict:
esp_mountpt = mntentdict['mnt_mountp']
mnttab_close()
except IOError as ioerr:
# If there was a problem trying to determine if the ESP is
# already mounted, proceed anyway -- we'll fail during the
# mount if it was already mounted.
pass
if not esp_mountpt:
try:
esp_tempdir = tempfile.mkdtemp(dir='/system/volatile')
except IOError as ioerr:
raise BootLoaderInstallError('Error while trying to create '
'a temporary dir for mounting the ESP', ioerr)
esp_mountpt = esp_tempdir
mount_cmd = ['/usr/lib/fs/pcfs/mount', esp_dev, esp_mountpt]
try:
self._execute_cmd(mount_cmd)
except BootmgmtError:
try:
os.rmdir(esp_tempdir)
except OSError:
pass
raise BootLoaderInstallError('Error while trying to mount '
'the EFI System Partition (%s)' % esp_dev)
need_unmount = True
else:
need_unmount = False
instdev = esp_mountpt + '/EFI/ORACLE'
default_bootapp = esp_mountpt + '/efi/boot/bootx64.efi'
oracle_bootapp = instdev + '/grubx64.efi'
try:
default_statinfo = os.stat(default_bootapp)
except OSError:
default_statinfo = None
if default_statinfo is None:
copy_efi_boot_to_default = True
else:
copy_efi_boot_to_default = False
try:
if filecmp.cmp(default_bootapp, oracle_bootapp,
shallow=False):
copy_efi_boot_to_default = True
except OSError:
pass
else:
raise BootmgmtUnsupportedPlatformError(target +
' is not supported')
grub_inst_cmd.append(instdev)
cmd_succeeded = True # Optimism
try:
if verbose_file:
print >> verbose_file, 'Output from boot loader ' \
'installation command "%s" is:' % ' '.join(grub_inst_cmd)
cmdout, cmderr = self._execute_cmd(grub_inst_cmd,
return_stdout=True, return_stderr=True)
print >> verbose_file, '<STDOUT>:\n%s' % cmdout,
print >> verbose_file, '<STDERR>:\n%s' % cmderr,
print >> verbose_file, '<END OF OUTPUT>'
else:
self._execute_cmd(grub_inst_cmd, 'Installing boot loader',
'Boot loader installation')
except BootmgmtConfigWriteError as bmwerr:
cmd_succeeded = False
# If we're not able to install the bootloader due to
# incompatibility between the GRUB2 utilities and the modules
# from the data directory, begin the deferred boot loader
# activation process by creating the required file in the
# root directory of the data_root.
if bmwerr.retcode == GRUB2BootLoader.EXIT_CODE_INCOMPATIBLE:
defer_file = os.path.join(data_root,
GRUB2BootLoader.DEFERRED_ACTIVATION_FILENAME)
try:
open(defer_file, 'w').close()
except IOError as ioerr:
raise BootLoaderInstallError('Unable to install '
'GRUB2 boot loader and unable to create required '
'deferred boot loader activation file %s' % defer_file,
ioerr)
syslog.openlog('pybootmgmt-GRUB2', 0, syslog.LOG_DAEMON)
syslog.syslog(syslog.LOG_NOTICE,
'GRUB2 could not be installed at this time. The GRUB2 '
'utilities on the root filesystem are older than (and '
'incompatible with) the GRUB2 modules in %s. '
'The new version of GRUB2 will be installed automatically '
'when booting the Solaris instance in which the new GRUB2 '
'modules are located.' % data_root)
syslog.closelog()
else:
raise BootLoaderInstallError('GRUB2 installation failed',
bmwerr)
finally:
if target == 'uefi64':
try:
try:
statinfo = os.stat(oracle_bootapp)
except OSError:
statinfo = None
if (cmd_succeeded and statinfo and statinfo.st_size > 0 and
copy_efi_boot_to_default):
try:
self._make_dirtree(esp_mountpt + '/efi/boot/')
shutil.copyfile(oracle_bootapp, default_bootapp)
except (IOError, OSError, BootmgmtConfigWriteError):
self._debug('Failed to copy boot app to '
'default boot location -- ignoring')
if need_unmount:
umount_cmd = ['/sbin/umount', esp_mountpt]
self._execute_cmd(umount_cmd)
# Final sanity check
if cmd_succeeded and (statinfo is None or
statinfo.st_size == 0):
raise BootLoaderInstallError('Error while creating '
'the UEFI boot loader (size=0) on %s' % esp_dev)
finally:
if esp_tempdir:
try:
os.rmdir(esp_tempdir)
except OSError:
pass
def _copy_grub2_mods(self, target, basepath, exclusion_list):
"""Copies GRUB2 modules (and config files) to a directory under
basepath. Returns a list of tuples, each of which describes one of the
files copied. This is suitable for DiskBootConfig and ODDBootConfig
only.
"""
if isinstance(self._boot_config, NetBootConfig):
# No additional modules for netboot images -- everything is baked
# in.
return []
if not isinstance(self._boot_config, ODDBootConfig):
return []
modpath = os.path.join(self._get_boot_loader_data_root(),
GRUB2BootLoader.GRUB_DATA_PATHS[target])
tuple_list = []
# Iterate through all files in the modpath directory, excluding ones
# that are in the exclusion_list:
boot_subdir = GRUB2BootLoader.BOOT_GRUB_SUBDIR + '/' + \
GRUB2BootLoader.GRUB_BOOT_SUBDIRS[target]
self._make_dirtree(os.path.join(basepath, boot_subdir))
for name in os.listdir(modpath):
# We're only interested in *.img, *.mod, *.lst and efiemu*
if not (name.endswith('.mod') or name.endswith('.lst') or
name.startswith('efiemu') or name.endswith('.img')):
continue
if name.split('.')[0] in exclusion_list:
self._debug('Skipping copying file %s', name)
continue
srcpath = os.path.join(modpath, name)
destpath = os.path.join(basepath, boot_subdir, name)
try:
shutil.copy(srcpath, destpath)
except (OSError, shutil.Error) as copyerr:
self._debug('Error while copying %s -> %s: %s',
(srcpath, destpath, copyerr))
for tup in tuple_list:
try:
os.remove(tup[1])
except OSError:
pass
raise BootmgmtConfigWriteError('Error while copying GRUB2'
' file %s to %s' % (srcpath, destpath), copyerr)
else:
tuple_list.append((BootConfig.OUTPUT_TYPE_FILE,
destpath,
None,
'%(' + ODDBootConfig.TOKEN_ODD_ROOT + ')s' +
os.path.join('/', boot_subdir, name),
'root', 'root', 0644))
return tuple_list
def _create_grub2_image(self, target, basepath):
"""Construct the bootable grub2 image for the specified target. The
boot image is BootConfig-specific (BootLoaders associated with
ODDBootConfig instances get BIOS-targeted El Torito images and UEFI64-
targeted FAT filesystem images with embedded UEFI64-targeted boot
applications (suitable for direct inclusion in an ODD image);
NetBootConfig boot loaders get BIOS PXE images and UEFI64 net boot
application images).
"""
# microconfigs are needed even on BIOS targets, since the resulting
# image is used to build a USB image, where we will need to search.
if isinstance(self._boot_config, ODDBootConfig):
microconfig = self._create_microconfig(target)
else:
microconfig = None
tuples = None
if isinstance(self._boot_config, ODDBootConfig):
if target == 'bios':
tuples = self._create_bios_odd_image(basepath, microconfig)
elif target == 'uefi64':
tuples = self._create_uefi64_odd_image(basepath, microconfig)
elif isinstance(self._boot_config, NetBootConfig):
if target == 'bios':
tuples = self._create_bios_pxe_image(basepath)
elif target == 'uefi64':
tuples = self._create_uefi64_net_image(basepath)
if microconfig:
shutil.rmtree(microconfig)
if tuples is not None:
return tuples
raise BootmgmtUnsupportedPlatformError('%s and target=%s not supported'
% (self._boot_config.__class__.__name__, target))
def _create_bios_odd_image(self, basepath, microconfig):
"""Create an El Torito image by invoking grub-mkimage with the proper
parameters, then prepending the result with cdboot.img.
"""
boot_subdir = GRUB2BootLoader.BOOT_GRUB_SUBDIR + '/' + \
GRUB2BootLoader.GRUB_BOOT_SUBDIRS['bios']
# Write the image to 'core.img', as that's where we'll look for it
# if we want to repurpose the ISO to make other bootable media
imagename = os.path.join(basepath, boot_subdir,
GRUB2BootLoader.CORE_IMG_BASENAME)
self._make_basedir(imagename)
wholeimagename = os.path.join(basepath,
GRUB2BootLoader.EL_TORITO_IMAGENAMES['bios'])
self._make_basedir(wholeimagename)
modlist = ['biosdisk', 'iso9660', 'part_msdos', 'part_sunpc',
'part_gpt', 'ufs1']
modlist.extend(GRUB2BootLoader.PRELOADED_MODULES['bios'])
self._grub2_mkimage_generic('bios', imagename, modlist, microconfig)
# Finally, create the final file with cdboot.img prepended to the
# image we just created.
try:
modpath = os.path.join(self._get_boot_loader_data_root(),
GRUB2BootLoader.GRUB_DATA_PATHS['bios'])
cdboot_img = os.path.join(modpath, 'cdboot.img')
wholeimg = open(wholeimagename, 'wb')
for fname in [cdboot_img, imagename]:
shutil.copyfileobj(open(fname), wholeimg)
wholeimg.close()
except (OSError, IOError) as copyerr:
raise BootmgmtConfigWriteError('Could not finish construction of '
' %s' % wholeimagename, copyerr)
tuple_list = [(BootConfig.OUTPUT_TYPE_BIOS_ELTORITO, wholeimagename,
None, None, None, None, None),
(BootConfig.OUTPUT_TYPE_FILE, imagename, None,
'%(' + ODDBootConfig.TOKEN_ODD_ROOT + ')s' +
os.path.join('/', boot_subdir,
GRUB2BootLoader.CORE_IMG_BASENAME),
'root', 'root', 0644)]
mod_tuples = self._copy_grub2_mods('bios', basepath, modlist)
if mod_tuples:
tuple_list.extend(mod_tuples)
return tuple_list
def _grub2_mkimage_generic(self, target, imagename, modlist,
microconfig=None):
"""Create a GRUB2 boot image for the specified target, named imagename,
with modlist modules embedded and preloaded. If microconfig is not
None, it must be the path to a directory containing a boot/grub/grub.cfg
file.
"""
modpath = os.path.join(self._get_boot_loader_data_root(),
GRUB2BootLoader.GRUB_DATA_PATHS[target])
base_cmd = os.path.join('/',
GRUB2BootLoader.GRUB_PLATFORM_PREFIXES[target],
GRUB2BootLoader.GRUB_MKIMAGE)
if microconfig:
memdisk_path = self._make_memdisk_image(microconfig, target)
modlist.extend(['memdisk', 'tar', 'search', 'gzio', 'regexp',
'configfile'])
img_cmd = [base_cmd, '-o', imagename, '-d', modpath,
'-O', GRUB2BootLoader.MKIMAGE_FORMATS[target],
'-m', memdisk_path,
'--prefix=(memdisk)/boot/grub'] + modlist
else:
memdisk_path = None
img_cmd = [base_cmd, '-o', imagename, '-d', modpath,
'-O', GRUB2BootLoader.MKIMAGE_FORMATS[target],
'--prefix=%s' % os.path.join('/',
GRUB2BootLoader.BOOT_GRUB_SUBDIR,
GRUB2BootLoader.GRUB_BOOT_SUBDIRS[target])] + modlist
self._execute_cmd(img_cmd, 'Generating %s boot loader image' % target,
'%s boot image generation' % target)
if memdisk_path:
# Now we can remove the temporary memdisk image file:
try:
os.remove(memdisk_path)
except OSError:
pass
def _make_memdisk_image(self, microconfig, target):
"""Create a "tarfs" (tar file) with the microconfig specified. The
microconfig specified is the parent directory -- there must be a
boot/grub/grub.cfg under that directory.
"""
memdisk_img = None
orig_dir = None
try:
memdisk_img = tempfile.NamedTemporaryFile(dir='/system/volatile',
delete=False)
# Change into the directory that includes microconfig. This is
# required so that we can generate the tarfile that includes it.
orig_dir = os.getcwd()
os.chdir(microconfig)
self._debug('cwd => %s', microconfig)
tar_cmd = ['/bin/tar', 'cf', '-', 'boot']
self._execute_cmd(tar_cmd, 'Generating %s memdisk image' % target,
'%s memdisk image creation' % target, memdisk_img)
memdisk_img.close()
# We change back to the original directory in the finally clause,
# below
return memdisk_img.name
except OSError as oserr:
if memdisk_img:
memdisk_img.close()
try:
os.remove(memdisk_img.name)
except OSError:
pass
raise BootmgmtConfigWriteError('Error during GRUB2 memdisk '
'image construction', oserr)
finally:
if orig_dir:
try:
os.chdir(orig_dir)
self._debug('cwd => %s', orig_dir)
except OSError:
pass
# pylint: disable=R0912,R0913,R0914,R0915
def _execute_cmd(self, cmd, msg=None, whatisthis=None, stdout_file=None,
stdin_file=None, return_stdout=False, return_stderr=False):
"""Spawn a command.
"""
fullcmd = ' '.join(cmd)
self._debug('Invoking %s', fullcmd)
try:
if msg:
self._debug(msg)
if return_stdout or not stdout_file:
stdout_val = subprocess.PIPE
else:
stdout_val = stdout_file
if return_stderr or stdout_file:
stderr_val = subprocess.PIPE
else:
stderr_val = stdout_val
proc = subprocess.Popen(cmd, stdin=stdin_file, stdout=stdout_val,
stderr=stderr_val)
stdout_output = ''
stderr_output = ''
# Set up the stderr/out pipes to be non-blocking so we can read
# from both without blocking.
filedescs = []
if stderr_val == subprocess.PIPE:
filedescs.append(proc.stderr.fileno())
if stdout_val == subprocess.PIPE:
filedescs.append(proc.stdout.fileno())
for fdesc in filedescs:
fcinfo = fcntl.fcntl(fdesc, fcntl.F_GETFL)
fcntl.fcntl(fdesc, fcntl.F_SETFL, fcinfo | os.O_NONBLOCK)
while proc.poll() is None:
try:
if stderr_val == subprocess.PIPE:
stderr_output += proc.stderr.read()
except IOError as ioerr: # EWOULDBLOCK
if ioerr.errno != errno.EWOULDBLOCK:
self._debug('Error while reading from stderr'
' pipe: %s', ioerr)
try:
if stdout_val == subprocess.PIPE:
stdout_output += proc.stdout.read()
except IOError as ioerr: # EWOULDBLOCK
if ioerr.errno != errno.EWOULDBLOCK:
self._debug('Error while reading from stderr'
' pipe: %s', ioerr)
time.sleep(0.5)
if msg:
self._debug('.')
retcode = proc.returncode
try:
if stderr_val == subprocess.PIPE:
stderr_output += proc.stderr.read()
except IOError as ioerr:
self._debug('Error reading final stderr data: %s',
ioerr)
try:
if stdout_val == subprocess.PIPE:
stdout_output += proc.stdout.read()
except IOError as ioerr:
self._debug('Error reading final stderr data: %s',
ioerr)
if retcode != 0:
self._debug('.failed (exit code %d). '
'stderr was: %s\nstdout was: %s' % (retcode,
stderr_output, stdout_output))
if not whatisthis:
whatisthis = 'command execution'
raise BootmgmtConfigWriteError('Error during %s: '
'%s returned error code %d. stderr was:\n%s' %
(whatisthis, cmd[0], retcode, stderr_output),
retcode=retcode)
if msg:
self._debug('.completed successfully.')
if return_stdout or return_stderr:
return (stdout_output, stderr_output)
else:
return None
except OSError as oserr:
self._debug('Error while invoking %s: %s', (fullcmd, oserr))
raise BootmgmtConfigWriteError('Error invoking %s' % fullcmd, oserr)
# pylint: enable=R0913
def _create_uefi64_odd_image(self, basepath, microconfig):
"""Create a FAT filesystem image that contains a GRUB2 boot application
(constructed by grub-mkimage) suitable for 64-bit UEFI systems. If a
microconfig file is specified, it is embedded into the image via the
-c argument to grub-mkimage.
"""
cleanup_list = []
def _uefi64_odd_cleanup():
"""Perform a cleanup of activities in LIFO order
"""
for func in cleanup_list.reverse():
# pylint: disable=W0702
try:
func()
except:
pass
# pylint: enable=W0702
# Make a temporary directory that will be used to hold the image:
try:
tmpf = tempfile.NamedTemporaryFile(dir='/system/volatile',
delete=False)
tmpf.close()
imagetempname = tmpf.name
cleanup_list.append(lambda : os.remove(imagetempname))
except OSError as oserr:
raise BootmgmtConfigWriteError('Error during 64-bit UEFI El Torito '
'image construction', oserr)
# gzio must be in the list before normal because normal attempts
# to grub_dl_load(gzio) very early in GRUB startup (before $prefix
# is set) so the net result would be an error message from
# grub_dl_load() that $prefix is not set. We use gzio for the
# Solaris boot archive, so we might as well load it now anyway.
modlist = ['iso9660', 'part_msdos', 'part_sunpc', 'part_gpt', 'ufs1']
modlist.extend(GRUB2BootLoader.PRELOADED_MODULES['uefi64'])
self._grub2_mkimage_generic('uefi64', imagetempname, modlist,
microconfig)
imagename = os.path.join(basepath,
GRUB2BootLoader.EL_TORITO_IMAGENAMES['uefi64'])
try:
self._make_basedir(imagename)
except BootmgmtError as bmerr:
_uefi64_odd_cleanup()
raise BootmgmtConfigWriteError('Error during 64-bit UEFI El Torito '
'image construction', bmerr)
# Create the image with a size that will fit the boot image and
# filesystem metadata
try:
imagesize = os.path.getsize(imagetempname)
if imagesize < GRUB2BootLoader.UEFI_FS_MIN_SIZE:
imagesize = GRUB2BootLoader.UEFI_FS_MIN_SIZE
imagefile = open(imagename, 'w')
imagefile.seek(imagesize - 1)
imagefile.write('\0')
imagefile.close()
cleanup_list.append(lambda : os.remove(imagename))
except (IOError, OSError) as un_err:
_uefi64_odd_cleanup()
raise BootmgmtConfigWriteError('Error during 64-bit UEFI El Torito '
'image construction', un_err)
# Associate the image with the lofi device
lofiadm_cmd = ['/sbin/lofiadm', '-a', imagename]
lofiadm_d_cmd = list(lofiadm_cmd) # Reuse the command list from above
lofiadm_d_cmd[1] = '-d'
try:
lofidev = self._execute_cmd(lofiadm_cmd, return_stdout=True)[0]
cleanup_list.append(lambda : self._execute_cmd(lofiadm_d_cmd))
except BootmgmtError as bmerr:
_uefi64_odd_cleanup()
raise BootmgmtConfigWriteError('Error during 64-bit UEFI El Torito '
'image construction', bmerr)
lofidev = lofidev.strip()
raw_lofidev = lofidev.replace('/lofi/', '/rlofi/')
self._debug('lofi association: %s => %s', (imagename, lofidev))
self._debug('Creating pcfs on image size %dK', (imagesize / 1024))
# Create a pcfs filesystem on the lofi device
mkfs_cmd = ['/usr/lib/fs/pcfs/mkfs', '-o', 'nofdisk,size=%d' %
(imagesize / 1024), raw_lofidev]
try:
self._execute_cmd(mkfs_cmd, stdin_file=open('/dev/zero', 'r'))
except BootmgmtError as bmerr:
_uefi64_odd_cleanup()
raise BootmgmtConfigWriteError('Error during 64-bit UEFI El Torito '
'image construction', bmerr)
# Create a temp dir to use as a mountpoint
try:
mount_tempdir = tempfile.mkdtemp(dir='/system/volatile')
cleanup_list.append(lambda : os.rmdir(mount_tempdir))
# Mount the pcfs filesystem just created
mount_cmd = ['/usr/lib/fs/pcfs/mount', lofidev, mount_tempdir]
umount_cmd = ['/sbin/umount', mount_tempdir]
self._execute_cmd(mount_cmd)
cleanup_list.append(lambda : self._execute_cmd(umount_cmd))
except (IOError, BootmgmtError) as un_err:
_uefi64_odd_cleanup()
raise BootmgmtConfigWriteError('Error during 64-bit UEFI El Torito '
'image construction', un_err)
# Create the EFI/BOOT directory that will hold the boot image
image_destdir = os.path.join(mount_tempdir, 'EFI/BOOT')
try:
self._make_dirtree(image_destdir)
except BootmgmtError as bmerr:
_uefi64_odd_cleanup()
raise BootmgmtConfigWriteError('Error during 64-bit UEFI El Torito '
'image construction', bmerr)
# Copy the image onto the mounted pcfs filesystem
try:
shutil.copyfile(imagetempname,
os.path.join(image_destdir, 'BOOTx64.EFI'))
except (OSError, IOError) as copyerr:
_uefi64_odd_cleanup()
raise BootmgmtConfigWriteError('Error during 64-bit UEFI El Torito '
'image construction', copyerr)
# Unmount the pcfs filesystem
try:
self._execute_cmd(umount_cmd)
except BootmgmtError as bmerr:
self._debug('Unmounting the uefi64 image failed: %s', bmerr)
try:
os.rmdir(mount_tempdir)
except OSError as oserr:
self._debug('Removing temp mount dir for uefi64 image failed: %s',
oserr)
# Remove the lofi association
try:
self._execute_cmd(lofiadm_d_cmd)
except BootmgmtError as bmerr:
self._debug('Removing lofi associated failed: %s', bmerr)
# Remove the boot image temporary file and temp dir
try:
os.remove(imagetempname)
except OSError as oserr:
self._debug('Removing temp uefi64 image failed: %s', oserr)
tuple_list = [(BootConfig.OUTPUT_TYPE_UEFI_ELTORITO, imagename,
None, None, None, None, None)]
mod_tuples = self._copy_grub2_mods('uefi64', basepath, modlist)
if mod_tuples:
tuple_list.extend(mod_tuples)
return tuple_list
# pylint: enable=R0912,R0914,R0915
def _nbp_path(self, target):
"""Copies the NBP program for the specified target to the specified
path. Raises BootmgmtConfigWriteError if the copy failed.
"""
return os.path.join(self._boot_config.net_osimage_root,
GRUB2BootLoader.GRUB_NBP_PATHS[target])
# pylint: disable=W0613
def _create_bios_pxe_image(self, basepath):
"""Create a GRUB2 image bootable via the PXE on systems with BIOS
firmware.
"""
# For now, do not create the image-- just use the one delivered
# into the installation image.
return [(BootConfig.OUTPUT_TYPE_BIOS_NBP,
self._nbp_path('bios'),
None,
None,
None,
None,
None)]
def _create_uefi64_net_image(self, basepath):
"""Create a GRUB2 UEFI boot application image bootable via the native
UEFI network boot mechanism.
"""
# For now, do not create the image-- just use the one delivered
# into the installation image.
return [(BootConfig.OUTPUT_TYPE_UEFI64_NBP,
self._nbp_path('uefi64'),
None,
None,
None,
None,
None)]
# pylint: enable=W0613
def _create_microconfig(self, target):
"""Create a small configuration file whose purpose is one:
(a) to locate the REAL root device (since the microconfig file will
be loaded from a memdisk baked inside the GRUB2 image), or (b) to
source the REAL configuration file (i.e. when booting from the network,
the microconfig contains the GRUB2 script that searches the tftp
hierarchy, in accordance with the Solaris-defined search order for
the GRUB2 configuration file.)
"""
lines = []
if isinstance(self._boot_config, ODDBootConfig):
emitter = GRUB2MenuEntryEmitter()
root_search = emitter.search_cmd_generic(None, self)
if root_search:
lines.append(root_search)
lines.append('set prefix=($root)%s' %
os.path.join('/', GRUB2BootLoader.BOOT_GRUB_SUBDIR,
GRUB2BootLoader.GRUB_BOOT_SUBDIRS[target]))
lines.append('source $prefix/grub.cfg')
self._debug('Microconfig:\n%s', '\n'.join(lines))
else:
# Future enhancement: Generate the GRUB2 NetBootConfig microconfig
# here by using the script delivered to the install image, making
# sure to set prefix to a directory whose last component is the
# platform name (that means that _grub2_mkimage_generic must be
# modified also to parameterize the --prefix argument given to
# grub-mkimage)
raise BootmgmtUnsupportedOperationError('Microconfig creation '
'not supported for class %s' %
self._boot_config.__class__.__name__)
if not lines:
return None
tmpdir = None
try:
tmpdir = tempfile.mkdtemp(dir='/system/volatile')
# The microconfig file must be stored in a temp dir, under
# boot/grub, since that's where the image construction commands
# will be told to find it (prefix will be set to
# (memdisk)/boot/grub).
tmpfiledir = os.path.join(tmpdir, 'boot/grub')
self._make_dirtree(tmpfiledir, 0700)
cfgfile = open(os.path.join(tmpfiledir,
GRUB2BootLoader.GRUB_CFG_BASENAME), 'w')
for line in lines:
cfgfile.write(line)
cfgfile.write('\n')
cfgfile.close()
# Return the parent dir holding the config file
return tmpdir
except IOError as err:
if tmpdir:
shutil.rmtree(tmpdir, True)
raise BootmgmtConfigWriteError('Error while trying to write '
'GRUB2 microconfig', err)
def _make_basedir(self, basepath, mode=0755):
"""Create directories for dirname(basepath).
May raise BootmgmtConfigWriteError if there was a problem creating the
directories.
"""
self._make_dirtree(os.path.dirname(basepath), mode)
@staticmethod
def _make_dirtree(path, mode=0755):
"""Create directories for path.
May raise BootmgmtConfigWriteError if there was a problem creating the
directories.
"""
old_umask = os.umask(0)
try:
os.makedirs(path, mode)
except OSError as oserr:
if oserr.errno != errno.EEXIST:
raise BootmgmtConfigWriteError('Could not create '
'directory', oserr)
finally:
os.umask(old_umask)
class GRUB2MenuOrganizer(LoggerMixin):
"""GRUB2MenuOrganizer includes shared code between the GRUB2 pybootmgmt
backend and the GRUB2 menu generator program in $PREFIX/etc/grub.d.
Methods are provided to get an ordered list of boot instances from the
system (via the autogen backend (which gets the list of BEs and
generates a boot instance for each BE) if none exist in the menu.conf) and
the rest from the menu.conf, which has sections for each customized boot
instance (and, therefore, menu entry)) and to synch a list of boot
instances with the menu.conf.
"""
def __init__(self, boot_config, menu_conf_path):
"""Constructor for GRUB2MenuOrganizer
"""
super(GRUB2MenuOrganizer, self).__init__()
self._mcp = None # menu.conf parser instance
self._menu_conf_path = menu_conf_path
self._boot_config = boot_config
@property
def dirty(self):
"""Pseudoproperty returns True of the underlying MenuConfigParser is
dirty.
"""
if self._mcp:
return self._mcp.dirty
return False
@property
def menu_conf_source(self):
"""Accessor for the path to the last menu.conf written.
"""
if self._mcp.last_written_path:
return self._mcp.last_written_path
elif self._mcp.filename:
return self._mcp.filename
else:
return None
def new_config(self):
"""Start over with a new (empty) menu configuration
"""
self._mcp = MenuConfigParser(self._menu_conf_path, True)
def get_bootloader_props(self):
"""Returns a dictionary of properties from the global section of the
menu.conf.
"""
global_props = self._mcp.global_options
blprops = dict(global_props)
self._debug('Serial properties: %s',
global_props.get(BootLoader.PROP_SERIAL_PARAMS))
return blprops
def generate_boot_instances(self):
"""Load the menu.conf file and processes the sections, generating
BootInstance objects and returning the list to the caller.
"""
# The total number of boot instances added to the BootConfig instance
# stored in self._boot_config is equal to the number of BE sections
# in the menu.conf file (including the transient entry, if it exists).
# If there are no BE sections, the autogenerator is used to populate
# the list.
# Get the list of instances from the menu.conf file:
inst_list = self._build_custom_inst_list()
if inst_list:
# If an explicit ordering is specified in the menu configuration
# file, rearrange inst_list and return it.
explicit_order = self._mcp.get_order()
if explicit_order:
do_update = self._rearrange_inst_list(inst_list, explicit_order)
if do_update:
self._mcp.clear_order()
elif self._boot_config.boot_class == BootConfig.BOOT_CLASS_DISK:
# If no instances are present, generate instances for the BEs
# on the system
from bootmgmt.backend.autogen import BootInstanceAutogenFactory
inst_list = BootInstanceAutogenFactory.autogen(self._boot_config,
['solaris'])
for inst in inst_list: # Add a menu.conf entry for each generated BE
classname = inst.__class__.__name__
propdict = self._boot_instance_to_propdict(inst)
self._mcp.add_entry(classname, propdict)
# Now inst_list has the full set of boot instances
return inst_list
def synch_menu_config(self, bi_list, bl_props):
"""Given a list of boot instances and a boot loader properties dict,
synchronize the underlying MenuConfigParser in preparation for writing
the menu.conf.
"""
# First update the properties in the [global] section
for key, value in bl_props.items():
if key in GRUB2BootLoader.SUPPORTED_PROPS:
if key == BootLoader.PROP_SERIAL_PARAMS:
try:
strlist = []
for item in value:
strlist.append(str(item))
value = ', '.join(strlist)
except TypeError:
self._debug('Failed to set serial props')
continue
self._mcp.global_options[key] = str(value)
if (self._boot_config and
self._boot_config.boot_class == BootConfig.BOOT_CLASS_DISK):
valid_bootfses = get_bootfs_list_from_libbe()
else:
valid_bootfses = None
# Loop through all boot instances, keeping track of the order
# for the order property, and synchronizing the menu entry with the
# boot instance.
order_list = []
for inst in bi_list:
# Filter out Solaris boot instances that refer to deleted BEs
# (this is only possible if the boot instance has a bootfs
# member)
if valid_bootfses and isinstance(inst, SolarisDiskBootInstance):
inst_bootfs = getattr(inst, 'bootfs', None)
if inst_bootfs and inst_bootfs not in valid_bootfses:
self._debug('Removing boot instance %s from menu.conf',
getattr(inst, 'title', '<Title Unavailable>'))
continue
custom_entry = getattr(inst, '_menu_conf_entry', None)
if custom_entry:
# Synch boot instance to custom entry:
self._synch_boot_instance_to_entry(inst, custom_entry)
else:
classname = inst.__class__.__name__
# Check if this class has a corresponding emitter in
# the GRUB2MenuEntryEmitter class. If it does not,
# do not add it to menu.conf.
emitter = getattr(GRUB2MenuEntryEmitter,
'_write_menuentry_' + inst.__class__.__name__,
None)
if emitter:
propdict = self._boot_instance_to_propdict(inst)
custom_entry = self._mcp.add_entry(classname, propdict)
inst._menu_conf_entry = custom_entry
else:
custom_entry = None
if custom_entry:
order_list.append(custom_entry.section_string)
self._mcp.set_order(order_list)
# Remove sections in the menu.conf file that correspond to deleted boot
# instances (i.e. all those not in the order_list)
self._mcp.delete_entries(cmp_fn=
(lambda x: x.section_string not in order_list))
def load(self, new_conf_path=None):
"""Loads the menu configuration file.
"""
# This will succeeds either if the menu.conf is loaded successfully,
# or if the menu.conf is not present. In the latter case, an empty
# menu.conf is created (but not written, yet).
if new_conf_path:
self._menu_conf_path = new_conf_path
self._mcp = MenuConfigParser(self._menu_conf_path)
def store(self, new_conf_path=None, force=False):
"""Save the menu configuration to stable storage.
"""
self._mcp.write(new_conf_path, force)
@staticmethod
def _boot_instance_to_propdict(inst):
"""Convert boot instance to a dictionary suitable for serialization
in the menu.conf file. The bulk of the work is performed by methods
tailored for each type of BootInstance.
"""
if inst and hasattr(inst, 'serialization_dict'):
return inst.serialization_dict()
else:
return {}
def _propdict_to_boot_instance(self, section_type, propdict):
"""Convert a propdict from a menu.conf section into a boot instance.
The type of BootInstance is given by the string section_type.
"""
import bootmgmt.bootconfig
classtype = getattr(bootmgmt.bootconfig, section_type, None)
if classtype:
# Make a copy of the properties from the section and remove the
# 'modified' attribute, since it has nothing to do with creation
# of a boot instance:
attributes = dict(propdict)
if 'modified' in attributes:
del attributes['modified']
# The default property must be a bool, else we get an exception
# at BootInstance construction time.
if 'default' in attributes and type(attributes['default']) != bool:
attributes['default'] = parse_bool(attributes['default'])
self._debug('Instantiating type %s with %s', (section_type,
str(attributes)))
# Add the platform value from the associated BootConfig so that
# the BootInstance is properly initialized.
plat = self._boot_config.platform_requested
if plat:
firmw = self._boot_config.firmware_requested
attributes['platform'] = (plat, firmw)
# BootInstance instantiation takes a path as the first parameter
# and **kwargs for all others.
# pylint: disable=W0142
return classtype(None, **attributes)
# pylint: enable=W0142
self._debug('Could not find type %s', section_type)
return None
def _synch_boot_instance_to_entry(self, boot_inst, mc_entry):
"""Copy properties from the boot instance to the MenuConfigEntry
"""
propdict = self._boot_instance_to_propdict(boot_inst)
self._debug('Updating %s with dict %s', (mc_entry.section_string,
propdict))
mc_entry.update_options(propdict)
def _rearrange_inst_list(self, boot_instances, explicit_order):
"""Rearranges the boot instance list passed in to match the order
specified. The boot_instances list is modified in place.
"""
# The explicit_order list contains strings that are of the forms:
# '<classtype>|<index>'. Note that if the explicit order list is
# incomplete, order is undefined for other entries. If a boot
# instance is referenced in the explicit order list, but it doesn't
# exist on the system, that order entry is ignored
need_update = False
boot_insts = list(boot_instances)
ordered_list = []
for current in explicit_order:
found = False
for instidx, inst in enumerate(boot_insts):
if inst is None: # Skip instance slots marked with None
continue
# Each instance in the instance list is guaranteed to have
# a _menu_conf_entry, so there is no need to use getattr here.
# pylint: disable=W0212
if inst._menu_conf_entry.match_section_by_string(current):
# Found a match -- add the instance to the temp list
ordered_list.append(inst)
# and mark the slot in boot_insts so we don't duplicate
# effort
boot_insts[instidx] = None
found = True
break
# pylint: enable=W0212
if not found: # the order property is out of date!
need_update = True
# Now update boot_instances with the ordered_list first:
cur_inst = 0
self._debug('%s', ordered_list)
for inst in ordered_list:
boot_instances[cur_inst] = inst
cur_inst += 1
# Then any leftover instances that were not specified explicitly.
# Note that the relative order of these unmatched instances is
# not disturbed.
for inst in boot_insts:
if inst:
self._debug('Remaining[%d]: %s', (cur_inst, inst))
boot_instances[cur_inst] = inst
cur_inst += 1
return need_update
def _build_custom_inst_list(self):
"""Builds a list of boot instances, each of which corresponds to a
single entry-type section in the menu.conf file.
"""
custom_inst_list = []
for entry in self._mcp.entry_list:
# Ignore entries with sections whos names are tuples with <> 2
# items:
if len(entry.section) != 2:
continue
boot_inst = self._propdict_to_boot_instance(entry.section[0],
entry.options)
# Store the menu.conf entry this boot instance was created from
# for later use (i.e. sorting, etc.)
boot_inst._menu_conf_entry = entry
self._debug('Created boot instance:\n%s', boot_inst)
custom_inst_list.append(boot_inst)
return custom_inst_list
@staticmethod
def escape_title_str_for_grub_cfg(rawstr):
"""Escape a menuentry title. In addition to the regular escapes,
'>' must be escaped (replaced with '>>').
"""
escstr = GRUB2MenuOrganizer.escape_str_for_grub_cfg(rawstr)
if escstr:
escstr = escstr.replace('>', '>>')
return escstr
@staticmethod
def escape_str_for_grub_cfg(rawstr):
"""Escape a string so it evaluates to its raw value in grub.cfg.
"""
# Quotation marks must be escaped, as must backslashes
cookedstr = rawstr.replace('\\', '\\\\')
cookedstr = cookedstr.replace('"', '\\"')
return cookedstr
class GRUB2MenuEntryEmitter(LoggerMixin):
"""Emits GRUB menuentry blocks for the grub.cfg file.
"""
def __init__(self, fileobj=None, vardict=None, force_grub2=False):
"""If fileobj is None, STDOUT is used. vardict is the dictionary
that holds variables that may be used by the menu entry
generators. If it's None, os.environ is used. If force_grub2 is
True, we instantiate the BootConfigs with a loaderclass keyword arg
to ensure we get the BootInstances from GRUB2 and not a different
(perhaps higher-ranked) BootLoader, whose BootInstances may be in
the BootConfig's boot_instances list.
"""
super(GRUB2MenuEntryEmitter, self).__init__()
if not fileobj:
self._fileobj = sys.stdout
else:
self._fileobj = fileobj
self.menuentry_theme_string = ''
self.submenu_theme_string = ''
self._rpool_props = {}
self.lzfsh = None
self.force_grub2 = force_grub2
if not vardict:
self._vardict = os.environ
else:
self._vardict = dict(vardict)
def cache_zpool_props(self, lzfsh, rpool):
"""Caches zpool properties used by the menu generator methods.
"""
zph = zpool_open(lzfsh, rpool)
if self._rpool_props.get(rpool) is None:
self._rpool_props[rpool] = {}
rawguid = zpool_get_prop(lzfsh, zph, ZPOOL_PROP_GUID)
try:
# The GRUB2 guid for a zfs pool is a straight hex conversion--
# there are NO dashes, but there ARE leading zeroes!
hexguid = '%016x' % int(rawguid)
self._rpool_props[rpool][ZPOOL_PROP_GUID] = hexguid
except ValueError as valerr:
self._debug("Couldn't convert guid `%s' to hex: %s",
(rawguid, valerr))
zpool_close(zph)
def emit_entries(self):
"""Emit entries to the fileobj specified in __init__. This should only
be invoked when this class is instantiated by the GRUB2 autogen script.
"""
all_root_pools = libbe_py.beGetRootPoolList()
valid_bootfses = get_bootfs_list_from_libbe()
self.lzfsh = libzfs_init()
for rpool in all_root_pools:
if self.force_grub2:
argdict = { 'loaderclass' : GRUB2BootLoader }
self._debug('Forcing loaderclass = GRUB2BootLoader')
else:
argdict = None
with libbe.get_boot_config(rpool, init_argdict=argdict) as bootconf:
self.cache_zpool_props(self.lzfsh, rpool)
for bootinst in bootconf.boot_instances:
# Filter out entries that managed to exist despite
# referring to a nonexistant BE:
if (valid_bootfses and
isinstance(bootinst, SolarisDiskBootInstance)):
inst_bootfs = getattr(bootinst, 'bootfs', None)
if inst_bootfs and inst_bootfs not in valid_bootfses:
continue
self.write_menuentry(bootinst)
self._fileobj.write('\n')
# Emit code to check for the presence and nonzero size of
# a GRUB Legacy configuration file. If it exists, extract
# the entries into a submenu.
self._fileobj.write('if [ "$target" = "%(bios)s" ]; then\n' %
GRUB2BootLoader.GRUB_TARGET_STRINGS)
rootvar = 'root_' + ''.join([x for x in rpool if x.isalnum()])
search_cmd = self._search_cmd_for_pool(rpool, rootvar)
self._fileobj.write('\t%s\n' % search_cmd)
legacy_cfg_file = '($%s)/@/boot/grub/menu.lst' % rootvar
self._fileobj.write('\tif [ -s "%s" ]; then\n' %
legacy_cfg_file)
title = 'Legacy GRUB Menu (from root pool %s)' % rpool
self._fileobj.write('\t\tsubmenu "%s" "%s" %s{\n' % (title,
legacy_cfg_file, self.submenu_theme_string))
self._fileobj.write(
'\t\t\textract_legacy_entries_source "$2"\n')
self._fileobj.write('\t\t}\n')
self._fileobj.write('\tfi\n')
self._fileobj.write('fi\n\n')
libzfs_fini(self.lzfsh)
self.lzfsh = None
def write_menuentry(self, bootinst, target=None):
"""Call the BootInstance-specific function to emit a menuentry block
"""
func = getattr(self, '_write_menuentry_' + bootinst.__class__.__name__,
None)
if func and getattr(bootinst, 'title', None):
clean_title = GRUB2MenuOrganizer.escape_str_for_grub_cfg(
bootinst.title)
tabs = ''
# If the boot instance is target-specific, emit code that causes
# GRUB2 to only process the menu entry when running on that target.
if bootinst.TARGET and bootinst.TARGET[1]:
targdict = GRUB2BootLoader.GRUB_TARGET_STRINGS
self._fileobj.write('if [ "$target" = "%s"' %
targdict[bootinst.TARGET[1][0]])
if len(bootinst.TARGET[1]) > 1:
for targstring in bootinst.TARGET[1][1:]:
self._fileobj.write(' -o "$target" = "%s"' %
targdict[targstring])
self._fileobj.write(' ]; then\n')
tabs = '\t'
needclose = True
else:
needclose = False
menu_start = tabs + 'menuentry "%s" %s{\n' % \
(clean_title, self.menuentry_theme_string)
self._fileobj.write(menu_start)
entry = func(bootinst, target)
for line in entry:
if line == '':
self._fileobj.write('\n')
else:
self._fileobj.write(tabs + '\t' + line + '\n')
self._fileobj.write(tabs + '}\n')
if needclose:
self._fileobj.write('else\n')
self._fileobj.write('\tmenuentry "Entry [%s] not supported '
'on this firmware" {\n' % clean_title)
self._fileobj.write('\t\techo "Not supported"\n')
self._fileobj.write('\t}\n')
self._fileobj.write('fi\n')
else:
self._debug('Not emitting entry for instance type %s (title=%s)',
(bootinst.__class__.__name__,
str(getattr(bootinst, 'title', None))))
def _search_cmd_for_pool(self, rpool, varname='root'):
"""Returns a string containing the search command to use for the
specified pool.
"""
if (self._rpool_props.get(rpool) and
self._rpool_props[rpool].get(ZPOOL_PROP_GUID)):
guid = self._rpool_props[rpool][ZPOOL_PROP_GUID]
# XXX Add hints:
return 'search --no-floppy --fs-uuid --set=%s %s' % (varname, guid)
else:
# XXX Add hints:
return 'search --no-floppy --label --set=%s %s' % (varname, rpool)
# pylint: disable=C0103
def _write_menuentry_SolarisNetBootInstance(self, bootinst, target=None):
"""Emits a menuentry for a Solaris network boot instance.
"""
entry = []
bootconfig = getattr(bootinst, '_bootconfig', None)
# If the parent boot configuration isn't a NetBootConfig, we'll need an
# explicit load of the network driver.
if not isinstance(bootconfig, NetBootConfig):
if target.startswith('uefi'):
entry.append('insmod efinet')
else:
entry.append('insmod pxe')
kargs = bootinst.kargs if bootinst.kargs else ''
# Net boot instances frequently have $ strings embedded in the
# kernel arguments. If any are found, escape them from the grub2
# scripting parser
if kargs:
kargs = kargs.replace('$', '\\$')
return self._write_menuentry_generic(bootinst, entry, None, kargs)
# pylint: disable=W0613
def _write_menuentry_SolarisODDBootInstance(self, bootinst, target=None):
"""Emits a menuentry for a ODD-based Solaris instance.
"""
entry = []
bootconfig = getattr(bootinst, '_bootconfig', None)
boot_loader = getattr(bootconfig, 'boot_loader', None)
# If the parent boot configuration isn't on an ODD, we'll need an
# explicit load of iso9660.
if not isinstance(bootconfig, ODDBootConfig):
entry.append('insmod iso9660')
kargs = bootinst.kargs if bootinst.kargs else ''
search = self.search_cmd_generic(bootinst, boot_loader)
if search:
entry.append(search)
return self._write_menuentry_generic(bootinst, entry, None, kargs)
# pylint: disable=W0212
def _write_menuentry_SolarisDiskBootInstance(self, bootinst, target=None):
"""Custom emitter for a SolarisDiskBootInstance
"""
entry = []
# Cover all bases by loading part_msdos, part_sunpc, and part_gpt
entry.append('insmod part_msdos')
entry.append('insmod part_sunpc')
entry.append('insmod part_gpt')
entry.append('')
bootconfig = getattr(bootinst, '_bootconfig', None)
boot_loader = getattr(bootconfig, 'boot_loader', None)
kargs = bootinst.kargs if bootinst.kargs else ''
if bootinst.fstype == 'ufs':
rootpath = None
search = self.search_cmd_generic(bootinst, boot_loader)
if search:
entry.append(search)
is_encrypted = False
else:
# else, assume ZFS
(kargs, rootpath,
is_encrypted) = self._handle_zfs_SolarisDiskBootInstance(
bootinst, entry, kargs)
if not kargs:
return
# If the BootConfig that this boot instance is a member of is a
# disk boot config (i.e. we invoked grub-mkconfig to generate the
# grub.cfg file) AND the console type of this BootConfig is NOT
# graphical, then we need to manually add a call to load_video
# (the body of which is emitted by the 00_header script). This is
# essential so that GRUB can find and set graphics modes for Solaris
# instances that use framebuffer console.
if bootconfig and boot_loader:
try:
constype = boot_loader.getprop(BootLoader.PROP_CONSOLE)
except BootmgmtUnsupportedPropertyError:
pass
else:
if (bootconfig.boot_class == BootConfig.BOOT_CLASS_DISK and
constype != BootLoader.PROP_CONSOLE_GFX):
entry.append('load_video')
# Common code for the remainder of the entry:
return self._write_menuentry_generic(bootinst, entry, rootpath, kargs,
is_encrypted)
# pylint: enable=W0613
def search_cmd_generic(self, bootinst, boot_loader):
"""Returns a search command string that includes the identifying file
from either the bootinst (tried first) or boot_loader.
"""
# If the boot instance specifies a identifying file, use it to
# find $root
if getattr(bootinst, 'identfile', None):
ident_file = bootinst.identfile
else:
ident_file = None
# Fall back on the boot loader's identifying file to find $root
if not ident_file and boot_loader:
ident_file = boot_loader.getprop(BootLoader.PROP_IDENT_FILE)
if ident_file:
return 'search --no-floppy --file --set=root ' + ident_file
else:
self._debug('no identifying file -- search omitted')
return None
def _write_menuentry_generic(self, bootinst, entry, rootpath, kargs,
is_encrypted=False):
"""Generic code for finishing a Solaris boot instance's menuentry
"""
kargs = GRUB2MenuOrganizer.escape_str_for_grub_cfg(kargs)
gfxpayload_modes = self._vardict.get('GRUB_GFXPAYLOAD_MODE')
if gfxpayload_modes:
if gfxpayload_modes[0] != '"': # Add quotes if needed
gfxpayload_modes = '"%s"' % gfxpayload_modes
gfxpayload_modes = 'set gfxpayload=%s' % gfxpayload_modes
# Only 64-bit is supported, so replace the format-string here:
try:
kernel = bootinst.kernel % { 'karch' : 'amd64' }
except KeyError:
kernel = bootinst.kernel
try:
boot_archive = bootinst.boot_archive % { 'karch' : 'amd64' }
except KeyError:
boot_archive = bootinst.boot_archive
if (kernel.find('$ISADIR') != -1 or
boot_archive.find('$ISADIR') != -1):
entry.append('if cpuid -l; then')
entry.append('\tset ISADIR=amd64')
entry.append('else')
entry.append('\tset ISADIR=')
entry.append('fi')
if is_encrypted:
multiboot = 'multiboot2'
module = 'module2'
else:
multiboot = '$multiboot'
module = '$module'
entry.append('set kern=%s' % kernel)
if rootpath:
entry.append('echo -n "Loading ${root}%s$kern: "' % rootpath)
entry.append('%s %s/$kern $kern %s' % (multiboot, rootpath, kargs))
if gfxpayload_modes:
entry.append(gfxpayload_modes)
entry.append('insmod gzio')
bootarch_string = '%s%s' % (rootpath, boot_archive)
entry.append('echo -n "Loading ${root}%s: "' % bootarch_string)
entry.append('%s %s' % (module, bootarch_string))
else:
# If the kernel specified has another path prepended, ensure
# that the second parameter to multiboot[2] is the boot-archive-
# relative kernel path.
idx = kernel.find('/platform')
if idx > 0:
ba_rel_kern = kernel[idx:]
else:
ba_rel_kern = '$kern'
entry.append('echo -n "Loading ${root}$kern: "')
entry.append('%s $kern %s %s' % (multiboot, ba_rel_kern, kargs))
if gfxpayload_modes:
entry.append(gfxpayload_modes)
entry.append('insmod gzio')
entry.append('echo -n "Loading ${root}%s: "' % boot_archive)
entry.append('%s %s' % (module, boot_archive))
return entry
def _handle_zfs_encrypted_dataset(self, rpool, bootfs, entry):
"""Checks if the specified dataset (or implied dataset) has encryption
enabled, and if so, emits the proper commands to prompt for and set
the zfs key. Returns True if the dataset is encrypted.
"""
if bootfs:
dataset_name = bootfs
elif rpool:
dataset_name = get_zpool_default_bootfs(self.lzfsh, rpool)
else:
dataset_name = None
if dataset_name and is_zfs_encrypted_dataset(self.lzfsh, dataset_name):
entry.append('insmod zfscrypt')
entry.append('echo -n "[Encrypted dataset %s] " ' % dataset_name)
entry.append('zfskey')
return True
return False
def _handle_zfs_SolarisDiskBootInstance(self, bootinst, entry, kargs):
"""Handle the ZFS-specific part of menuentry generation. Returns the
kargs to use in the entry.
"""
bootfs = getattr(bootinst, 'bootfs', None)
if bootfs:
rpool, bename = bootfs_split(bootfs)
else: # No bootfs -- check for rpool
rpool = getattr(bootinst, 'rpool', None)
if not rpool:
entry.append('# Cannot determine the root pool for this entry.')
return None
entry.append('insmod zfs')
entry.append('')
search_cmd = self._search_cmd_for_pool(rpool)
if search_cmd:
entry.append(search_cmd)
# If this is an encrypted dataset, prompt user for the password
is_encrypted = self._handle_zfs_encrypted_dataset(rpool, bootfs, entry)
if bootfs:
rootpath = '/ROOT/%s/@' % bename
entry.append('zfs-bootfs %s/ zfs_bootfs' % rootpath)
else:
entry.append('zfs-defaultbootfs $root zfs_rootpath zfs_bootfs')
rootpath = '/${zfs_rootpath}/@'
if kargs:
kargs = kargs.strip() + ' -B $zfs_bootfs'
else:
kargs = '-B $zfs_bootfs'
return (kargs, rootpath, is_encrypted)
# pylint: disable=W0613
def _write_menuentry_ChainDiskBootInstance(self, inst, target=None):
"""Custom emitter for a ChainDiskBootInstance
"""
entry = []
if ((inst.chaininfo is None) or
(type(inst.chaininfo) is not tuple or len(inst.chaininfo) == 0) or
(type(inst.chaininfo[0]) is not int) or
((len(inst.chaininfo) > 1 and type(inst.chaininfo[1]) is not
int and type(inst.chaininfo[1]) is not tuple)) or
(len(inst.chaininfo) > 1 and type(inst.chaininfo[1]) is tuple and
len(inst.chaininfo[1]) > 2)):
self._debug('Invalid chainloader info in boot instance')
entry.append('# Could not process this chainload entry')
return entry
diskstr = '(hd' + str(inst.chaininfo[0])
if len(inst.chaininfo) > 1:
if type(inst.chaininfo[1]) is tuple:
for tuple_item in inst.chaininfo[1]:
try:
int_item = int(tuple_item)
# GRUB2 uses 1-based partition indices
diskstr += ',' + str(int_item + 1)
except ValueError:
# Special case -- use of letters: GRUB2 does not
# support using letters to access VTOC-labelled drives
# so convert the letter into a numerical index
if (isinstance(tuple_item, basestring) and
len(tuple_item) == 1 and tuple_item >= 'a' and
tuple_item <= 'z'):
part = (ord(tuple_item) - ord('a')) + 1
diskstr += ',%d' % part
else:
try:
int_item = int(inst.chaininfo[1])
# GRUB2 uses 1-based partition indices
diskstr += ',' + str(int_item + 1)
except ValueError:
diskstr += ',' + inst.chaininfo[1]
diskstr += ')'
entry.append('set root=%s' % diskstr)
if inst.forceactive:
entry.append('parttool %s boot+' % diskstr)
if int(inst.chainstart) != 0:
entry.append('chainloader --force %s+%s' %
(inst.chainstart, inst.chaincount))
else:
entry.append('chainloader --force +%s' % inst.chaincount)
return entry
# pylint: enable=C0103,W0212,W0613
def bootloader_classes():
"""Returns a list of boot loader classes, consumed by the boot loader
factory.
"""
return [GRUB2BootLoader]