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