#!/usr/bin/python
#
# 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) 2008, 2017, Oracle and/or its affiliates. All rights reserved.
import errno
import os
import shutil
import tempfile
from pkg.client import global_settings
logger = global_settings.logger
import pkg.client.api_errors as api_errors
import pkg.misc as misc
import pkg.portable as portable
import pkg.pkgsubprocess as subprocess
# pkg(1) can be installed and used without any BE management module.
# However, in order to provide recovery feature,
# it will try to use the BE management module if it exists.
# We will first try to import the pybemgmt module, which is only
# available in Solaris 12. If it's not found, we will
# also attempt to use the old libbe module, if it exists.
try:
# First, try importing the pybemgmt module.
import bemgmt
from bemgmt.be_errors import BeFmriError, BeNameError, \
BeNotFoundError, BeMgmtError, BeMgmtOpError
except ImportError:
# Try importing older libbe
try:
import libbe as be
except ImportError:
try:
# try importing using libbe module's old name (pre 172)
import libbe_py as be
except ImportError:
# All recovery actions are disabled when libbe can't
# be imported.
pass
class GenericBootEnv(object):
"""This class contains common functions used by both bemgmt module
and the older pylibbe module.
"""
def __init__(self, img, progress_tracker=None):
self.be_name = None
self.dataset = None
self.be_name_clone = None
self.be_name_clone_uuid = None
self.clone_dir = None
self.img = img
self.is_live_BE = False
self.is_valid = False
self.snapshot_name = None
self.progress_tracker = progress_tracker
# record current location of image root so we can remember
# original source BE if we clone existing image
self.root = self.img.get_root()
rc = 0
assert self.root != None
def _store_image_state(self):
"""Internal function used to preserve current image information
and history state to be restored later with _reset_image_state
if needed."""
# Preserve the current history information and state so that if
# boot environment operations fail, they can be written to the
# original image root, etc.
self.img.history.create_snapshot()
def _reset_image_state(self, failure=False):
"""Internal function intended to be used to reset the image
state, if needed, after the failure or success of boot
environment operations."""
if not self.img:
# Nothing to restore.
return
if self.root != self.img.root:
if failure:
# Since the image root changed and the operation
# was not successful, restore the original
# history and state information so that it can
# be recorded in the original image root. This
# needs to be done before the image root is
# reset since it might fail.
self.img.history.restore_snapshot()
self.img.history.discard_snapshot()
# After the completion of an operation that has changed
# the image root, it needs to be reset back to its
# original value so that the client will read and write
# information using the correct location (this is
# especially important for bootenv operations).
self.img.find_root(self.root)
else:
self.img.history.discard_snapshot()
def exists(self):
"""Return true if this object represents a valid BE."""
return self.is_valid
def update_boot_archive(self):
"""Rebuild the boot archive in the current image.
Just report errors; failure of pkg command is not needed,
and bootadm problems should be rare."""
cmd = [
"/sbin/bootadm", "update-archive", "-R",
self.img.get_root()
]
try:
ret = subprocess.call(cmd,
stdout = open(os.devnull), stderr=subprocess.STDOUT)
except OSError as e:
logger.error(_("pkg: A system error {e} was "
"caught executing {cmd}").format(e=e,
cmd=" ".join(cmd)))
return
if ret:
logger.error(_("pkg: '{cmd}' failed. \nwith "
"a return code of {ret:d}.").format(
cmd=" ".join(cmd), ret=ret))
def activate_install_uninstall(self):
"""Activate an install/uninstall attempt. Which just means
destroy the snapshot for the live and non-live case."""
self.destroy_snapshot()
class BeadmV2BootEnv(GenericBootEnv):
"""A BeadmV2BootEnv object is an object containing the
logic for managing the recovery of image-modifying operations such
as install, uninstall, and update.
This class makes use of BE management interfaces from
usr/lib/python*/vendor-packages/bemgmt. The BE management module is
delivered by the pkg:/system/boot-environment-utilities package.
This package is not required for pkg(1) to operate successfully.
It is soft required, meaning if it exists the bootenv class will
attempt to provide recovery support."""
def __init__(self, img, progress_tracker=None):
GenericBootEnv.__init__(self, img, progress_tracker)
self.bemgr = bemgmt.BEManager(logger=logger)
self.img_be = None
self.be_clone = None
try:
self.beList = self.bemgr.list()
except BeMgmtOpError:
# Unable to get the list of BEs
if portable.osname == "sunos":
raise RuntimeError("recoveryDisabled")
# Need to find the name of the BE we're operating on in order
# to create a snapshot and/or a clone of the BE.
for be in self.beList:
if not be.mounted:
continue
# Check if we're operating on the live BE.
# If so it must also be active. If we are not
# operating on the live BE, then verify
# that the mountpoint of the BE matches
# the -R argument passed in by the user.
if self.root == '/':
if not be.active:
continue
else:
self.is_live_BE = True
else:
if be.mountpoint != self.root:
continue
# Set the needed BE components so snapshots
# and clones can be managed.
self.be_name = be.name
self.dataset = be.root_dataset.name
self.img_be = be
try:
# Take a snapshot of the BE being operated on.
# Let BE management generate a snapshot name.
self.snapshot_name = self.bemgr.snapshot(
fmri=self.img_be.fmri)
img.history.operation_snapshot = \
self.snapshot_name
except Exception as ex:
logger.error(_("pkg: unable to create an auto "
"snapshot. pkg recovery is disabled."))
raise RuntimeError("recoveryDisabled")
self.clone_dir = tempfile.mkdtemp()
self.is_valid = True
break
else:
# We will get here if we don't find find any BE's. e.g
# if were are on UFS.
raise RuntimeError("recoveryDisabled")
@staticmethod
def libbe_exists():
return True
@staticmethod
def check_verify():
# The bemgmt module always has the validate_bename() function.
return True
@staticmethod
def split_be_entry(bee):
return (bee.name, bee.activate, bee.active_on_boot,
bee.space_used, bee.creation)
@staticmethod
def copy_be(src_be_name, dst_be_name):
bemgr = bemgmt.BEManager(logger=logger)
try:
bemgr.copy(dst_be_fmri=dst_be_name,
src_be_fmri=src_be_name)
except BeMgmtError:
raise api_errors.UnableToCopyBE()
@staticmethod
def rename_be(orig_name, new_name):
bemgr = bemgmt.BEManager(logger=logger)
bemgr.rename(orig_name, new_name)
@staticmethod
def destroy_be(be_name):
bemgr = bemgmt.BEManager(logger=logger)
return bemgr.destroy(be_name, destroy_snaps=True,
force_umount=True)
@staticmethod
def cleanup_be(be_name):
''' Force unmount and destroy BE. Ignore all errors '''
bemgr = bemgmt.BEManager(logger=logger)
try:
be_obj = bemgr.list(fmri=be_name)
be_obj = be_obj[0]
except Exception as e:
# BE is not found.
return
try:
mounted = be_obj.mounted
mountpoint = be_obj.mountpoint
bemgr.destroy(be_name, destroy_snaps=True,
force_umount=True)
if mounted:
shutil.rmtree(mountpoint,
ignore_errors=True)
except Exception as e:
pass
@staticmethod
def mount_be(be_name, mntpt, include_bpool=False):
bemgr = bemgmt.BEManager(logger=logger)
bemgr.mount(fmri=be_name, mountpoint=mntpt,
mount_bpool=include_bpool)
@staticmethod
def unmount_be(be_name, force=False):
bemgr = bemgmt.BEManager(logger=logger)
bemgr.unmount(fmri=be_name, force=force)
@staticmethod
def set_default_be(be_name):
bemgr = bemgmt.BEManager(logger=logger)
return bemgr.activate(be_name)
@staticmethod
def check_be_name(be_name):
try:
if be_name is None:
return
bemgr = bemgmt.BEManager(logger=logger)
bemgr.validate_bename(be_name)
# Check whether there's already a BE or ZBE with
# the given name.
be_obj = bemgr.be_exists(be_name)
except (BeFmriError, BeNameError):
raise api_errors.InvalidBENameException(be_name)
except BeMgmtError:
raise api_errors.BENamingNotSupported(be_name)
if be_obj:
# A BE or Zone BE with the given be_name exists.
zonename = None
if be_obj.parent_uuid:
zonename = be_obj.be_group.name
raise api_errors.DuplicateBEName(
be_name, zonename=zonename)
@staticmethod
def get_be_list(raise_error=False):
# This check enables the test suite to run much more quickly.
# It is necessary because pkg5unittest (eventually) imports this
# module before the environment is sanitized.
if "PKG_NO_LIVE_ROOT" in os.environ:
return BootEnvNull.get_be_list()
bemgr = bemgmt.BEManager(logger=logger)
try:
beList = bemgr.list()
except Exception:
return []
return (beList)
@staticmethod
def get_be_names():
"""Return a list of BE names."""
return [
be.name for be in BeadmV2BootEnv.get_be_list() if be.name
]
@staticmethod
def get_be_name(path):
"""Looks for the name of the boot environment corresponding to
an image root, returning name and uuid """
# This check enables the test suite to run much more quickly.
# The bemgr.list() call in the bemgmt module scans the
# whole system, and might take a long time depending
# on how many BEs are there on the system.
#
# This is necessary because pkg5unittest (eventually) imports
# the module before the environment is sanitized.
if "PKG_NO_LIVE_ROOT" in os.environ:
return BootEnvNull.get_be_name(path)
bemgr = bemgmt.BEManager(logger=logger)
try:
beList = bemgr.list()
except Exception:
# Unable to get the list of BEs.
return None, None
for be in beList:
if not be.mounted:
continue
# Check if we're operating on the live BE.
# If so it must also be active. If we are not
# operating on the live BE, then verify
# that the mountpoint of the BE matches
# the path argument passed in by the user.
if path == '/':
if be.active:
return be.name, be.uuid
else:
if be.mountpoint == path:
return be.name, be.uuid
return None, None
@staticmethod
def get_uuid_be_dic():
"""Return a dictionary of all boot environment names on the
system, keyed by uuid"""
bemgr = bemgmt.BEManager(logger=logger)
try:
beList = bemgr.list()
except Exception:
return {}
uuid_bes = {}
for be in beList:
uuid_bes[be.uuid] = be.name
return uuid_bes
@staticmethod
def get_activated_be_name():
try:
bemgr = bemgmt.BEManager(logger=logger)
be_obj = bemgr.get_active_on_boot_be()
return (be_obj.name)
except Exception:
raise api_errors.BENamingNotSupported("")
@staticmethod
def get_active_be_name():
try:
bemgr = bemgmt.BEManager(logger=logger)
be_obj = bemgr.get_active_be()
return (be_obj.name)
except Exception:
raise api_errors.BENamingNotSupported("")
def create_backup_be(self, be_name=None):
"""Create a backup BE if the BE being modified is the live one.
'be_name' is an optional string indicating the name to use
for the new backup BE."""
if self.is_live_BE:
if not be_name:
suffix = "-backup"
else:
suffix = None
# Create a clone of the live BE, but do not mount or
# activate it.
try:
self.bemgr.copy(dst_be_fmri=be_name,
dst_bename_suffix=suffix)
except Exception as ex:
raise api_errors.UnableToCopyBE()
elif be_name is not None:
raise api_errors.BENameGivenOnDeadBE(be_name)
def init_image_recovery(self, img, be_name=None):
"""Initialize for an update. If a be_name is given,
validate it. If we're operating on a live BE then clone the
live BE and operate on the clone. If we're operating on a
non-live BE we use the already created snapshot"""
self.img = img
if self.is_live_BE:
# Create a clone of the live BE and mount it.
self.destroy_snapshot()
self.check_be_name(be_name)
try:
self.be_clone = self.bemgr.copy(
dst_be_fmri=be_name)
self.be_name_clone = self.be_clone.fmri
except Exception:
raise api_errors.UnableToCopyBE()
try:
self.bemgr.mount(fmri=self.be_clone.fmri,
mountpoint=self.clone_dir)
except Exception:
raise api_errors.UnableToMountBE(
self.be_clone.name, self.clone_dir)
# record the UUID of this cloned boot environment
self.be_name_clone_uuid = self.be_clone.uuid
# Set the image to our new mounted BE.
img.find_root(self.clone_dir, exact_match=True)
elif be_name is not None:
raise api_errors.BENameGivenOnDeadBE(be_name)
def activate_image(self, set_active=True):
"""Activate a clone of the BE being operated on.
If we are operating on a non-live BE then destroy the snapshot.
'set_active' is an optional boolean indicating that the new
BE (if created) should be set as the active one on next boot.
"""
def activate_live_be():
if set_active:
try:
self.bemgr.activate(fmri=self.be_clone.fmri)
except Exception:
logger.error(_("pkg: unable to activate "
"{0}").format(self.be_clone.name))
return
# Consider the last operation a success, and log it as
# ending here so that it will be recorded in the new
# image's history.
self.img.history.operation_new_be = self.be_clone.name
self.img.history.operation_new_be_uuid = \
self.be_clone.uuid
self.img.history.log_operation_end(release_notes=
self.img.imageplan.pd.release_notes_name)
try:
self.bemgr.unmount(fmri=self.be_clone.fmri)
except Exception as ex:
logger.error(_("unable to unmount BE "
"{be_name} mounted at {be_path}").format(
be_name=self.be_clone.name,
be_path=self.clone_dir))
return
os.rmdir(self.clone_dir)
if set_active:
logger.info(_("""
A clone of {be_name} exists and has been updated and activated.
On the next boot the Boot Environment {be_name_clone} will be
mounted on '/'. Reboot when ready to switch to this updated BE.
""").format(**self.__dict__))
else:
logger.info(_("""
A clone of {be_name} exists and has been updated. To set the
new BE as the active one on next boot, execute the following
command as a privileged user and reboot when ready to switch to
the updated BE:
beadm activate {be_name_clone}
""").format(**self.__dict__))
def activate_be():
# Delete the snapshot that was taken before we
# updated the image and the boot archive.
logger.info(_("{0} has been updated "
"successfully").format(self.be_name))
os.rmdir(self.clone_dir)
self.destroy_snapshot()
self.img.history.operation_snapshot = None
self._store_image_state()
# Ensure cache is flushed before activating and unmounting BE.
self.img.cleanup_cached_content(progtrack=self.progress_tracker)
relock = False
if self.img.locked:
# This is necessary since the lock will
# prevent the boot environment from being
# unmounted during activation. Normally,
# locking for the image is handled
# automatically.
relock = True
self.img.unlock()
caught_exception = None
try:
if self.is_live_BE:
activate_live_be()
else:
activate_be()
except Exception as e:
caught_exception = e
if relock:
# Re-lock be image.
relock = False
self.img.lock()
self._reset_image_state(failure=caught_exception)
if relock:
# Activation was successful so the be image was
# unmounted and the parent image must be re-locked.
self.img.lock()
if caught_exception:
self.img.history.log_operation_error(error=e)
raise caught_exception
def restore_image(self):
"""Restore a failed update attempt."""
# flush() is necessary here so that the warnings get printed
# on a new line.
if self.progress_tracker:
self.progress_tracker.flush()
self._reset_image_state(failure=True)
# Leave the clone around for debugging purposes if we're
# operating on the live BE.
if self.is_live_BE:
logger.error(_("The running system has not been "
"modified. Modifications were only made to a clone "
"of the running system. This clone is mounted at "
"{0} should you wish to inspect it.").format(
self.clone_dir))
else:
# Rollback and destroy the snapshot.
try:
try:
self.bemgr.rollback(self.snapshot_fmri)
except Exception:
logger.error(_("pkg: unable to "
"rollback BE {0} and restore "
"image").format(self.be_name))
self.destroy_snapshot()
os.rmdir(self.clone_dir)
except Exception as e:
self.img.history.log_operation_error(error=e)
raise e
logger.error(_("{bename} failed to be updated. No "
"changes have been made to {bename}.").format(
bename=self.be_name))
def destroy_snapshot(self):
"""Destroy a snapshot of the BE being operated on.
Note that this will destroy the last created
snapshot and does not support destroying
multiple snapshots. Create another instance of
BootEnv to manage multiple snapshots."""
try:
self.bemgr.destroy(fmri=self.snapshot_name)
except IOError:
logger.error("Got IOError from bemgr.destroy %s",
self.snapshot_name)
except Exception:
logger.error(_("pkg: unable to destroy snapshot "
"{0}").format(self.snapshot_name))
def restore_install_uninstall(self):
"""Restore a failed install or uninstall attempt.
Clone the snapshot, mount the BE and notify user of its
existence. Rollback if not operating on a live BE. """
# flush() is necessary here so that the warnings get printed
# on a new line.
if self.progress_tracker:
self.progress_tracker.flush()
if self.is_live_BE:
# Create a new BE based on the previously taken
# snapshot.
try:
self.be_clone = self.bemgr.copy(
src_be_fmri=self.snapshot_name)
except Exception:
logger.error(_("pkg: unable to create "
"BE from snapshot {0}").format(
self.snapshot_name))
return
try:
self.bemgr.mount(fmri=self.be_clone.fmri,
mountpoint=self.clone_dir)
except Exception:
logger.error(_("pkg: unable to mount BE "
"{name} on {clone_dir}").format(
name=self.be_clone.name,
clone_dir=self.clone_dir))
return
logger.error(_("The Boot Environment {name} failed "
"to be updated. A snapshot was taken before the "
"failed attempt and is mounted here {clone_dir}. "
"Use 'beadm unmount {clone_name}' and then "
"'beadm activate {clone_name}' if you wish to "
"boot to this BE.").format(name=self.be_name,
clone_dir=self.clone_dir,
clone_name=self.be_clone.name))
else:
try:
self.bemgr.rollback(
snapshot_fmri=self.snapshot_name)
except Exception:
logger.error("pkg: unable to rollback BE "
"{0}".format(self.be_name))
self.destroy_snapshot()
logger.error(_("The Boot Environment {bename} failed "
"to be updated. A snapshot was taken before the "
"failed attempt and has been restored so no "
"changes have been made to {bename}.").format(
bename=self.be_name))
class BeadmV1BootEnv(GenericBootEnv):
"""A BeadmV1BootEnv object is an object containing the
logic for managing the recovery of image-modifying operations such
as install, uninstall, and update.
Recovery is only enabled for ZFS filesystems. Any operation attempted
on UFS will not be handled by BootEnv.
This class makes use of usr/lib/python*/vendor-packages/libbe.py
as the python wrapper for interfacing with libbe. Both libraries are
delivered by the pkg:/system/boot-environment-utilities package.
This package is not required for pkg(1) to operate successfully.
It is soft required, meaning if it exists the bootenv class will
attempt to provide recovery support."""
def __init__(self, img, progress_tracker=None):
GenericBootEnv.__init__(self, img, progress_tracker)
# Need to find the name of the BE we're operating on in order
# to create a snapshot and/or a clone of the BE.
self.beList = self.get_be_list(raise_error=True)
for i, beVals in enumerate(self.beList):
# pkg(1) expects a directory as the target of an
# operation. BootEnv needs to determine if this target
# directory maps to a BE. If a bogus directory is
# provided to pkg(1) via -R, then pkg(1) just updates
# '/' which also causes BootEnv to manage '/' as well.
# This should be fixed before this class is ever
# instantiated.
be_name = beVals.get("orig_be_name")
# If we're not looking at a boot env entry or an
# entry that is not mounted then continue.
if not be_name or not beVals.get("mounted"):
continue
# Check if we're operating on the live BE.
# If so it must also be active. If we are not
# operating on the live BE, then verify
# that the mountpoint of the BE matches
# the -R argument passed in by the user.
if self.root == '/':
if not beVals.get("active"):
continue
else:
self.is_live_BE = True
else:
if beVals.get("mountpoint") != self.root:
continue
# Set the needed BE components so snapshots
# and clones can be managed.
self.be_name = be_name
self.dataset = beVals.get("dataset")
# Let libbe provide the snapshot name
err, snapshot_name = be.beCreateSnapshot(self.be_name)
self.clone_dir = tempfile.mkdtemp()
# Check first field for failure.
# 2nd field is the returned snapshot name
if err == 0:
self.snapshot_name = snapshot_name
# we require BootEnv to be initialised within
# the context of a history operation, i.e.
# after img.history.operation_name has been set.
img.history.operation_snapshot = snapshot_name
else:
logger.error(_("pkg: unable to create an auto "
"snapshot. pkg recovery is disabled."))
raise RuntimeError("recoveryDisabled")
self.is_valid = True
break
else:
# We will get here if we don't find find any BE's. e.g
# if we are on UFS.
raise RuntimeError("recoveryDisabled")
def __get_new_be_name(self, suffix=None):
"""Create a new boot environment name."""
new_bename = self.be_name
if suffix:
new_bename += suffix
base, sep, rev = new_bename.rpartition("-")
if sep and rev.isdigit():
# The source BE has already been auto-named, so we need
# to bump the revision. List all BEs, cycle through the
# names and find the one with the same basename as
# new_bename, and has the highest revision. Then add
# one to it. This means that gaps in the numbering will
# not be filled.
rev = int(rev)
maxrev = rev
for d in self.beList:
oben = d.get("orig_be_name", None)
if not oben:
continue
nbase, sep, nrev = oben.rpartition("-")
if (not sep or nbase != base or
not nrev.isdigit()):
continue
maxrev = max(int(nrev), rev)
else:
# If we didn't find the separator, or if the rightmost
# part wasn't an integer, then we just start with the
# original name.
base = new_bename
maxrev = 0
good = False
num = maxrev
while not good:
new_bename = "-".join((base, str(num)))
for d in self.beList:
oben = d.get("orig_be_name", None)
if not oben:
continue
if oben == new_bename:
break
else:
good = True
num += 1
return new_bename
@staticmethod
def libbe_exists():
return True
@staticmethod
def check_verify():
return hasattr(be, "beVerifyBEName")
@staticmethod
def split_be_entry(bee):
name = bee.get("orig_be_name")
return (name, bee.get("active"), bee.get("active_boot"),
bee.get("space_used"), bee.get("date"))
@staticmethod
def copy_be(src_be_name, dst_be_name):
ret, be_name_clone, not_used = be.beCopy(
dst_bename=dst_be_name,
src_bename=src_be_name)
if ret != 0:
raise api_errors.UnableToCopyBE()
@staticmethod
def rename_be(orig_name, new_name):
return be.beRename(orig_name, new_name)
@staticmethod
def destroy_be(be_name):
return be.beDestroy(be_name, 1, True)
@staticmethod
def cleanup_be(be_name):
be_list = BootEnv.get_be_list()
for elem in be_list:
if "orig_be_name" in elem and be_name == \
elem["orig_be_name"]:
# Force unmount the be and destroy it.
# Ignore errors.
try:
if elem.get("mounted"):
BootEnv.unmount_be(
be_name, force=True)
shutil.rmtree(elem.get(
"mountpoint"),
ignore_errors=True)
BootEnv.destroy_be(
be_name)
except Exception as e:
pass
break
@staticmethod
def mount_be(be_name, mntpt, include_bpool=False):
return be.beMount(be_name, mntpt, include_bpool=include_bpool)
@staticmethod
def unmount_be(be_name, force=False):
return be.beUnmount(be_name, force=force)
@staticmethod
def set_default_be(be_name):
return be.beActivate(be_name)
@staticmethod
def check_be_name(be_name):
try:
if be_name is None:
return
if be.beVerifyBEName(be_name) != 0:
raise api_errors.InvalidBENameException(be_name)
beList = BootEnv.get_be_list()
# If there is already a BE with the same name as
# be_name, then raise an exception.
if be_name in (be.get("orig_be_name") for be in beList):
raise api_errors.DuplicateBEName(be_name)
except AttributeError:
raise api_errors.BENamingNotSupported(be_name)
@staticmethod
def get_be_list(raise_error=False):
# This check enables the test suite to run much more quickly.
# It is necessary because pkg5unittest (eventually) imports this
# module before the environment is sanitized.
if "PKG_NO_LIVE_ROOT" in os.environ:
return BootEnvNull.get_be_list()
# Check for the old beList() API since pkg(1) can be
# back published and live on a system without the
# latest libbe.
rc = 0
beVals = be.beList()
if isinstance(beVals[0], int):
rc, beList = beVals
else:
beList = beVals
if not beList or rc != 0:
if raise_error:
# Happens e.g. in zones (for now) or live CD
# environment.
raise RuntimeError("nobootenvironments")
beList = []
return beList
@staticmethod
def get_be_name(path):
"""Looks for the name of the boot environment corresponding to
an image root, returning name and uuid """
beList = BootEnv.get_be_list()
for be in beList:
be_name = be.get("orig_be_name")
be_uuid = be.get("uuid_str")
if not be_name or not be.get("mounted"):
continue
# Check if we're operating on the live BE.
# If so it must also be active. If we are not
# operating on the live BE, then verify
# that the mountpoint of the BE matches
# the path argument passed in by the user.
if path == '/':
if be.get("active"):
return be_name, be_uuid
else:
if be.get("mountpoint") == path:
return be_name, be_uuid
return None, None
@staticmethod
def get_be_names():
"""Return a list of BE names."""
return [
be["orig_be_name"] for be in BeadmV1BootEnv.get_be_list()
if "orig_be_name" in be
]
@staticmethod
def get_uuid_be_dic():
"""Return a dictionary of all boot environment names on the
system, keyed by uuid"""
beList = BootEnv.get_be_list()
uuid_bes = {}
for be in beList:
uuid_bes[be.get("uuid_str")] = be.get("orig_be_name")
return uuid_bes
@staticmethod
def get_activated_be_name():
try:
beList = BootEnv.get_be_list()
for be in beList:
# don't look at active but unbootable BEs.
# (happens in zones when we have ZBEs
# associated with other global zone BEs.)
if be.get("active_unbootable", False):
continue
if be.get("active_boot"):
return be.get("orig_be_name")
except AttributeError:
raise api_errors.BENamingNotSupported(be_name)
@staticmethod
def get_active_be_name():
try:
beList = BootEnv.get_be_list()
for be in beList:
if be.get("active"):
return be.get("orig_be_name")
except AttributeError:
raise api_errors.BENamingNotSupported(be_name)
def create_backup_be(self, be_name=None):
"""Create a backup BE if the BE being modified is the live one.
'be_name' is an optional string indicating the name to use
for the new backup BE."""
self.check_be_name(be_name)
if self.is_live_BE:
# Create a clone of the live BE, but do not mount or
# activate it. Do nothing with the returned snapshot
# name that is taken of the clone during beCopy.
ret, be_name_clone, not_used = be.beCopy()
if ret != 0:
raise api_errors.UnableToCopyBE()
if not be_name:
be_name = self.__get_new_be_name(
suffix="-backup-1")
ret = be.beRename(be_name_clone, be_name)
if ret != 0:
raise api_errors.UnableToRenameBE(
be_name_clone, be_name)
elif be_name is not None:
raise api_errors.BENameGivenOnDeadBE(be_name)
def init_image_recovery(self, img, be_name=None):
"""Initialize for an update.
If a be_name is given, validate it.
If we're operating on a live BE then clone the
live BE and operate on the clone.
If we're operating on a non-live BE we use
the already created snapshot"""
self.img = img
if self.is_live_BE:
# Create a clone of the live BE and mount it.
self.destroy_snapshot()
self.check_be_name(be_name)
# Do nothing with the returned snapshot name
# that is taken of the clone during beCopy.
ret, self.be_name_clone, not_used = be.beCopy()
if ret != 0:
raise api_errors.UnableToCopyBE()
if be_name:
ret = be.beRename(self.be_name_clone, be_name)
if ret == 0:
self.be_name_clone = be_name
else:
raise api_errors.UnableToRenameBE(
self.be_name_clone, be_name)
if be.beMount(self.be_name_clone, self.clone_dir) != 0:
raise api_errors.UnableToMountBE(
self.be_name_clone, self.clone_dir)
# record the UUID of this cloned boot environment
not_used, self.be_name_clone_uuid = \
BootEnv.get_be_name(self.clone_dir)
# Set the image to our new mounted BE.
img.find_root(self.clone_dir, exact_match=True)
elif be_name is not None:
raise api_errors.BENameGivenOnDeadBE(be_name)
def update_boot_archive(self):
"""Rebuild the boot archive in the current image.
Just report errors; failure of pkg command is not needed,
and bootadm problems should be rare."""
cmd = [
"/sbin/bootadm", "update-archive", "-R",
self.img.get_root()
]
try:
ret = subprocess.call(cmd,
stdout = open(os.devnull), stderr=subprocess.STDOUT)
except OSError as e:
logger.error(_("pkg: A system error {e} was "
"caught executing {cmd}").format(e=e,
cmd=" ".join(cmd)))
return
if ret:
logger.error(_("pkg: '{cmd}' failed. \nwith "
"a return code of {ret:d}.").format(
cmd=" ".join(cmd), ret=ret))
def activate_image(self, set_active=True):
"""Activate a clone of the BE being operated on.
If were operating on a non-live BE then
destroy the snapshot.
'set_active' is an optional boolean indicating that the new
BE (if created) should be set as the active one on next boot.
"""
def activate_live_be():
if set_active and \
be.beActivate(self.be_name_clone) != 0:
logger.error(_("pkg: unable to activate "
"{0}").format(self.be_name_clone))
return
# Consider the last operation a success, and log it as
# ending here so that it will be recorded in the new
# image's history.
self.img.history.operation_new_be = self.be_name_clone
self.img.history.operation_new_be_uuid = self.be_name_clone_uuid
self.img.history.log_operation_end(release_notes=
self.img.imageplan.pd.release_notes_name)
if be.beUnmount(self.be_name_clone) != 0:
logger.error(_("unable to unmount BE "
"{be_name} mounted at {be_path}").format(
be_name=self.be_name_clone,
be_path=self.clone_dir))
return
os.rmdir(self.clone_dir)
if set_active:
logger.info(_("""
A clone of {be_name} exists and has been updated and activated.
On the next boot the Boot Environment {be_name_clone} will be
mounted on '/'. Reboot when ready to switch to this updated BE.
""").format(**self.__dict__))
else:
logger.info(_("""
A clone of {be_name} exists and has been updated. To set the
new BE as the active one on next boot, execute the following
command as a privileged user and reboot when ready to switch to
the updated BE:
beadm activate {be_name_clone}
""").format(**self.__dict__))
def activate_be():
# Delete the snapshot that was taken before we
# updated the image and the boot archive.
logger.info(_("{0} has been updated "
"successfully").format(self.be_name))
os.rmdir(self.clone_dir)
self.destroy_snapshot()
self.img.history.operation_snapshot = None
self._store_image_state()
# Ensure cache is flushed before activating and unmounting BE.
self.img.cleanup_cached_content(progtrack=self.progress_tracker)
relock = False
if self.img.locked:
# This is necessary since the lock will
# prevent the boot environment from being
# unmounted during activation. Normally,
# locking for the image is handled
# automatically.
relock = True
self.img.unlock()
caught_exception = None
try:
if self.is_live_BE:
activate_live_be()
else:
activate_be()
except Exception as e:
caught_exception = e
if relock:
# Re-lock be image.
relock = False
self.img.lock()
self._reset_image_state(failure=caught_exception)
if relock:
# Activation was successful so the be image was
# unmounted and the parent image must be re-locked.
self.img.lock()
if caught_exception:
self.img.history.log_operation_error(error=e)
raise caught_exception
def restore_image(self):
"""Restore a failed update attempt."""
# flush() is necessary here so that the warnings get printed
# on a new line.
if self.progress_tracker:
self.progress_tracker.flush()
self._reset_image_state(failure=True)
# Leave the clone around for debugging purposes if we're
# operating on the live BE.
if self.is_live_BE:
logger.error(_("The running system has not been "
"modified. Modifications were only made to a clone "
"of the running system. This clone is mounted at "
"{0} should you wish to inspect it.").format(
self.clone_dir))
else:
# Rollback and destroy the snapshot.
try:
if be.beRollback(self.be_name,
self.snapshot_name) != 0:
logger.error(_("pkg: unable to "
"rollback BE {0} and restore "
"image").format(self.be_name))
self.destroy_snapshot()
os.rmdir(self.clone_dir)
except Exception as e:
self.img.history.log_operation_error(error=e)
raise e
logger.error(_("{bename} failed to be updated. No "
"changes have been made to {bename}.").format(
bename=self.be_name))
def destroy_snapshot(self):
"""Destroy a snapshot of the BE being operated on.
Note that this will destroy the last created
snapshot and does not support destroying
multiple snapshots. Create another instance of
BootEnv to manage multiple snapshots."""
if be.beDestroySnapshot(self.be_name, self.snapshot_name) != 0:
logger.error(_("pkg: unable to destroy snapshot "
"{0}").format(self.snapshot_name))
def restore_install_uninstall(self):
"""Restore a failed install or uninstall attempt.
Clone the snapshot, mount the BE and
notify user of its existence. Rollback
if not operating on a live BE"""
# flush() is necessary here so that the warnings get printed
# on a new line.
if self.progress_tracker:
self.progress_tracker.flush()
if self.is_live_BE:
# Create a new BE based on the previously taken
# snapshot.
ret, self.be_name_clone, not_used = \
be.beCopy(None, self.be_name, self.snapshot_name)
if ret != 0:
# If the above beCopy() failed we will try it
# without expecting the BE clone name to be
# returned by libbe. We do this in case an old
# version of libbe is on a system with
# a new version of pkg.
self.be_name_clone = self.be_name + "_" + \
self.snapshot_name
ret, not_used, not_used2 = \
be.beCopy(self.be_name_clone, \
self.be_name, self.snapshot_name)
if ret != 0:
logger.error(_("pkg: unable to create "
"BE {0}").format(
self.be_name_clone))
return
if be.beMount(self.be_name_clone, self.clone_dir) != 0:
logger.error(_("pkg: unable to mount BE "
"{name} on {clone_dir}").format(
name=self.be_name_clone,
clone_dir=self.clone_dir))
return
logger.error(_("The Boot Environment {name} failed "
"to be updated. A snapshot was taken before the "
"failed attempt and is mounted here {clone_dir}. "
"Use 'beadm unmount {clone_name}' and then "
"'beadm activate {clone_name}' if you wish to "
"boot to this BE.").format(name=self.be_name,
clone_dir=self.clone_dir,
clone_name=self.be_name_clone))
else:
if be.beRollback(self.be_name, self.snapshot_name) != 0:
logger.error("pkg: unable to rollback BE "
"{0}".format(self.be_name))
self.destroy_snapshot()
logger.error(_("The Boot Environment {bename} failed "
"to be updated. A snapshot was taken before the "
"failed attempt and has been restored so no "
"changes have been made to {bename}.").format(
bename=self.be_name))
class BootEnvNull(object):
"""BootEnvNull is a class that gets used when libbe doesn't exist."""
def __init__(self, img, progress_tracker=None):
pass
@staticmethod
def update_boot_archive():
pass
@staticmethod
def exists():
return False
@staticmethod
def libbe_exists():
return False
@staticmethod
def check_verify():
return False
@staticmethod
def split_be_entry(bee):
return None
@staticmethod
def copy_be(src_be_name, dst_be_name):
pass
@staticmethod
def rename_be(orig_name, new_name):
pass
@staticmethod
def destroy_be(be_name):
pass
@staticmethod
def cleanup_be(be_name):
pass
@staticmethod
def mount_be(be_name, mntpt, include_bpool=False):
return None
@staticmethod
def unmount_be(be_name, force=False):
return None
@staticmethod
def set_default_be(be_name):
pass
@staticmethod
def check_be_name(be_name):
if be_name:
raise api_errors.BENamingNotSupported(be_name)
@staticmethod
def get_be_list():
return []
@staticmethod
def get_be_names():
return []
@staticmethod
def get_be_name(path):
return None, None
@staticmethod
def get_uuid_be_dic():
return misc.EmptyDict
@staticmethod
def get_activated_be_name():
pass
@staticmethod
def get_active_be_name():
pass
@staticmethod
def create_backup_be(be_name=None):
if be_name is not None:
raise api_errors.BENameGivenOnDeadBE(be_name)
@staticmethod
def init_image_recovery(img, be_name=None):
if be_name is not None:
raise api_errors.BENameGivenOnDeadBE(be_name)
@staticmethod
def activate_image():
pass
@staticmethod
def restore_image():
pass
@staticmethod
def destroy_snapshot():
pass
@staticmethod
def restore_install_uninstall():
pass
@staticmethod
def activate_install_uninstall():
pass
if "bemgmt" in locals():
BootEnv = BeadmV2BootEnv
elif "be" in locals():
BootEnv = BeadmV1BootEnv
else:
BootEnv = BootEnvNull