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