pkgplan.py revision 2610
1057N/A#!/usr/bin/python
1057N/A#
1057N/A# CDDL HEADER START
1057N/A#
1057N/A# The contents of this file are subject to the terms of the
1057N/A# Common Development and Distribution License (the "License").
660N/A# You may not use this file except in compliance with the License.
1057N/A#
1057N/A# You can obtain a copy of the license at usr/src/OPENSOLARIS.LICENSE
1057N/A# or http://www.opensolaris.org/os/licensing.
1057N/A# See the License for the specific language governing permissions
1057N/A# and limitations under the License.
1057N/A#
1057N/A# When distributing Covered Code, include this CDDL HEADER in each
1057N/A# file and include the License file at usr/src/OPENSOLARIS.LICENSE.
1057N/A# If applicable, add the following below this CDDL HEADER, with the
1057N/A# fields enclosed by brackets "[]" replaced with your own identifying
1057N/A# information: Portions Copyright [yyyy] [name of copyright owner]
1057N/A#
1057N/A# CDDL HEADER END
1057N/A#
1057N/A
660N/A#
1109N/A# Copyright (c) 2007, 2012, Oracle and/or its affiliates. All rights reserved.
1109N/A#
660N/A
660N/Aimport itertools
1057N/Aimport cPickle as pickle
660N/A
1109N/Afrom pkg.client import global_settings
1057N/Alogger = global_settings.logger
1057N/A
1057N/Aimport pkg.actions
660N/Aimport pkg.actions.directory as directory
660N/Aimport pkg.client.api_errors as apx
660N/Aimport pkg.manifest as manifest
660N/Afrom pkg.misc import expanddirs, get_pkg_otw_size, EmptyI
1109N/A
684N/Aimport os.path
684N/A
684N/Aclass PkgPlan(object):
684N/A """A package plan takes two package FMRIs and an Image, and produces the
684N/A set of actions required to take the Image from the origin FMRI to the
684N/A destination FMRI.
684N/A
1109N/A If the destination FMRI is None, the package is removed.
1057N/A """
1057N/A
1057N/A __slots__ = [
684N/A "actions", "check_cancelation", "destination_fmri", "image",
684N/A "origin_fmri", "pkg_summary", "__destination_mfst",
684N/A "__license_status", "__origin_mfst", "__progtrack",
684N/A "__repair_actions", "__xferfiles", "__xfersize",
1109N/A "_autofix_pkgs", "__executed"
684N/A ]
684N/A
684N/A def __init__(self, image, progtrack, check_cancelation):
684N/A self.destination_fmri = None
684N/A self.__destination_mfst = manifest.NullFactoredManifest
684N/A
1057N/A self.origin_fmri = None
1057N/A self.__origin_mfst = manifest.NullFactoredManifest
1057N/A
684N/A self.actions = manifest.ManifestDifference([], [], [])
684N/A self.check_cancelation = check_cancelation
684N/A self.image = image
684N/A self.pkg_summary = None
684N/A
1057N/A self.__executed = False
1057N/A self.__license_status = {}
684N/A self.__progtrack = progtrack
1057N/A self.__repair_actions = {}
1057N/A self.__xferfiles = -1
1057N/A self.__xfersize = -1
684N/A self._autofix_pkgs = []
684N/A
684N/A def __str__(self):
684N/A s = "%s -> %s\n" % (self.origin_fmri, self.destination_fmri)
684N/A for src, dest in itertools.chain(*self.actions):
684N/A s += " %s -> %s\n" % (src, dest)
1057N/A return s
684N/A
684N/A def __add_license(self, src, dest):
684N/A """Adds a license status entry for the given src and dest
684N/A license actions.
684N/A
684N/A 'src' should be None or the source action for a license.
684N/A
684N/A 'dest' must be the destination action for a license."""
684N/A
684N/A self.__license_status[dest.attrs["license"]] = {
1109N/A "src": src,
1057N/A "dest": dest,
1057N/A "accepted": False,
1057N/A "displayed": False,
684N/A }
684N/A
684N/A @staticmethod
684N/A def __pickle_actions(actions):
1109N/A """Return a list of pickled actions."""
684N/A action_list = []
684N/A for pair in actions:
684N/A newpair = [None, None]
684N/A if pair[0]:
684N/A newpair[0] = pickle.dumps(pair[0])
684N/A if pair[1]:
1057N/A newpair[1] = pickle.dumps(pair[1])
1057N/A action_list.append(newpair)
1057N/A return action_list
684N/A
684N/A @staticmethod
684N/A def __unpickle_actions(pickled_actions):
684N/A """Return a list of unpickled actions."""
684N/A action_list = []
1057N/A for pair in pickled_actions:
1057N/A newpair = [None, None]
684N/A if pair[0]:
1057N/A newpair[0] = pickle.loads(str(pair[0]))
1057N/A if pair[1]:
1057N/A newpair[1] = pickle.loads(str(pair[1]))
684N/A action_list.append(newpair)
684N/A return action_list
684N/A
684N/A def setstate(self, state):
684N/A """Update the state of this object using the contents of
684N/A the supplied dictionary."""
1109N/A
684N/A import pkg.fmri
684N/A
684N/A # if there is no origin, don't allocate an fmri obj
684N/A if state["src"]:
1109N/A state["src"] = pkg.fmri.PkgFmri(state["src"])
684N/A
684N/A # if there is no destination, don't allocate an fmri obj
684N/A if state["dst"]:
1109N/A state["dst"] = pkg.fmri.PkgFmri(state["dst"])
684N/A
684N/A self.origin_fmri = state["src"]
684N/A self.destination_fmri = state["dst"]
1109N/A self.pkg_summary = state["summary"]
684N/A self.actions = manifest.ManifestDifference([], [], [])
684N/A self.actions.added.extend(
684N/A self.__unpickle_actions(state["add"]))
1109N/A self.actions.changed.extend(
684N/A self.__unpickle_actions(state["change"]))
684N/A self.actions.removed.extend(
684N/A self.__unpickle_actions(state["remove"]))
1109N/A for src, dest in itertools.chain(self.gen_update_actions(),
684N/A self.gen_install_actions()):
684N/A if dest.name == "license":
684N/A self.__add_license(src, dest)
684N/A
1109N/A def getstate(self):
1057N/A """Returns a dictionary containing the state of this object
1057N/A so that it can be easily stored using JSON, pickle, etc."""
1057N/A
1057N/A state = {}
1109N/A state["src"] = self.origin_fmri
684N/A state["dst"] = self.destination_fmri
684N/A state["summary"] = self.pkg_summary
684N/A state["add"] = self.__pickle_actions(self.actions.added)
1109N/A state["change"] = self.__pickle_actions(self.actions.changed)
684N/A state["remove"] = self.__pickle_actions(self.actions.removed)
684N/A return state
684N/A
684N/A def propose(self, of, om, df, dm):
684N/A """Propose origin and dest fmri, manifest"""
684N/A self.origin_fmri = of
684N/A self.__origin_mfst = om
1109N/A self.destination_fmri = df
679N/A self.__destination_mfst = dm
679N/A
684N/A def propose_repair(self, fmri, mfst, install, remove, autofix=False):
684N/A self.propose(fmri, mfst, fmri, mfst)
684N/A # self.origin_fmri = None
684N/A # I'd like a cleaner solution than this; we need to actually
684N/A # construct a list of actions as things currently are rather
684N/A # than just re-applying the current set of actions.
1109N/A #
684N/A # Create a list of (src, dst) pairs for the actions to send to
684N/A # execute_repair.
684N/A
1109N/A self.__repair_actions = {
684N/A # src is none for repairs.
684N/A "install": [(None, x) for x in install],
684N/A # dest is none for removals.
684N/A "remove": [(x, None) for x in remove],
1109N/A }
684N/A
684N/A if autofix:
684N/A self._autofix_pkgs.append(fmri)
684N/A
1109N/A def get_actions(self):
684N/A raise NotImplementedError()
684N/A
684N/A def get_nactions(self):
1109N/A return len(self.actions.added) + len(self.actions.changed) + \
684N/A len(self.actions.removed)
684N/A
684N/A def update_pkg_set(self, fmri_set):
684N/A """ updates a set of installed fmris to reflect
684N/A proposed new state"""
684N/A
684N/A if self.origin_fmri:
684N/A fmri_set.discard(self.origin_fmri)
1109N/A
684N/A if self.destination_fmri:
684N/A fmri_set.add(self.destination_fmri)
684N/A
684N/A def evaluate(self, old_excludes=EmptyI, new_excludes=EmptyI,
684N/A can_exclude=False):
684N/A """Determine the actions required to transition the package."""
684N/A
1109N/A # If new actions are being installed, check the destination
684N/A # manifest for signatures.
684N/A if self.destination_fmri is not None:
684N/A try:
684N/A dest_pub = self.image.get_publisher(
684N/A prefix=self.destination_fmri.publisher)
684N/A except apx.UnknownPublisher:
684N/A # Since user removed publisher, assume this is
1109N/A # the same as if they had set signature-policy
684N/A # ignore for the publisher.
684N/A sig_pol = None
684N/A else:
679N/A sig_pol = self.image.signature_policy.combine(
684N/A dest_pub.signature_policy)
684N/A
684N/A sigs = list(self.__destination_mfst.gen_actions_by_type(
1109N/A "signature", new_excludes))
684N/A if sig_pol and (sigs or sig_pol.name != "ignore"):
684N/A # Only perform signature verification logic if
684N/A # there are signatures or if signature-policy
684N/A # is not 'ignore'.
1109N/A
684N/A try:
684N/A sig_pol.process_signatures(sigs,
684N/A self.__destination_mfst.gen_actions(),
1109N/A dest_pub, self.image.trust_anchors,
684N/A self.image.cfg.get_policy(
684N/A "check-certificate-revocation"))
684N/A except apx.SigningException, e:
684N/A e.pfmri = self.destination_fmri
684N/A if isinstance(e, apx.BrokenChain):
684N/A e.ext_exs.extend(
684N/A self.image.bad_trust_anchors
684N/A )
1109N/A raise
1109N/A if can_exclude:
1109N/A if self.__destination_mfst is not None:
1109N/A self.__destination_mfst.exclude_content(
1109N/A new_excludes)
684N/A if self.__origin_mfst is not None and \
684N/A self.__destination_mfst != self.__origin_mfst:
684N/A self.__origin_mfst.exclude_content(old_excludes)
1109N/A old_excludes = EmptyI
684N/A new_excludes = EmptyI
684N/A
684N/A self.actions = self.__destination_mfst.difference(
1109N/A self.__origin_mfst, old_excludes, new_excludes)
1109N/A
684N/A # figure out how many implicit directories disappear in this
684N/A # transition and add directory remove actions. These won't
684N/A # do anything unless no pkgs reference that directory in
684N/A # new state....
1109N/A
684N/A # Retrieving origin_dirs first and then checking it for any
684N/A # entries allows avoiding an unnecessary expanddirs for the
684N/A # destination manifest when it isn't needed.
684N/A origin_dirs = expanddirs(self.__origin_mfst.get_directories(
684N/A old_excludes))
684N/A
684N/A # Manifest.get_directories() returns implicit directories, which
684N/A # means that this computation ends up re-adding all the explicit
1109N/A # directories getting removed to the removed list. This is
684N/A # ugly, but safe.
684N/A if origin_dirs:
684N/A absent_dirs = origin_dirs - \
684N/A expanddirs(self.__destination_mfst.get_directories(
684N/A new_excludes))
684N/A
684N/A for a in absent_dirs:
1109N/A self.actions.removed.append(
684N/A [directory.DirectoryAction(path=a), None])
684N/A
684N/A # Stash information needed by legacy actions.
684N/A self.pkg_summary = \
1109N/A self.__destination_mfst.get("pkg.summary",
684N/A self.__destination_mfst.get("description", "none provided"))
684N/A
684N/A # Add any install repair actions to the update list
684N/A self.actions.changed.extend(self.__repair_actions.get("install",
1109N/A EmptyI))
684N/A self.actions.removed.extend(self.__repair_actions.get("remove",
684N/A EmptyI))
684N/A
1109N/A # No longer needed.
684N/A self.__repair_actions = None
684N/A
684N/A for src, dest in itertools.chain(self.gen_update_actions(),
1109N/A self.gen_install_actions()):
679N/A if dest.name == "license":
660N/A self.__add_license(src, dest)
684N/A if not src:
684N/A # Initial installs require acceptance.
660N/A continue
684N/A src_ma = src.attrs.get("must-accept", False)
684N/A dest_ma = dest.attrs.get("must-accept", False)
684N/A if (dest_ma and src_ma) and \
1109N/A src.hash == dest.hash:
679N/A # If src action required acceptance,
684N/A # then license was already accepted
684N/A # before, and if the hashes are the
684N/A # same for the license payload, then
1109N/A # it doesn't need to be accepted again.
679N/A self.set_license_status(
684N/A dest.attrs["license"],
684N/A accepted=True)
684N/A
1109N/A def get_licenses(self):
1109N/A """A generator function that yields tuples of the form (license,
1109N/A entry). Where 'entry' is a dict containing the license status
1109N/A information."""
1109N/A
1109N/A for lic, entry in self.__license_status.iteritems():
1109N/A yield lic, entry
1109N/A
1109N/A def set_license_status(self, plicense, accepted=None, displayed=None):
1109N/A """Sets the license status for the given license entry.
1109N/A
1109N/A 'plicense' should be the value of the license attribute for the
1109N/A destination license action.
1109N/A
1109N/A 'accepted' is an optional parameter that can be one of three
1109N/A values:
1109N/A None leaves accepted status unchanged
1109N/A False sets accepted status to False
1109N/A True sets accepted status to True
1109N/A
1109N/A 'displayed' is an optional parameter that can be one of three
1109N/A values:
1109N/A None leaves displayed status unchanged
1109N/A False sets displayed status to False
1109N/A True sets displayed status to True"""
1109N/A
1109N/A entry = self.__license_status[plicense]
1109N/A if accepted is not None:
1057N/A entry["accepted"] = accepted
1057N/A if displayed is not None:
1057N/A entry["displayed"] = displayed
1057N/A
1109N/A def get_xferstats(self):
1109N/A if self.__xfersize != -1:
1109N/A return (self.__xferfiles, self.__xfersize)
1109N/A
1109N/A self.__xfersize = 0
1109N/A self.__xferfiles = 0
1109N/A for src, dest in itertools.chain(*self.actions):
1109N/A if dest and dest.needsdata(src, self):
1109N/A self.__xfersize += get_pkg_otw_size(dest)
1109N/A self.__xferfiles += 1
1109N/A if dest.name == "signature":
1109N/A self.__xfersize += \
1109N/A dest.get_action_chain_csize()
1109N/A self.__xferfiles += \
684N/A len(dest.attrs.get("chain",
684N/A "").split())
684N/A
1109N/A return (self.__xferfiles, self.__xfersize)
684N/A
684N/A def get_bytes_added(self):
684N/A """Return tuple of compressed bytes possibly downloaded
1109N/A and number of bytes laid down; ignore removals
1109N/A because they're usually pinned by snapshots"""
684N/A def sum_dest_size(a, b):
684N/A if b[1]:
684N/A return (a[0] + int(b[1].attrs.get("pkg.csize" ,0)),
1109N/A a[1] + int(b[1].attrs.get("pkg.size", 0)))
684N/A return (a[0], a[1])
684N/A
684N/A return reduce(sum_dest_size, itertools.chain(*self.actions),
1109N/A (0, 0))
1057N/A
1057N/A def get_xfername(self):
1057N/A if self.destination_fmri:
1109N/A return self.destination_fmri.get_name()
1057N/A if self.origin_fmri:
1057N/A return self.origin_fmri.get_name()
1057N/A return None
1109N/A
1057N/A def preexecute(self):
1057N/A """Perform actions required prior to installation or removal of
1057N/A a package.
1109N/A
1057N/A This method executes each action's preremove() or preinstall()
1057N/A methods, as well as any package-wide steps that need to be taken
1057N/A at such a time.
1057N/A """
1109N/A
1057N/A # Determine if license acceptance requirements have been met as
1057N/A # early as possible.
1057N/A errors = []
1109N/A for lic, entry in self.get_licenses():
1057N/A dest = entry["dest"]
1057N/A if (dest.must_accept and not entry["accepted"]) or \
1057N/A (dest.must_display and not entry["displayed"]):
1109N/A errors.append(apx.LicenseAcceptanceError(
1057N/A self.destination_fmri, **entry))
1057N/A
1057N/A if errors:
1109N/A raise apx.PkgLicenseErrors(errors)
1057N/A
1057N/A for src, dest in itertools.chain(*self.actions):
1057N/A if dest:
1109N/A dest.preinstall(self, src)
1057N/A else:
1057N/A src.preremove(self)
1057N/A
1109N/A def download(self):
1057N/A """Download data for any actions that need it."""
1057N/A self.__progtrack.download_start_pkg(self.get_xfername())
1057N/A mfile = self.image.transport.multi_file(self.destination_fmri,
1109N/A self.__progtrack, self.check_cancelation)
1057N/A
1057N/A if mfile is None:
1057N/A self.__progtrack.download_end_pkg()
1109N/A return
684N/A
684N/A for src, dest in itertools.chain(*self.actions):
684N/A if dest and dest.needsdata(src, self):
684N/A mfile.add_action(dest)
684N/A
684N/A mfile.wait_files()
684N/A self.__progtrack.download_end_pkg()
1109N/A
684N/A def gen_install_actions(self):
684N/A for src, dest in self.actions.added:
684N/A yield src, dest
684N/A
684N/A def gen_removal_actions(self):
684N/A for src, dest in self.actions.removed:
684N/A yield src, dest
684N/A
684N/A def gen_update_actions(self):
1109N/A for src, dest in self.actions.changed:
679N/A yield src, dest
684N/A
684N/A def execute_install(self, src, dest):
684N/A """ perform action for installation of package"""
1109N/A self.__executed = True
1057N/A try:
1057N/A dest.install(self, src)
1057N/A except (pkg.actions.ActionError, EnvironmentError):
1109N/A # Don't log these as they're expected, and should be
684N/A # handled by the caller.
684N/A raise
684N/A except Exception, e:
1109N/A logger.error("Action install failed for '%s' (%s):\n "
684N/A "%s: %s" % (dest.attrs.get(dest.key_attr, id(dest)),
684N/A self.destination_fmri.get_pkg_stem(),
684N/A e.__class__.__name__, e))
1109N/A raise
679N/A
684N/A def execute_update(self, src, dest):
684N/A """ handle action updates"""
684N/A self.__executed = True
1109N/A try:
684N/A dest.install(self, src)
684N/A except (pkg.actions.ActionError, EnvironmentError):
684N/A # Don't log these as they're expected, and should be
684N/A # handled by the caller.
1109N/A raise
679N/A except Exception, e:
684N/A logger.error("Action upgrade failed for '%s' (%s):\n "
684N/A "%s: %s" % (dest.attrs.get(dest.key_attr, id(dest)),
684N/A self.destination_fmri.get_pkg_stem(),
1109N/A e.__class__.__name__, e))
684N/A raise
684N/A
684N/A def execute_removal(self, src, dest):
684N/A """ handle action removals"""
1109N/A self.__executed = True
1057N/A try:
1057N/A src.remove(self)
1057N/A except (pkg.actions.ActionError, EnvironmentError):
1057N/A # Don't log these as they're expected, and should be
1109N/A # handled by the caller.
1057N/A raise
1057N/A except Exception, e:
1057N/A logger.error("Action removal failed for '%s' (%s):\n "
1109N/A "%s: %s" % (src.attrs.get(src.key_attr, id(src)),
1057N/A self.origin_fmri.get_pkg_stem(),
1057N/A e.__class__.__name__, e))
1057N/A raise
1057N/A
1057N/A def postexecute(self):
1057N/A """Perform actions required after install or remove of a pkg.
1057N/A
1109N/A This method executes each action's postremove() or postinstall()
1057N/A methods, as well as any package-wide steps that need to be taken
1057N/A at such a time.
1057N/A """
1109N/A # record that package states are consistent
1057N/A for src, dest in itertools.chain(*self.actions):
1057N/A if dest:
1057N/A dest.postinstall(self, src)
1109N/A else:
1057N/A src.postremove(self)
1057N/A
1057N/A def salvage(self, path):
1109N/A """Used to save unexpected files or directories found during
1057N/A plan execution. Salvaged items are tracked in the imageplan.
1057N/A """
1057N/A
1109N/A assert self.__executed
1057N/A spath = self.image.salvage(path)
1057N/A # get just the file path that was salvaged
1057N/A fpath = path[len(self.image.get_root()) + 1:]
1057N/A self.image.imageplan.salvaged.append((fpath, spath))
1057N/A
1109N/A def salvage_from(self, local_path, full_destination):
679N/A """move unpackaged contents to specified destination"""
679N/A # remove leading / if present
1057N/A if local_path.startswith(os.path.sep):
1057N/A local_path = local_path[1:]
1057N/A
1057N/A for fpath, spath in self.image.imageplan.salvaged[:]:
1057N/A if fpath.startswith(local_path):
684N/A self.image.imageplan.salvaged.remove((fpath, spath))
1057N/A break
1057N/A else:
1057N/A return
1057N/A
1057N/A self.image.recover(spath, full_destination)
1057N/A
1109N/A @property
684N/A def destination_manifest(self):
1057N/A return self.__destination_mfst
1057N/A
1057N/A def clear_dest_manifest(self):
1109N/A self.__destination_mfst = None
1057N/A
1057N/A @property
1057N/A def origin_manifest(self):
1109N/A return self.__origin_mfst
1057N/A
1057N/A def clear_origin_manifest(self):
1057N/A self.__origin_mfst = None
1109N/A