pkgplan.py revision 2261
248N/A#!/usr/bin/python
248N/A#
248N/A# CDDL HEADER START
248N/A#
248N/A# The contents of this file are subject to the terms of the
248N/A# Common Development and Distribution License (the "License").
248N/A# You may not use this file except in compliance with the License.
248N/A#
248N/A# You can obtain a copy of the license at usr/src/OPENSOLARIS.LICENSE
248N/A# or http://www.opensolaris.org/os/licensing.
248N/A# See the License for the specific language governing permissions
248N/A# and limitations under the License.
248N/A#
248N/A# When distributing Covered Code, include this CDDL HEADER in each
248N/A# file and include the License file at usr/src/OPENSOLARIS.LICENSE.
248N/A# If applicable, add the following below this CDDL HEADER, with the
248N/A# fields enclosed by brackets "[]" replaced with your own identifying
248N/A# information: Portions Copyright [yyyy] [name of copyright owner]
248N/A#
248N/A# CDDL HEADER END
248N/A#
248N/A
2490N/A#
248N/A# Copyright (c) 2007, 2011, Oracle and/or its affiliates. All rights reserved.
248N/A#
814N/A
814N/Aimport errno
2238N/Aimport itertools
814N/Aimport os
248N/A
248N/Afrom pkg.client import global_settings
248N/Alogger = global_settings.logger
248N/A
248N/Aimport pkg.actions
248N/Aimport pkg.actions.directory as directory
248N/Aimport pkg.client.api_errors as apx
844N/Aimport pkg.manifest as manifest
844N/Afrom pkg.misc import expanddirs, get_pkg_otw_size, EmptyI
248N/A
1273N/A
248N/Aclass PkgPlan(object):
248N/A """A package plan takes two package FMRIs and an Image, and produces the
248N/A set of actions required to take the Image from the origin FMRI to the
248N/A destination FMRI.
248N/A
248N/A If the destination FMRI is None, the package is removed.
248N/A """
248N/A
248N/A __slots__ = [
248N/A "actions", "check_cancelation", "destination_fmri", "image",
690N/A "origin_fmri", "pkg_summary", "__destination_mfst",
248N/A "__license_status", "__origin_mfst", "__progtrack",
248N/A "__repair_actions", "__xferfiles", "__xfersize",
690N/A "_autofix_pkgs"
248N/A ]
248N/A
248N/A def __init__(self, image, progtrack, check_cancelation):
248N/A self.destination_fmri = None
248N/A self.__destination_mfst = manifest.NullFactoredManifest
248N/A
248N/A self.origin_fmri = None
248N/A self.__origin_mfst = manifest.NullFactoredManifest
248N/A
248N/A self.actions = manifest.ManifestDifference([], [], [])
248N/A self.check_cancelation = check_cancelation
248N/A self.image = image
248N/A self.pkg_summary = None
248N/A
248N/A self.__license_status = {}
248N/A self.__progtrack = progtrack
248N/A self.__repair_actions = []
2490N/A self.__xferfiles = -1
248N/A self.__xfersize = -1
248N/A self._autofix_pkgs = []
248N/A
248N/A def __str__(self):
248N/A s = "%s -> %s\n" % (self.origin_fmri, self.destination_fmri)
248N/A
248N/A for src, dest in itertools.chain(*self.actions):
248N/A s += " %s -> %s\n" % (src, dest)
248N/A
248N/A return s
248N/A
def __add_license(self, src, dest):
"""Adds a license status entry for the given src and dest
license actions.
'src' should be None or the source action for a license.
'dest' must be the destination action for a license."""
self.__license_status[dest.attrs["license"]] = {
"src": src,
"dest": dest,
"accepted": False,
"displayed": False,
}
def propose(self, of, om, df, dm):
"""Propose origin and dest fmri, manifest"""
self.origin_fmri = of
self.__origin_mfst = om
self.destination_fmri = df
self.__destination_mfst = dm
def propose_repair(self, fmri, mfst, actions, autofix=False):
self.propose(fmri, mfst, fmri, mfst)
# self.origin_fmri = None
# I'd like a cleaner solution than this; we need to actually
# construct a list of actions as things currently are rather than
# just re-applying the current set of actions.
#
# Create a list of (src, dst) pairs for the actions to send to
# execute_repair. src is none in this case since we aren't
# upgrading, just repairing.
lst = [(None, x) for x in actions]
# Only install actions, no update or remove
self.__repair_actions = lst
if autofix:
self._autofix_pkgs.append(fmri)
def get_actions(self):
raise NotImplementedError()
def get_nactions(self):
return len(self.actions.added) + len(self.actions.changed) + \
len(self.actions.removed)
def update_pkg_set(self, fmri_set):
""" updates a set of installed fmris to reflect
proposed new state"""
if self.origin_fmri:
fmri_set.discard(self.origin_fmri)
if self.destination_fmri:
fmri_set.add(self.destination_fmri)
def evaluate(self, old_excludes=EmptyI, new_excludes=EmptyI):
"""Determine the actions required to transition the package."""
# If new actions are being installed, check the destination
# manifest for signatures.
if self.destination_fmri is not None:
try:
dest_pub = self.image.get_publisher(
prefix=self.destination_fmri.publisher)
except apx.UnknownPublisher:
# Since user removed publisher, assume this is
# the same as if they had set signature-policy
# ignore for the publisher.
sig_pol = None
else:
sig_pol = self.image.signature_policy.combine(
dest_pub.signature_policy)
sigs = list(self.__destination_mfst.gen_actions_by_type(
"signature", new_excludes))
if sig_pol and (sigs or sig_pol.name != "ignore"):
# Only perform signature verification logic if
# there are signatures or if signature-policy
# is not 'ignore'.
# Check that the publisher's CA certs validate
# against the image's trust anchors.
sig_pol.check_cas(dest_pub,
self.image.trust_anchors)
try:
sig_pol.process_signatures(sigs,
self.__destination_mfst.gen_actions(),
dest_pub)
self.__destination_mfst.exclude_content(
new_excludes)
except apx.SigningException, e:
e.pfmri = self.destination_fmri
raise
self.actions = self.__destination_mfst.difference(
self.__origin_mfst, old_excludes, new_excludes)
# figure out how many implicit directories disappear in this
# transition and add directory remove actions. These won't
# do anything unless no pkgs reference that directory in
# new state....
# Retrieving origin_dirs first and then checking it for any
# entries allows avoiding an unnecessary expanddirs for the
# destination manifest when it isn't needed.
origin_dirs = expanddirs(self.__origin_mfst.get_directories(
old_excludes))
# No longer needed.
self.__origin_mfst = None
if origin_dirs:
absent_dirs = origin_dirs - \
expanddirs(self.__destination_mfst.get_directories(
new_excludes))
for a in absent_dirs:
self.actions.removed.append(
[directory.DirectoryAction(path=a), None])
# Stash information needed by legacy actions.
self.pkg_summary = \
self.__destination_mfst.get("pkg.summary",
self.__destination_mfst.get("description", "none provided"))
# No longer needed.
self.__destination_mfst = None
# Add any repair actions to the update list
self.actions.changed.extend(self.__repair_actions)
for src, dest in itertools.chain(self.gen_update_actions(),
self.gen_install_actions()):
if dest.name == "license":
self.__add_license(src, dest)
if not src or self.__repair_actions:
# Never assume acceptance for
# fix/repair scenario.
continue
src_ma = src.attrs.get("must-accept", False)
dest_ma = dest.attrs.get("must-accept", False)
if (dest_ma and src_ma) and \
src.hash == dest.hash:
# If src action required acceptance,
# then license was already accepted
# before, and if the hashes are the
# same for the license payload, then
# it doesn't need to be accepted again.
self.set_license_status(
dest.attrs["license"],
accepted=True)
def get_licenses(self):
"""A generator function that yields tuples of the form (license,
entry). Where 'entry' is a dict containing the license status
information."""
for lic, entry in self.__license_status.iteritems():
yield lic, entry
def set_license_status(self, plicense, accepted=None, displayed=None):
"""Sets the license status for the given license entry.
'plicense' should be the value of the license attribute for the
destination license action.
'accepted' is an optional parameter that can be one of three
values:
None leaves accepted status unchanged
False sets accepted status to False
True sets accepted status to True
'displayed' is an optional parameter that can be one of three
values:
None leaves displayed status unchanged
False sets displayed status to False
True sets displayed status to True"""
entry = self.__license_status[plicense]
if accepted is not None:
entry["accepted"] = accepted
if displayed is not None:
entry["displayed"] = displayed
def get_xferstats(self):
if self.__xfersize != -1:
return (self.__xferfiles, self.__xfersize)
self.__xfersize = 0
self.__xferfiles = 0
for src, dest in itertools.chain(*self.actions):
if dest and dest.needsdata(src, self):
self.__xfersize += get_pkg_otw_size(dest)
self.__xferfiles += 1
return (self.__xferfiles, self.__xfersize)
def get_xfername(self):
if self.destination_fmri:
return self.destination_fmri.get_name()
if self.origin_fmri:
return self.origin_fmri.get_name()
return None
def preexecute(self):
"""Perform actions required prior to installation or removal of
a package.
This method executes each action's preremove() or preinstall()
methods, as well as any package-wide steps that need to be taken
at such a time.
"""
# Determine if license acceptance requirements have been met as
# early as possible.
errors = []
for lic, entry in self.get_licenses():
dest = entry["dest"]
if (dest.must_accept and not entry["accepted"]) or \
(dest.must_display and not entry["displayed"]):
errors.append(apx.LicenseAcceptanceError(
self.destination_fmri, **entry))
if errors:
raise apx.PkgLicenseErrors(errors)
for src, dest in itertools.chain(*self.actions):
if dest:
dest.preinstall(self, src)
else:
src.preremove(self)
def download(self):
"""Download data for any actions that need it."""
self.__progtrack.download_start_pkg(self.get_xfername())
mfile = self.image.transport.multi_file(self.destination_fmri,
self.__progtrack, self.check_cancelation)
if mfile is None:
self.__progtrack.download_end_pkg()
return
for src, dest in itertools.chain(*self.actions):
if dest and dest.needsdata(src, self):
mfile.add_action(dest)
mfile.wait_files()
self.__progtrack.download_end_pkg()
def gen_install_actions(self):
for src, dest in self.actions.added:
yield src, dest
def gen_removal_actions(self):
for src, dest in self.actions.removed:
yield src, dest
def gen_update_actions(self):
for src, dest in self.actions.changed:
yield src, dest
def execute_install(self, src, dest):
""" perform action for installation of package"""
try:
dest.install(self, src)
except pkg.actions.ActionError:
# Don't log these as they're expected, and should be
# handled by the caller.
raise
except Exception, e:
logger.error("Action install failed for '%s' (%s):\n "
"%s: %s" % (dest.attrs.get(dest.key_attr, id(dest)),
self.destination_fmri.get_pkg_stem(),
e.__class__.__name__, e))
raise
def execute_update(self, src, dest):
""" handle action updates"""
try:
dest.install(self, src)
except pkg.actions.ActionError:
# Don't log these as they're expected, and should be
# handled by the caller.
raise
except Exception, e:
logger.error("Action upgrade failed for '%s' (%s):\n "
"%s: %s" % (dest.attrs.get(dest.key_attr, id(dest)),
self.destination_fmri.get_pkg_stem(),
e.__class__.__name__, e))
raise
def execute_removal(self, src, dest):
""" handle action removals"""
try:
src.remove(self)
except pkg.actions.ActionError:
# Don't log these as they're expected, and should be
# handled by the caller.
raise
except Exception, e:
logger.error("Action removal failed for '%s' (%s):\n "
"%s: %s" % (src.attrs.get(src.key_attr, id(src)),
self.origin_fmri.get_pkg_stem(),
e.__class__.__name__, e))
raise
def postexecute(self):
"""Perform actions required after install or remove of a pkg.
This method executes each action's postremove() or postinstall()
methods, as well as any package-wide steps that need to be taken
at such a time.
"""
# record that package states are consistent
for src, dest in itertools.chain(*self.actions):
if dest:
dest.postinstall(self, src)
else:
src.postremove(self)