#!/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, 2013, Oracle and/or its affiliates. All rights reserved.
#
"""This module provides the supported, documented interface for clients to
interface with the pkg(5) system.
Refer to pkg.api_common and pkg.plandesc for additional core class
documentation.
Consumers should catch ApiException when calling any API function, and
may optionally catch any subclass of ApiException for further, specific
error handling.
"""
#
# this file is not completely pylint clean
#
# pylint: disable=C0111,C0301,C0321,E0702,R0201,W0102
# pylint: disable=W0212,W0511,W0612,W0613,W0702
#
# C0111 Missing docstring
# C0301 Line too long
# C0321 More than one statement on a single line
# E0702 Raising NoneType while only classes, instances or string are allowed
# R0201 Method could be a function
# W0102 Dangerous default value %s as argument
# W0212 Access to a protected member %s of a client class
# W0511 XXX
# W0612 Unused variable '%s'
# W0613 Unused argument '%s'
# W0702 No exception type(s) specified
#
import collections
import copy
import datetime
import errno
import fnmatch
import glob
import os
import shutil
import simplejson as json
import sys
import tempfile
import threading
import time
import urllib
import pkg.client.api_errors as apx
import pkg.client.bootenv as bootenv
import pkg.client.history as history
import pkg.client.image as image
import pkg.client.imageconfig as imgcfg
import pkg.client.imageplan as imageplan
import pkg.client.imagetypes as imgtypes
import pkg.client.indexer as indexer
import pkg.client.pkgdefs as pkgdefs
import pkg.client.plandesc as plandesc
import pkg.client.publisher as publisher
import pkg.client.query_parser as query_p
import pkg.fmri as fmri
import pkg.mediator as med
import pkg.misc as misc
import pkg.nrlock
import pkg.p5i as p5i
import pkg.p5s as p5s
import pkg.portable as portable
import pkg.search_errors as search_errors
import pkg.version
from pkg.api_common import (PackageInfo, LicenseInfo, PackageCategory,
_get_pkg_cat_data)
from pkg.client import global_settings
from pkg.client.debugvalues import DebugValues
from pkg.client.pkgdefs import * # pylint: disable=W0401
from pkg.smf import NonzeroExitException
# we import PlanDescription here even though it isn't used so that consumers
# of the api still have access to the class definition and are able to do
# things like help(pkg.client.api.PlanDescription)
from pkg.client.plandesc import PlanDescription # pylint: disable=W0611
CURRENT_API_VERSION = 76
COMPATIBLE_API_VERSIONS = frozenset([72, 73, 74, 75, CURRENT_API_VERSION])
CURRENT_P5I_VERSION = 1
# Image type constants.
IMG_TYPE_NONE = imgtypes.IMG_NONE # No image.
IMG_TYPE_ENTIRE = imgtypes.IMG_ENTIRE # Full image ('/').
IMG_TYPE_PARTIAL = imgtypes.IMG_PARTIAL # Not yet implemented.
IMG_TYPE_USER = imgtypes.IMG_USER # Not '/'; some other location.
# History result constants.
RESULT_CANCELED = history.RESULT_CANCELED
RESULT_NOTHING_TO_DO = history.RESULT_NOTHING_TO_DO
RESULT_SUCCEEDED = history.RESULT_SUCCEEDED
RESULT_FAILED_BAD_REQUEST = history.RESULT_FAILED_BAD_REQUEST
RESULT_FAILED_CONFIGURATION = history.RESULT_FAILED_CONFIGURATION
RESULT_FAILED_CONSTRAINED = history.RESULT_FAILED_CONSTRAINED
RESULT_FAILED_LOCKED = history.RESULT_FAILED_LOCKED
RESULT_FAILED_SEARCH = history.RESULT_FAILED_SEARCH
RESULT_FAILED_STORAGE = history.RESULT_FAILED_STORAGE
RESULT_FAILED_TRANSPORT = history.RESULT_FAILED_TRANSPORT
RESULT_FAILED_ACTUATOR = history.RESULT_FAILED_ACTUATOR
RESULT_FAILED_OUTOFMEMORY = history.RESULT_FAILED_OUTOFMEMORY
RESULT_CONFLICTING_ACTIONS = history.RESULT_CONFLICTING_ACTIONS
RESULT_FAILED_UNKNOWN = history.RESULT_FAILED_UNKNOWN
# Globals.
logger = global_settings.logger
class _LockedGenerator(object):
"""This is a private class and should not be used by API consumers.
This decorator class wraps API generator functions, managing the
activity and cancelation locks. Due to implementation differences
in the decorator protocol, the decorator must be used with
parenthesis in order for this to function correctly. Always
decorate functions @_LockedGenerator()."""
def __init__(self, *d_args, **d_kwargs):
object.__init__(self)
def __call__(self, f):
def wrapper(*fargs, **f_kwargs):
instance, fargs = fargs[0], fargs[1:]
instance._acquire_activity_lock()
instance._enable_cancel()
clean_exit = True
canceled = False
try:
for v in f(instance, *fargs, **f_kwargs):
yield v
except GeneratorExit:
return
except apx.CanceledException:
canceled = True
raise
except Exception:
clean_exit = False
raise
finally:
if canceled:
instance._cancel_done()
elif clean_exit:
try:
instance._disable_cancel()
except apx.CanceledException:
instance._cancel_done()
instance._activity_lock.release()
raise
else:
instance._cancel_cleanup_exception()
instance._activity_lock.release()
return wrapper
class _LockedCancelable(object):
"""This is a private class and should not be used by API consumers.
This decorator class wraps non-generator cancelable API functions,
managing the activity and cancelation locks. Due to implementation
differences in the decorator protocol, the decorator must be used with
parenthesis in order for this to function correctly. Always
decorate functions @_LockedCancelable()."""
def __init__(self, *d_args, **d_kwargs):
object.__init__(self)
def __call__(self, f):
def wrapper(*fargs, **f_kwargs):
instance, fargs = fargs[0], fargs[1:]
instance._acquire_activity_lock()
instance._enable_cancel()
clean_exit = True
canceled = False
try:
return f(instance, *fargs, **f_kwargs)
except apx.CanceledException:
canceled = True
raise
except Exception:
clean_exit = False
raise
finally:
instance._img.cleanup_downloads()
try:
if int(os.environ.get("PKG_DUMP_STATS",
0)) > 0:
instance._img.transport.stats.dump()
except ValueError:
# Don't generate stats if an invalid
# value is supplied.
pass
if canceled:
instance._cancel_done()
elif clean_exit:
try:
instance._disable_cancel()
except apx.CanceledException:
instance._cancel_done()
# if f() acquired the image
# lock, drop it
if instance._img.locked:
instance._img.unlock()
instance._activity_lock.release()
raise
else:
instance._cancel_cleanup_exception()
# if f() acquired the image lock, drop it
if instance._img.locked:
instance._img.unlock()
instance._activity_lock.release()
return wrapper
class ImageInterface(object):
"""This class presents an interface to images that clients may use.
There is a specific order of methods which must be used to install
or uninstall packages, or update an image. First, a gen_plan_* method
must be called. After that method completes successfully, describe may
be called, and prepare must be called. Finally, execute_plan may be
called to implement the previous created plan. The other methods
do not have an ordering imposed upon them, and may be used as
needed. Cancel may only be invoked while a cancelable method is
running."""
FACET_ALL = 0
FACET_IMAGE = 1
FACET_INSTALLED = 2
FACET_SRC_SYSTEM = pkg.facet.Facets.FACET_SRC_SYSTEM
FACET_SRC_LOCAL = pkg.facet.Facets.FACET_SRC_LOCAL
FACET_SRC_PARENT = pkg.facet.Facets.FACET_SRC_PARENT
# Constants used to reference specific values that info can return.
INFO_FOUND = 0
INFO_MISSING = 1
INFO_ILLEGALS = 3
LIST_ALL = 0
LIST_INSTALLED = 1
LIST_INSTALLED_NEWEST = 2
LIST_NEWEST = 3
LIST_UPGRADABLE = 4
MATCH_EXACT = 0
MATCH_FMRI = 1
MATCH_GLOB = 2
VARIANT_ALL = 0
VARIANT_ALL_POSSIBLE = 1
VARIANT_IMAGE = 2
VARIANT_IMAGE_POSSIBLE = 3
VARIANT_INSTALLED = 4
VARIANT_INSTALLED_POSSIBLE = 5
def __init__(self, img_path, version_id, progresstracker,
cancel_state_callable, pkg_client_name, exact_match=True,
cmdpath=None):
"""Constructs an ImageInterface object.
'img_path' is the absolute path to an existing image or to a
path from which to start looking for an image. To control this
behaviour use the 'exact_match' parameter.
'version_id' indicates the version of the api the client is
expecting to use.
'progresstracker' is the ProgressTracker object the client wants
the api to use for UI progress callbacks.
'cancel_state_callable' is an optional function reference that
will be called if the cancellable status of an operation
changes.
'pkg_client_name' is a string containing the name of the client,
such as "pkg".
'exact_match' is a boolean indicating whether the API should
attempt to find a usable image starting from the specified
directory, going up to the filesystem root until it finds one.
If set to True, an image must exist at the location indicated
by 'img_path'.
"""
if version_id not in COMPATIBLE_API_VERSIONS:
raise apx.VersionException(CURRENT_API_VERSION,
version_id)
if sys.path[0].startswith("/dev/fd/"):
#
# Normally when the kernel forks off an interpreted
# program, it executes the interpreter with the first
# argument being the path to the interpreted program
# we're executing. But in the case of suid scripts
# this presents a security problem because that path
# could be updated after exec but before the
# interpreter opens reads the program. To avoid this
# race, for suid script the kernel replaces the name
# of the interpreted program with /dev/fd/###, and
# opens the interpreted program such that it can be
# read from the specified file descriptor device node.
# So if we detect that path[0] (which should be then
# interpreted program name) is a /dev/fd/ path, that
# means we're being run as an suid script, which we
# don't really want to support. (Since this breaks
# our subsequent code that attempt to determine the
# name of the executable we are running as.)
#
raise apx.SuidUnsupportedError()
# The image's History object will use client_name from
# global_settings, but if the program forgot to set it,
# we'll go ahead and do so here.
if global_settings.client_name is None:
global_settings.client_name = pkg_client_name
if cmdpath == None:
cmdpath = misc.api_cmdpath()
self.cmdpath = cmdpath
# prevent brokeness in the test suite
if self.cmdpath and \
"PKG_NO_RUNPY_CMDPATH" in os.environ and \
self.cmdpath.endswith(os.sep + "run.py"):
raise RuntimeError, """
An ImageInterface object was allocated from within ipkg test suite and
cmdpath was not explicitly overridden. Please make sure to set
explicitly set cmdpath when allocating an ImageInterface object, or
override cmdpath when allocating an Image object by setting PKG_CMDPATH
in the environment or by setting simulate_cmdpath in DebugValues."""
if isinstance(img_path, basestring):
# Store this for reset().
self._img_path = img_path
self._img = image.Image(img_path,
progtrack=progresstracker,
user_provided_dir=exact_match,
cmdpath=self.cmdpath)
# Store final image path.
self._img_path = self._img.get_root()
elif isinstance(img_path, image.Image):
# This is a temporary, special case for client.py
# until the image api is complete.
self._img = img_path
self._img_path = img_path.get_root()
else:
# API consumer passed an unknown type for img_path.
raise TypeError(_("Unknown img_path type."))
self.__progresstracker = progresstracker
lin = None
if self._img.linked.ischild():
lin = self._img.linked.child_name
self.__progresstracker.set_linked_name(lin)
self.__cancel_state_callable = cancel_state_callable
self.__plan_type = None
self.__api_op = None
self.__plan_desc = None
self.__planned_children = False
self.__prepared = False
self.__executed = False
self.__be_activate = True
self.__backup_be_name = None
self.__be_name = None
self.__can_be_canceled = False
self.__canceling = False
self._activity_lock = pkg.nrlock.NRLock()
self.__blocking_locks = False
self._img.blocking_locks = self.__blocking_locks
self.__cancel_lock = pkg.nrlock.NRLock()
self.__cancel_cv = threading.Condition(self.__cancel_lock)
self.__backup_be = None # create if needed
self.__new_be = None # create if needed
self.__alt_sources = {}
def __set_blocking_locks(self, value):
self._activity_lock.acquire()
self.__blocking_locks = value
self._img.blocking_locks = value
self._activity_lock.release()
def __set_img_alt_sources(self, repos):
"""Private helper function to change image to use alternate
package sources if applicable."""
# When using alternate package sources with the image, the
# result is a composite of the package data already known
# by the image and the alternate sources.
if repos:
self._img.set_alt_pkg_sources(
self.__get_alt_pkg_data(repos))
else:
self._img.set_alt_pkg_sources(None)
@_LockedCancelable()
def set_alt_repos(self, repos):
"""Public function to specify alternate package sources."""
self.__set_img_alt_sources(repos)
blocking_locks = property(lambda self: self.__blocking_locks,
__set_blocking_locks, doc="A boolean value indicating whether "
"the API should wait until the image interface can be locked if "
"it is in use by another thread or process. Clients should be "
"aware that there is no timeout mechanism in place if blocking is "
"enabled, and so should take steps to remain responsive to user "
"input or provide a way for users to cancel operations.")
@property
def excludes(self):
"""The list of excludes for the image."""
return self._img.list_excludes()
@property
def img(self):
"""Private; public access to this property will be removed at
a later date. Do not use."""
return self._img
@property
def img_type(self):
"""Returns the IMG_TYPE constant for the image's type."""
if not self._img:
return None
return self._img.image_type(self._img.root)
@property
def is_liveroot(self):
"""A boolean indicating whether the image to be modified is
for the live system root."""
return self._img.is_liveroot()
@property
def is_zone(self):
"""A boolean value indicating whether the image is a zone."""
return self._img.is_zone()
@property
def is_active_liveroot_be(self):
"""A boolean indicating whether the image to be modified is
the active BE for the system's root image."""
if not self._img.is_liveroot():
return False
try:
be_name, be_uuid = bootenv.BootEnv.get_be_name(
self._img.root)
return be_name == \
bootenv.BootEnv.get_activated_be_name()
except apx.BEException:
# If boot environment logic isn't supported, return
# False. This is necessary for user images and for
# the test suite.
return False
@property
def img_plandir(self):
"""A path to the image planning directory."""
plandir = self._img.plandir
misc.makedirs(plandir)
return plandir
@property
def last_modified(self):
"""A datetime object representing when the image's metadata was
last updated."""
return self._img.get_last_modified()
def __set_progresstracker(self, value):
self._activity_lock.acquire()
self.__progresstracker = value
# tell the progress tracker about this image's name
lin = None
if self._img.linked.ischild():
lin = self._img.linked.child_name
self.__progresstracker.set_linked_name(lin)
self._activity_lock.release()
progresstracker = property(lambda self: self.__progresstracker,
__set_progresstracker, doc="The current ProgressTracker object. "
"This value should only be set when no other API calls are in "
"progress.")
@property
def mediators(self):
"""A dictionary of the mediators and their configured version
and implementation of the form:
{
mediator-name: {
"version": mediator-version-string,
"version-source": (site|vendor|system|local),
"implementation": mediator-implementation-string,
"implementation-source": (site|vendor|system|local),
}
}
'version' is an optional string that specifies the version
(expressed as a dot-separated sequence of non-negative
integers) of the mediator for use.
'version-source' is a string describing the source of the
selected version configuration. It indicates how the
version component of the mediation was selected.
'implementation' is an optional string that specifies the
implementation of the mediator for use in addition to or
instead of 'version'.
'implementation-source' is a string describing the source of
the selected implementation configuration. It indicates how
the implementation component of the mediation was selected.
"""
ret = {}
for m, mvalues in self._img.cfg.mediators.iteritems():
ret[m] = copy.copy(mvalues)
if "version" in ret[m]:
# Don't expose internal Version object to
# external consumers.
ret[m]["version"] = \
ret[m]["version"].get_short_version()
if "implementation-version" in ret[m]:
# Don't expose internal Version object to
# external consumers.
ret[m]["implementation-version"] = \
ret[m]["implementation-version"].get_short_version()
return ret
@property
def root(self):
"""The absolute pathname of the filesystem root of the image.
This property is read-only."""
if not self._img:
return None
return self._img.root
@staticmethod
def check_be_name(be_name):
bootenv.BootEnv.check_be_name(be_name)
return True
def __cert_verify(self, log_op_end=None):
"""Verify validity of certificates. Any apx.ExpiringCertificate
exceptions are caught here, a message is displayed, and
execution continues.
All other exceptions will be passed to the calling context.
The caller can also set log_op_end to a list of exceptions
that should result in a call to self.log_operation_end()
before the exception is passed on.
"""
if log_op_end == None:
log_op_end = []
# we always explicitly handle apx.ExpiringCertificate
assert apx.ExpiringCertificate not in log_op_end
try:
self._img.check_cert_validity()
except apx.ExpiringCertificate, e:
logger.error(e)
except:
exc_type, exc_value, exc_traceback = sys.exc_info()
if exc_type in log_op_end:
self.log_operation_end(error=exc_value)
raise
def __refresh_publishers(self):
"""Refresh publisher metadata; this should only be used by
functions in this module for implicit refresh cases."""
#
# Verify validity of certificates before possibly
# attempting network operations.
#
self.__cert_verify()
try:
self._img.refresh_publishers(immediate=True,
progtrack=self.__progresstracker)
except apx.ImageFormatUpdateNeeded:
# If image format update is needed to perform refresh,
# continue on and allow failure to happen later since
# an implicit refresh failing for this reason isn't
# important. (This allows planning installs and updates
# before the format of an image is updated. Yes, this
# means that if the refresh was needed to do that, then
# this isn't useful, but this is as good as it gets.)
logger.warning(_("Skipping publisher metadata refresh;"
"image rooted at %s must have its format updated "
"before a refresh can occur.") % self._img.root)
def _acquire_activity_lock(self):
"""Private helper method to aqcuire activity lock."""
rc = self._activity_lock.acquire(
blocking=self.__blocking_locks)
if not rc:
raise apx.ImageLockedError()
def __plan_common_start(self, operation, noexecute, backup_be,
backup_be_name, new_be, be_name, be_activate):
"""Start planning an operation:
Acquire locks.
Log the start of the operation.
Check be_name."""
self._acquire_activity_lock()
try:
self._enable_cancel()
if self.__plan_type is not None:
raise apx.PlanExistsException(
self.__plan_type)
self._img.lock(allow_unprivileged=noexecute)
except:
self._cancel_cleanup_exception()
self._activity_lock.release()
raise
assert self._activity_lock._is_owned()
self.log_operation_start(operation)
self.__backup_be = backup_be
self.__backup_be_name = backup_be_name
self.__new_be = new_be
self.__be_activate = be_activate
self.__be_name = be_name
for val in (self.__be_name, self.__backup_be_name):
if val is not None:
self.check_be_name(val)
if not self._img.is_liveroot():
raise apx.BENameGivenOnDeadBE(val)
def __plan_common_finish(self):
"""Finish planning an operation."""
assert self._activity_lock._is_owned()
self._img.cleanup_downloads()
self._img.unlock()
try:
if int(os.environ.get("PKG_DUMP_STATS", 0)) > 0:
self._img.transport.stats.dump()
except ValueError:
# Don't generate stats if an invalid value
# is supplied.
pass
self._activity_lock.release()
def __set_be_creation(self):
"""Figure out whether or not we'd create a new or backup boot
environment given inputs and plan. Toss cookies if we need a
new be and can't have one."""
if not self._img.is_liveroot():
self.__backup_be = False
self.__new_be = False
return
if self.__new_be is None:
# If image policy requires a new BE or the plan requires
# it, then create a new BE.
self.__new_be = (self._img.cfg.get_policy_str(
imgcfg.BE_POLICY) == "always-new" or
self._img.imageplan.reboot_needed())
elif self.__new_be is False and \
self._img.imageplan.reboot_needed():
raise apx.ImageUpdateOnLiveImageException()
if not self.__new_be and self.__backup_be is None:
# Create a backup be if allowed by policy (note that the
# 'default' policy is currently an alias for
# 'create-backup') ...
allow_backup = self._img.cfg.get_policy_str(
imgcfg.BE_POLICY) in ("default",
"create-backup")
self.__backup_be = False
if allow_backup:
# ...when packages are being
# updated...
for src, dest in self._img.imageplan.plan_desc:
if src and dest:
self.__backup_be = True
break
if allow_backup and not self.__backup_be:
# ...or if new packages that have
# reboot-needed=true are being
# installed.
self.__backup_be = \
self._img.imageplan.reboot_advised()
def abort(self, result=RESULT_FAILED_UNKNOWN):
"""Indicate that execution was unexpectedly aborted and log
operation failure if possible."""
try:
# This can raise if, for example, we're aborting
# because we have a PipeError and we can no longer
# write. So supress problems here.
if self.__progresstracker:
self.__progresstracker.flush()
except:
pass
self._img.history.abort(result)
def avoid_pkgs(self, fmri_strings, unavoid=False):
"""Avoid/Unavoid one or more packages. It is an error to
avoid an installed package, or unavoid one that would
be installed."""
self._acquire_activity_lock()
try:
if not unavoid:
self._img.avoid_pkgs(fmri_strings,
progtrack=self.__progresstracker,
check_cancel=self.__check_cancel)
else:
self._img.unavoid_pkgs(fmri_strings,
progtrack=self.__progresstracker,
check_cancel=self.__check_cancel)
finally:
self._activity_lock.release()
return True
def gen_available_mediators(self):
"""A generator function that yields tuples of the form (mediator,
mediations), where mediator is the name of the provided mediation
and mediations is a list of dictionaries of possible mediations
to set, provided by installed packages, of the form:
{
mediator-name: {
"version": mediator-version-string,
"version-source": (site|vendor|system|local),
"implementation": mediator-implementation-string,
"implementation-source": (site|vendor|system|local),
}
}
'version' is an optional string that specifies the version
(expressed as a dot-separated sequence of non-negative
integers) of the mediator for use.
'version-source' is a string describing how the version
component of the mediation will be evaluated during
mediation. (The priority.)
'implementation' is an optional string that specifies the
implementation of the mediator for use in addition to or
instead of 'version'.
'implementation-source' is a string describing how the
implementation component of the mediation will be evaluated
during mediation. (The priority.)
The list of possible mediations returned for each mediator is
ordered by source in the sequence 'site', 'vendor', 'system',
and then by version and implementation. It does not include
mediations that exist only in the image configuration.
"""
ret = collections.defaultdict(set)
excludes = self._img.list_excludes()
for f in self._img.gen_installed_pkgs():
mfst = self._img.get_manifest(f)
for m, mediations in mfst.gen_mediators(
excludes=excludes):
ret[m].update(mediations)
for mediator in sorted(ret):
for med_priority, med_ver, med_impl in sorted(
ret[mediator], cmp=med.cmp_mediations):
val = {}
if med_ver:
# Don't expose internal Version object
# to callers.
val["version"] = \
med_ver.get_short_version()
if med_impl:
val["implementation"] = med_impl
ret_priority = med_priority
if not ret_priority:
# For consistency with the configured
# case, list source as this.
ret_priority = "system"
# Always set both to be consistent
# with @mediators.
val["version-source"] = ret_priority
val["implementation-source"] = \
ret_priority
yield mediator, val
def get_avoid_list(self):
"""Return list of tuples of (pkg stem, pkgs w/ group
dependencies on this) """
return [a for a in self._img.get_avoid_dict().iteritems()]
def gen_facets(self, facet_list, patterns=misc.EmptyI):
"""A generator function that produces tuples of the form:
(
name, - (string) facet name (e.g. facet.doc)
value - (boolean) current facet value
src - (string) source for the value
masked - (boolean) is the facet maksed by another
)
Results are always sorted by facet name.
'facet_list' is one of the following constant values indicating
which facets should be returned based on how they were set:
FACET_ALL
Return all facets set in the image and all
facets listed in installed packages.
FACET_IMAGE
Return only the facets set in the image.
FACET_INSTALLED
Return only the facets listed in installed
packages.
'patterns' is an optional list of facet wildcard strings to
filter results by."""
facets = self._img.cfg.facets
if facet_list != self.FACET_INSTALLED:
# Include all facets set in image.
fimg = set(facets.keys())
else:
# Don't include any set only in image.
fimg = set()
# Get all facets found in packages and determine state.
fpkg = set()
excludes = self._img.list_excludes()
if facet_list != self.FACET_IMAGE:
for f in self._img.gen_installed_pkgs():
# The manifest must be loaded without
# pre-applying excludes so that gen_facets() can
# choose how to filter the actions.
mfst = self._img.get_manifest(f,
ignore_excludes=True)
for facet in mfst.gen_facets(excludes=excludes):
# Use Facets object to determine
# effective facet state.
fpkg.add(facet)
# Generate the results.
for name in misc.yield_matching("facet.", sorted(fimg | fpkg),
patterns):
# check if the facet is explicitly set.
if name not in facets:
# The image's Facets dictionary will return
# the effective value for any facets not
# explicitly set in the image (wildcards or
# implicit). _match_src() will tell us how
# that effective value was determined (via a
# local or inherited wildcard facet, or via a
# system default).
src = facets._match_src(name)
yield (name, facets[name], src, False)
continue
# This is an explicitly set facet.
for value, src, masked in facets._src_values(name):
yield (name, value, src, masked)
def gen_variants(self, variant_list, patterns=misc.EmptyI):
"""A generator function that produces tuples of the form:
(
name, - (string) variant name (e.g. variant.arch)
value - (string) current variant value,
possible - (list) list of possible variant values based
on installed packages; empty unless using
*_POSSIBLE variant_list.
)
Results are always sorted by variant name.
'variant_list' is one of the following constant values indicating
which variants should be returned based on how they were set:
VARIANT_ALL
Return all variants set in the image and all
variants listed in installed packages.
VARIANT_ALL_POSSIBLE
Return possible variant values (those found in
any installed package) for all variants set in
the image and all variants listed in installed
packages.
VARIANT_IMAGE
Return only the variants set in the image.
VARIANT_IMAGE_POSSIBLE
Return possible variant values (those found in
any installed package) for only the variants set
in the image.
VARIANT_INSTALLED
Return only the variants listed in installed
packages.
VARIANT_INSTALLED_POSSIBLE
Return possible variant values (those found in
any installed package) for only the variants
listed in installed packages.
'patterns' is an optional list of variant wildcard strings to
filter results by."""
variants = self._img.cfg.variants
if variant_list != self.VARIANT_INSTALLED and \
variant_list != self.VARIANT_INSTALLED_POSSIBLE:
# Include all variants set in image.
vimg = set(variants.keys())
else:
# Don't include any set only in image.
vimg = set()
# Get all variants found in packages and determine state.
vpkg = {}
excludes = self._img.list_excludes()
vposs = collections.defaultdict(set)
if variant_list != self.VARIANT_IMAGE:
# Only incur the overhead of reading through all
# installed packages if not just listing variants set in
# image or listing possible values for them.
for f in self._img.gen_installed_pkgs():
# The manifest must be loaded without
# pre-applying excludes so that gen_variants()
# can choose how to filter the actions.
mfst = self._img.get_manifest(f,
ignore_excludes=True)
for variant, vals in mfst.gen_variants(
excludes=excludes):
# Unlike facets, Variants class doesn't
# handle implicitly set values.
if variant[:14] == "variant.debug.":
# Debug variants are implicitly
# false and are not required
# to be set explicitly in the
# image.
vpkg[variant] = variants.get(
variant, "false")
elif variant not in vimg:
# Although rare, packages with
# unknown variants (those not
# set in the image) can be
# installed as long as content
# does not conflict. For those
# variants, return None.
vpkg[variant] = \
variants.get(variant)
if (variant_list == \
self.VARIANT_ALL_POSSIBLE or
variant_list == \
self.VARIANT_IMAGE_POSSIBLE or
variant_list == \
self.VARIANT_INSTALLED_POSSIBLE):
# Build possible list of variant
# values.
vposs[variant].update(set(vals))
# Generate the results.
for name in misc.yield_matching("variant.",
sorted(vimg | set(vpkg.keys())), patterns):
try:
yield (name, vpkg[name], sorted(vposs[name]))
except KeyError:
yield (name, variants[name],
sorted(vposs[name]))
def freeze_pkgs(self, fmri_strings, dry_run=False, comment=None,
unfreeze=False):
"""Freeze/Unfreeze one or more packages."""
# Comment is only a valid parameter if a freeze is happening.
assert not comment or not unfreeze
self._acquire_activity_lock()
try:
if unfreeze:
return self._img.unfreeze_pkgs(fmri_strings,
progtrack=self.__progresstracker,
check_cancel=self.__check_cancel,
dry_run=dry_run)
else:
return self._img.freeze_pkgs(fmri_strings,
progtrack=self.__progresstracker,
check_cancel=self.__check_cancel,
dry_run=dry_run, comment=comment)
finally:
self._activity_lock.release()
def get_frozen_list(self):
"""Return list of tuples of (pkg fmri, reason package was
frozen, timestamp when package was frozen)."""
return self._img.get_frozen_list()
def __plan_common_exception(self, log_op_end_all=False):
"""Deal with exceptions that can occur while planning an
operation. Any exceptions generated here are passed
onto the calling context. By default all exceptions
will result in a call to self.log_operation_end() before
they are passed onto the calling context."""
exc_type, exc_value, exc_traceback = sys.exc_info()
if exc_type == apx.PlanCreationException:
self.__set_history_PlanCreationException(exc_value)
elif exc_type == apx.CanceledException:
self._cancel_done()
elif exc_type == apx.ConflictingActionErrors:
self.log_operation_end(error=str(exc_value),
result=RESULT_CONFLICTING_ACTIONS)
elif exc_type in [
apx.IpkgOutOfDateException,
fmri.IllegalFmri]:
self.log_operation_end(error=exc_value)
elif log_op_end_all:
self.log_operation_end(error=exc_value)
if exc_type != apx.ImageLockedError:
# Must be called before reset_unlock, and only if
# the exception was not a locked error.
self._img.unlock()
try:
if int(os.environ.get("PKG_DUMP_STATS", 0)) > 0:
self._img.transport.stats.dump()
except ValueError:
# Don't generate stats if an invalid value
# is supplied.
pass
# In the case of duplicate actions, we want to save off the plan
# description for display to the client (if they requested it),
# as once the solver's done its job, there's interesting
# information in the plan. We have to save it here and restore
# it later because __reset_unlock() torches it.
if exc_type == apx.ConflictingActionErrors:
self._img.imageplan.set_be_options(self.__backup_be,
self.__backup_be_name, self.__new_be,
self.__be_activate, self.__be_name)
plan_desc = self._img.imageplan.describe()
self.__reset_unlock()
if exc_type == apx.ConflictingActionErrors:
self.__plan_desc = plan_desc
self._activity_lock.release()
# re-raise the original exception. (we have to explicitly
# restate the original exception since we may have cleared the
# current exception scope above.)
raise exc_type, exc_value, exc_traceback
def solaris_image(self):
"""Returns True if the current image is a solaris image, or an
image which contains the pkg(5) packaging system."""
# First check to see if the special package "release/name"
# exists and contains metadata saying this is Solaris.
results = self.__get_pkg_list(self.LIST_INSTALLED,
patterns=["release/name"], return_fmris=True)
results = [e for e in results]
if results:
pfmri, summary, categories, states, attrs = results[0]
mfst = self._img.get_manifest(pfmri)
osname = mfst.get("pkg.release.osname", None)
if osname == "sunos":
return True
# Otherwise, see if we can find package/pkg (or SUNWipkg) and
# system/core-os (or SUNWcs).
results = self.__get_pkg_list(self.LIST_INSTALLED,
patterns=["/package/pkg", "SUNWipkg", "/system/core-os",
"SUNWcs"])
installed = set(e[0][1] for e in results)
if ("SUNWcs" in installed or "system/core-os" in installed) and \
("SUNWipkg" in installed or "package/pkg" in installed):
return True
return False
def __ipkg_require_latest(self, noexecute):
"""Raises an IpkgOutOfDateException if the current image
contains the pkg(5) packaging system and a newer version
of the pkg(5) packaging system is installable."""
if not self.solaris_image():
return
# Get old purpose in order to be able to restore it on return.
p = self.__progresstracker.get_purpose()
try:
#
# Let progress tracker know that subsequent callbacks
# into it will all be in service of update checking.
# Note that even though this might return, the
# finally: will still reset the purpose.
#
self.__progresstracker.set_purpose(
self.__progresstracker.PURPOSE_PKG_UPDATE_CHK)
if self._img.ipkg_is_up_to_date(
self.__check_cancel, noexecute,
refresh_allowed=False,
progtrack=self.__progresstracker):
return
except apx.ImageNotFoundException:
# Can't do anything in this
# case; so proceed.
return
finally:
self.__progresstracker.set_purpose(p)
raise apx.IpkgOutOfDateException()
def __verify_args(self, args):
"""Verifies arguments passed into the API.
It tests for correct data types of the input args, verifies that
passed in FMRIs are valid, checks if repository URIs are valid
and does some logical tests for the combination of arguments."""
arg_types = {
# arg name type nullable
"_noexecute": (bool, False),
"_be_activate": (bool, False),
"_new_be": (bool, True),
"_be_name": (basestring, True),
"_backup_be": (bool, True),
"_backup_be_name": (basestring, True),
"_pubcheck": (bool, False),
"_refresh_catalogs": (bool, False),
"_repos": (iter, True),
"_update_index": (bool, False),
"_li_ignore": (iter, True),
"_li_parent_sync": (bool, False),
"_li_md_only": (bool, False),
"_ipkg_require_latest": (bool, False),
"pkgs_inst": (iter, True),
"pkgs_update": (iter, True),
"pkgs_to_uninstall": (iter, True),
"reject_list": (iter, True),
"mediators": (iter, True),
"variants": (dict, True),
"facets": (pkg.facet.Facets, True)
}
# merge kwargs into the main arg dict
if "kwargs" in args:
for name, value in args["kwargs"].items():
args[name] = value
# check arguments for proper type and nullability
for a in args:
try:
a_type, nullable = arg_types[a]
except KeyError:
# unknown argument passed, ignore
continue
assert nullable or args[a] is not None
if args[a] is not None and a_type == iter:
try:
iter(args[a])
except TypeError:
raise AssertionError("%s is not an "
"iterable" % a)
else:
assert (args[a] is None or
isinstance(args[a], a_type)), "%s is " \
"type %s; expected %s" % (a, type(a),
a_type)
# check if passed FMRIs are valid
illegals = []
for i in ("pkgs_inst", "pkgs_update", "pkgs_to_uninstall",
"reject_list"):
try:
fmris = args[i]
except KeyError:
continue
if fmris is None:
continue
for pat, err, pfmri, matcher in \
self.parse_fmri_patterns(fmris):
if not err:
continue
else:
illegals.append(fmris)
if illegals:
raise apx.PlanCreationException(illegal=illegals)
# some logical checks
errors = []
if not args["_new_be"] and args["_be_name"]:
errors.append(apx.InvalidOptionError(
apx.InvalidOptionError.REQUIRED, ["_be_name",
"_new_be"]))
if not args["_backup_be"] and args["_backup_be_name"]:
errors.append(apx.InvalidOptionError(
apx.InvalidOptionError.REQUIRED, ["_backup_be_name",
"_backup_be"]))
if args["_backup_be"] and args["_new_be"]:
errors.append(apx.InvalidOptionError(
apx.InvalidOptionError.INCOMPAT, ["_backup_be",
"_new_be"]))
if errors:
raise apx.InvalidOptionErrors(errors)
# check if repo URIs are valid
try:
repos = args["_repos"]
except KeyError:
return
if not repos:
return
illegals = []
for r in repos:
valid = False
if type(r) == publisher.RepositoryURI:
# RepoURI objects pass right away
continue
if not misc.valid_pub_url(r):
illegals.append(r)
if illegals:
raise apx.UnsupportedRepositoryURI(illegals)
def __plan_op(self, _op, _ad_kwargs=None,
_backup_be=None, _backup_be_name=None, _be_activate=True,
_be_name=None, _ipkg_require_latest=False, _li_ignore=None,
_li_md_only=False, _li_parent_sync=True, _new_be=False,
_noexecute=False, _pubcheck=True, _refresh_catalogs=True,
_repos=None, _update_index=True, **kwargs):
"""Contructs a plan to change the package or linked image
state of an image.
We can raise PermissionsException, PlanCreationException,
InventoryException, or LinkedImageException.
Arguments prefixed with '_' are primarily used within this
function. All other arguments must be specified via keyword
assignment and will be passed directly on to the image
interfaces being invoked."
'_op' is the API operation we will perform.
'_ad_kwargs' is only used dyring attach or detach and it
is a dictionary of arguments that will be passed to the
linked image attach/detach interfaces.
'_ipkg_require_latest' enables a check to verify that the
latest installable version of the pkg(5) packaging system is
installed before we proceed with the requested operation.
For all other '_' prefixed parameters, please refer to the
'gen_plan_*' functions which invoke this function for an
explanation of their usage and effects.
This function first yields the plan description for the global
zone, then either a series of dictionaries representing the
parsable output from operating on the child images or a series
of None values."""
# sanity checks
assert _op in api_op_values
assert _ad_kwargs == None or \
_op in [API_OP_ATTACH, API_OP_DETACH]
assert _ad_kwargs != None or \
_op not in [API_OP_ATTACH, API_OP_DETACH]
assert not _li_md_only or \
_op in [API_OP_ATTACH, API_OP_DETACH, API_OP_SYNC]
assert not _li_md_only or _li_parent_sync
self.__verify_args(locals())
# make some perf optimizations
if _li_md_only:
_refresh_catalogs = _update_index = False
if _op in [API_OP_DETACH, API_OP_SET_MEDIATOR]:
# these operations don't change fmris and don't need
# to recurse, so disable a bunch of linked image
# operations.
_li_parent_sync = False
_pubcheck = False
_li_ignore = [] # ignore all children
# All the image interface functions that we invoke have some
# common arguments. Set those up now.
args_common = {}
args_common["op"] = _op
args_common["progtrack"] = self.__progresstracker
args_common["check_cancel"] = self.__check_cancel
args_common["noexecute"] = _noexecute
# make sure there is no overlap between the common arguments
# supplied to all api interfaces and the arguments that the
# api arguments that caller passed to this function.
assert (set(args_common) & set(kwargs)) == set(), \
"%s & %s != set()" % (str(set(args_common)),
str(set(kwargs)))
kwargs.update(args_common)
# Lock the current image.
self.__plan_common_start(_op, _noexecute, _backup_be,
_backup_be_name, _new_be, _be_name, _be_activate)
try:
if _op == API_OP_ATTACH:
self._img.linked.attach_parent(**_ad_kwargs)
elif _op == API_OP_DETACH:
self._img.linked.detach_parent(**_ad_kwargs)
if _li_parent_sync:
# refresh linked image data from parent image.
self._img.linked.syncmd_from_parent()
# initialize recursion state
self._img.linked.api_recurse_init(
li_ignore=_li_ignore, repos=_repos)
if _pubcheck:
# check that linked image pubs are in sync
self.__linked_pubcheck(_op)
if _refresh_catalogs:
self.__refresh_publishers()
if _ipkg_require_latest:
# If this is an image update then make
# sure the latest version of the ipkg
# software is installed.
self.__ipkg_require_latest(_noexecute)
self.__set_img_alt_sources(_repos)
if _li_md_only:
self._img.make_noop_plan(**args_common)
elif _op in [API_OP_ATTACH, API_OP_DETACH, API_OP_SYNC]:
self._img.make_sync_plan(**kwargs)
elif _op in [API_OP_CHANGE_FACET,
API_OP_CHANGE_VARIANT]:
self._img.make_change_varcets_plan(**kwargs)
elif _op == API_OP_INSTALL:
self._img.make_install_plan(**kwargs)
elif _op == API_OP_REVERT:
self._img.make_revert_plan(**kwargs)
elif _op == API_OP_SET_MEDIATOR:
self._img.make_set_mediators_plan(**kwargs)
elif _op == API_OP_UNINSTALL:
self._img.make_uninstall_plan(**kwargs)
elif _op == API_OP_UPDATE:
self._img.make_update_plan(**kwargs)
else:
raise RuntimeError("Unknown api op: %s" % _op)
self.__api_op = _op
if self._img.imageplan.nothingtodo():
# no package changes mean no index changes
_update_index = False
self._disable_cancel()
self.__set_be_creation()
self._img.imageplan.set_be_options(
self.__backup_be, self.__backup_be_name,
self.__new_be, self.__be_activate, self.__be_name)
self.__plan_desc = self._img.imageplan.describe()
if not _noexecute:
self.__plan_type = self.__plan_desc.plan_type
# Yield to our caller so they can display our plan
# before we recurse into child images. Drop the
# activity lock before yielding because otherwise the
# caller can't do things like set the displayed
# license state for pkg plans).
self._activity_lock.release()
yield self.__plan_desc
self._activity_lock.acquire()
# plan operation in child images. This currently yields
# either a dictionary representing the parsable output
# from the child image operation, or None. Eventually
# these will yield plan descriptions objects instead.
for p_dict in self._img.linked.api_recurse_plan(
api_kwargs=kwargs,
refresh_catalogs=_refresh_catalogs,
update_index=_update_index,
progtrack=self.__progresstracker):
yield p_dict
self.__planned_children = True
except:
if _op in [
API_OP_UPDATE,
API_OP_INSTALL,
API_OP_REVERT,
API_OP_SYNC]:
self.__plan_common_exception(
log_op_end_all=True)
else:
self.__plan_common_exception()
# NOTREACHED
stuff_to_do = not self.planned_nothingtodo()
if not stuff_to_do or _noexecute:
self.log_operation_end(
result=RESULT_NOTHING_TO_DO)
self._img.imageplan.update_index = _update_index
self.__plan_common_finish()
if DebugValues["plandesc_validate"]:
# save, load, and get a new json copy of the plan,
# then compare that new copy against our current one.
# this regressions tests the plan save/load code.
pd_json1 = self.__plan_desc.getstate(self.__plan_desc,
reset_volatiles=True)
fobj = tempfile.TemporaryFile()
json.dump(pd_json1, fobj, encoding="utf-8")
pd_new = plandesc.PlanDescription(_op)
pd_new._load(fobj)
pd_json2 = pd_new.getstate(pd_new, reset_volatiles=True)
del fobj, pd_new
pkg.misc.json_diff("PlanDescription", \
pd_json1, pd_json2, pd_json1, pd_json2)
del pd_json1, pd_json2
@_LockedCancelable()
def load_plan(self, plan, prepared=False):
"""Load a previously generated PlanDescription."""
# Prevent loading a plan if one has been already.
if self.__plan_type is not None:
raise apx.PlanExistsException(self.__plan_type)
# grab image lock. we don't worry about dropping the image
# lock since __activity_lock will drop it for us us after we
# return (or if we generate an exception).
self._img.lock()
# load the plan
self.__plan_desc = plan
self.__plan_type = plan.plan_type
self.__planned_children = True
self.__prepared = prepared
# load BE related plan settings
self.__new_be = plan.new_be
self.__be_activate = plan.activate_be
self.__be_name = plan.be_name
# sanity check: verify the BE name
if self.__be_name is not None:
self.check_be_name(self.__be_name)
if not self._img.is_liveroot():
raise apx.BENameGivenOnDeadBE(self.__be_name)
# sanity check: verify that all the fmris in the plan are in
# the known catalog
pkg_cat = self._img.get_catalog(self._img.IMG_CATALOG_KNOWN)
for pp in plan.pkg_plans:
if pp.destination_fmri:
assert pkg_cat.get_entry(pp.destination_fmri), \
"fmri part of plan, but currently " \
"unknown: %s" % pp.destination_fmri
# allocate an image plan based on the supplied plan
self._img.imageplan = imageplan.ImagePlan(self._img, plan._op,
self.__progresstracker, check_cancel=self.__check_cancel,
pd=plan)
if prepared:
self._img.imageplan.skip_preexecute()
# create a history entry
self.log_operation_start(plan.plan_type)
def __linked_pubcheck(self, api_op=None):
"""Private interface to perform publisher check on this image
and its children."""
if api_op in [API_OP_DETACH, API_OP_SET_MEDIATOR]:
# we don't need to do a pubcheck for detach or
# changing mediators
return
# check the current image
self._img.linked.pubcheck()
# check child images
self._img.linked.api_recurse_pubcheck(self.__progresstracker)
@_LockedCancelable()
def linked_publisher_check(self):
"""If we're a child image, verify that the parent image's
publisher configuration is a subset of the child image's
publisher configuration. If we have any children, recurse
into them and perform a publisher check."""
# grab image lock. we don't worry about dropping the image
# lock since __activity_lock will drop it for us us after we
# return (or if we generate an exception).
self._img.lock(allow_unprivileged=True)
# get ready to recurse
self._img.linked.api_recurse_init()
# check that linked image pubs are in sync
self.__linked_pubcheck()
def planned_nothingtodo(self, li_ignore_all=False):
"""Once an operation has been planned check if there is
something todo.
Callers should pass all arguments by name assignment and
not by positional order.
'li_ignore_all' indicates if we should only report on work
todo in the parent image. (i.e., if an operation was planned
and that operation only involves changes to children, and
li_ignore_all is true, then we'll report that there's nothing
todo."""
if not self._img.imageplan:
# if theres no plan there nothing to do
return True
if not self._img.imageplan.nothingtodo():
return False
if not self._img.linked.nothingtodo():
return False
if not li_ignore_all:
assert self.__planned_children
if not self._img.linked.recurse_nothingtodo():
return False
return True
def plan_update(self, pkg_list, refresh_catalogs=True,
reject_list=misc.EmptyI, noexecute=False, update_index=True,
be_name=None, new_be=False, repos=None, be_activate=True):
"""DEPRECATED. use gen_plan_update()."""
for pd in self.gen_plan_update(
pkgs_update=pkg_list, refresh_catalogs=refresh_catalogs,
reject_list=reject_list, noexecute=noexecute,
update_index=update_index, be_name=be_name, new_be=new_be,
repos=repos, be_activate=be_activate):
continue
return not self.planned_nothingtodo()
def plan_update_all(self, refresh_catalogs=True,
reject_list=misc.EmptyI, noexecute=False, force=False,
update_index=True, be_name=None, new_be=True, repos=None,
be_activate=True):
"""DEPRECATED. use gen_plan_update()."""
for pd in self.gen_plan_update(
refresh_catalogs=refresh_catalogs, reject_list=reject_list,
noexecute=noexecute, force=force,
update_index=update_index, be_name=be_name, new_be=new_be,
repos=repos, be_activate=be_activate):
continue
return (not self.planned_nothingtodo(), self.solaris_image())
def gen_plan_update(self, pkgs_update=None, backup_be=None,
backup_be_name=None, be_activate=True, be_name=None,
force=False, li_ignore=None, li_parent_sync=True, new_be=True,
noexecute=False, pubcheck=True, refresh_catalogs=True,
reject_list=misc.EmptyI, repos=None, update_index=True):
"""This is a generator function that yields a PlanDescription
object. If parsable_version is set, it also yields dictionaries
containing plan information for child images.
If pkgs_update is not set, constructs a plan to update all
packages on the system to the latest known versions. Once an
operation has been planned, it may be executed by first
calling prepare(), and then execute_plan(). After execution
of a plan, or to abandon a plan, reset() should be called.
Callers should pass all arguments by name assignment and
not by positional order.
If 'pkgs_update' is set, constructs a plan to update the
packages provided in pkgs_update.
Once an operation has been planned, it may be executed by
first calling prepare(), and then execute_plan().
'force' indicates whether update should skip the package
system up to date check.
'pubcheck' indicates that we should skip the child image
publisher check before creating a plan for this image. only
pkg.1 should use this parameter, other callers should never
specify it.
For all other parameters, refer to the 'gen_plan_install'
function for an explanation of their usage and effects."""
if pkgs_update or force:
ipkg_require_latest = False
else:
ipkg_require_latest = True
op = API_OP_UPDATE
return self.__plan_op(op,
_backup_be=backup_be, _backup_be_name=backup_be_name,
_be_activate=be_activate, _be_name=be_name,
_ipkg_require_latest=ipkg_require_latest,
_li_ignore=li_ignore, _li_parent_sync=li_parent_sync,
_new_be=new_be, _noexecute=noexecute, _pubcheck=pubcheck,
_refresh_catalogs=refresh_catalogs, _repos=repos,
_update_index=update_index, pkgs_update=pkgs_update,
reject_list=reject_list)
def plan_install(self, pkg_list, refresh_catalogs=True,
noexecute=False, update_index=True, be_name=None,
reject_list=misc.EmptyI, new_be=False, repos=None,
be_activate=True):
"""DEPRECATED. use gen_plan_install()."""
for pd in self.gen_plan_install(
pkgs_inst=pkg_list, refresh_catalogs=refresh_catalogs,
noexecute=noexecute, update_index=update_index,
be_name=be_name, reject_list=reject_list, new_be=new_be,
repos=repos, be_activate=be_activate):
continue
return not self.planned_nothingtodo()
def gen_plan_install(self, pkgs_inst, backup_be=None,
backup_be_name=None, be_activate=True, be_name=None, li_ignore=None,
li_parent_sync=True, new_be=False, noexecute=False,
refresh_catalogs=True, reject_list=misc.EmptyI, repos=None,
update_index=True):
"""This is a generator function that yields a PlanDescription
object. If parsable_version is set, it also yields dictionaries
containing plan information for child images.
Constructs a plan to install the packages provided in
pkgs_inst. Once an operation has been planned, it may be
executed by first calling prepare(), and then execute_plan().
After execution of a plan, or to abandon a plan, reset()
should be called.
Callers should pass all arguments by name assignment and
not by positional order.
'pkgs_inst' is a list of packages to install.
'backup_be' indicates whether a backup boot environment should
be created before the operation is executed. If True, a backup
boot environment will be created. If False, a backup boot
environment will not be created. If None and a new boot
environment is not created, and packages are being updated or
are being installed and tagged with reboot-needed, a backup
boot environment will be created.
'backup_be_name' is a string to use as the name of any backup
boot environment created during the operation.
'be_name' is a string to use as the name of any new boot
environment created during the operation.
'li_ignore' is either None or a list. If it's None (the
default), the planning operation will attempt to keep all
linked children in sync. If it's an empty list the planning
operation will ignore all children. If this is a list of
linked image children names, those children will be ignored
during the planning operation. If a child is ignored during
the planning phase it will also be skipped during the
preparation and execution phases.
'li_parent_sync' if the current image is a child image, this
flag controls whether the linked image parent metadata will be
automatically refreshed.
'new_be' indicates whether a new boot environment should be
created during the operation. If True, a new boot environment
will be created. If False, and a new boot environment is
needed, an ImageUpdateOnLiveImageException will be raised.
If None, a new boot environment will be created only if needed.
'noexecute' determines whether the resulting plan can be
executed and whether history will be recorded after
planning is finished.
'refresh_catalogs' controls whether the catalogs will
automatically be refreshed.
'reject_list' is a list of patterns not to be permitted
in solution; installed packages matching these patterns
are removed.
'repos' is a list of URI strings or RepositoryURI objects that
represent the locations of additional sources of package data to
use during the planned operation. All API functions called
while a plan is still active will use this package data.
'be_activate' is an optional boolean indicating whether any
new boot environment created for the operation should be set
as the active one on next boot if the operation is successful.
'update_index' determines whether client search indexes
will be updated after operation completion during plan
execution."""
# certain parameters must be specified
assert pkgs_inst and type(pkgs_inst) == list
op = API_OP_INSTALL
return self.__plan_op(op,
_backup_be=backup_be, _backup_be_name=backup_be_name,
_be_activate=be_activate, _be_name=be_name,
_li_ignore=li_ignore, _li_parent_sync=li_parent_sync,
_new_be=new_be, _noexecute=noexecute,
_refresh_catalogs=refresh_catalogs, _repos=repos,
_update_index=update_index, pkgs_inst=pkgs_inst,
reject_list=reject_list)
def gen_plan_sync(self, backup_be=None, backup_be_name=None,
be_activate=True, be_name=None, li_ignore=None, li_md_only=False,
li_parent_sync=True, li_pkg_updates=True, new_be=False,
noexecute=False, pubcheck=True, refresh_catalogs=True,
reject_list=misc.EmptyI, repos=None, update_index=True):
"""This is a generator function that yields a PlanDescription
object. If parsable_version is set, it also yields dictionaries
containing plan information for child images.
Constructs a plan to sync the current image with its
linked image constraints. Once an operation has been planned,
it may be executed by first calling prepare(), and then
execute_plan(). After execution of a plan, or to abandon a
plan, reset() should be called.
Callers should pass all arguments by name assignment and
not by positional order.
'li_md_only' don't actually modify any packages in the current
images, only sync the linked image metadata from the parent
image. If this options is True, 'li_parent_sync' must also be
True.
'li_pkg_updates' when planning a sync operation, allow updates
to packages other than the constraints package. If this
option is False, planning a sync will fail if any packages
(other than the constraints package) need updating to bring
the image in sync with its parent.
For all other parameters, refer to 'gen_plan_install' and
'gen_plan_update' for an explanation of their usage and
effects."""
# we should only be invoked on a child image.
if not self.ischild():
raise apx.LinkedImageException(
self_not_child=self._img_path)
op = API_OP_SYNC
return self.__plan_op(op,
_backup_be=backup_be, _backup_be_name=backup_be_name,
_be_activate=be_activate, _be_name=be_name,
_li_ignore=li_ignore, _li_md_only=li_md_only,
_li_parent_sync=li_parent_sync, _new_be=new_be,
_noexecute=noexecute, _pubcheck=pubcheck,
_refresh_catalogs=refresh_catalogs,
_repos=repos,
_update_index=update_index,
li_pkg_updates=li_pkg_updates, reject_list=reject_list)
def gen_plan_attach(self, lin, li_path, allow_relink=False,
backup_be=None, backup_be_name=None, be_activate=True, be_name=None,
force=False, li_ignore=None, li_md_only=False, li_pkg_updates=True,
li_props=None, new_be=False, noexecute=False, refresh_catalogs=True,
reject_list=misc.EmptyI, repos=None, update_index=True):
"""This is a generator function that yields a PlanDescription
object. If parsable_version is set, it also yields dictionaries
containing plan information for child images.
Attach a parent image and sync the packages in the current
image with the new parent. Once an operation has been
planned, it may be executed by first calling prepare(), and
then execute_plan(). After execution of a plan, or to abandon
a plan, reset() should be called.
Callers should pass all arguments by name assignment and
not by positional order.
'lin' a LinkedImageName object that is a name for the current
image.
'li_path' a path to the parent image.
'allow_relink' allows re-linking of an image that is already a
linked image child. If this option is True we'll overwrite
all existing linked image metadata.
'li_props' optional linked image properties to apply to the
child image.
For all other parameters, refer to the 'gen_plan_install' and
'gen_plan_sync' functions for an explanation of their usage
and effects."""
if li_props == None:
li_props = dict()
op = API_OP_ATTACH
ad_kwargs = {
"allow_relink": allow_relink,
"force": force,
"lin": lin,
"path": li_path,
"props": li_props,
}
return self.__plan_op(op,
_backup_be=backup_be, _backup_be_name=backup_be_name,
_be_activate=be_activate, _be_name=be_name,
_li_ignore=li_ignore, _li_md_only=li_md_only,
_new_be=new_be, _noexecute=noexecute,
_refresh_catalogs=refresh_catalogs, _repos=repos,
_update_index=update_index, _ad_kwargs=ad_kwargs,
li_pkg_updates=li_pkg_updates, reject_list=reject_list)
def gen_plan_detach(self, backup_be=None,
backup_be_name=None, be_activate=True, be_name=None, force=False,
li_ignore=None, li_md_only=False, li_pkg_updates=True, new_be=False,
noexecute=False):
"""This is a generator function that yields a PlanDescription
object. If parsable_version is set, it also yields dictionaries
containing plan information for child images.
Detach from a parent image and remove any constraints
package from this image. Once an operation has been planned,
it may be executed by first calling prepare(), and then
execute_plan(). After execution of a plan, or to abandon a
plan, reset() should be called.
Callers should pass all arguments by name assignment and
not by positional order.
For all other parameters, refer to the 'gen_plan_install' and
'gen_plan_sync' functions for an explanation of their usage
and effects."""
op = API_OP_DETACH
ad_kwargs = {
"force": force
}
return self.__plan_op(op, _ad_kwargs=ad_kwargs,
_backup_be=backup_be, _backup_be_name=backup_be_name,
_be_activate=be_activate, _be_name=be_name,
_li_ignore=li_ignore, _li_md_only=li_md_only,
_new_be=new_be, _noexecute=noexecute,
_refresh_catalogs=False, _update_index=False,
li_pkg_updates=li_pkg_updates)
def plan_uninstall(self, pkg_list, noexecute=False, update_index=True,
be_name=None, new_be=False, be_activate=True):
"""DEPRECATED. use gen_plan_uninstall()."""
for pd in self.gen_plan_uninstall(pkgs_to_uninstall=pkg_list,
noexecute=noexecute, update_index=update_index,
be_name=be_name, new_be=new_be, be_activate=be_activate):
continue
return not self.planned_nothingtodo()
def gen_plan_uninstall(self, pkgs_to_uninstall,
backup_be=None, backup_be_name=None, be_activate=True,
be_name=None, li_ignore=None, li_parent_sync=True, new_be=False,
noexecute=False, update_index=True):
"""This is a generator function that yields a PlanDescription
object. If parsable_version is set, it also yields dictionaries
containing plan information for child images.
Constructs a plan to remove the packages provided in
pkgs_to_uninstall. Once an operation has been planned, it may
be executed by first calling prepare(), and then
execute_plan(). After execution of a plan, or to abandon a
plan, reset() should be called.
Callers should pass all arguments by name assignment and
not by positional order.
'pkgs_to_uninstall' is a list of packages to uninstall.
For all other parameters, refer to the 'gen_plan_install'
function for an explanation of their usage and effects."""
# certain parameters must be specified
assert pkgs_to_uninstall and type(pkgs_to_uninstall) == list
op = API_OP_UNINSTALL
return self.__plan_op(op,
_backup_be=backup_be, _backup_be_name=backup_be_name,
_be_activate=be_activate, _be_name=be_name,
_li_ignore=li_ignore, _li_parent_sync=li_parent_sync,
_new_be=new_be, _noexecute=noexecute,
_refresh_catalogs=False,
_update_index=update_index,
pkgs_to_uninstall=pkgs_to_uninstall)
def gen_plan_set_mediators(self, mediators, backup_be=None,
backup_be_name=None, be_activate=True, be_name=None, li_ignore=None,
li_parent_sync=True, new_be=None, noexecute=False,
update_index=True):
"""This is a generator function that yields a PlanDescription
object. If parsable_version is set, it also yields dictionaries
containing plan information for child images.
Creates a plan to change the version and implementation values
for mediators as specified in the provided dictionary. Once an
operation has been planned, it may be executed by first calling
prepare(), and then execute_plan(). After execution of a plan,
or to abandon a plan, reset() should be called.
Callers should pass all arguments by name assignment and not by
positional order.
'mediators' is a dict of dicts of the mediators to set version
and implementation for. If the dict for a given mediator-name
is empty, it will be intepreted as a request to revert the
specified mediator to the default, "optimal" mediation. It
should be of the form:
{
mediator-name: {
"implementation": mediator-implementation-string,
"version": mediator-version-string
}
}
'implementation' is an optional string that specifies the
implementation of the mediator for use in addition to or
instead of 'version'.
'version' is an optional string that specifies the version
(expressed as a dot-separated sequence of non-negative
integers) of the mediator for use.
For all other parameters, refer to the 'gen_plan_install'
function for an explanation of their usage and effects."""
assert mediators
return self.__plan_op(API_OP_SET_MEDIATOR,
_backup_be=backup_be, _backup_be_name=backup_be_name,
_be_activate=be_activate, _be_name=be_name,
_li_ignore=li_ignore, _li_parent_sync=li_parent_sync,
mediators=mediators, _new_be=new_be, _noexecute=noexecute,
_refresh_catalogs=False, _update_index=update_index)
def plan_change_varcets(self, variants=None, facets=None,
noexecute=False, be_name=None, new_be=None, repos=None,
be_activate=True):
"""DEPRECATED. use gen_plan_change_varcets()."""
for pd in self.gen_plan_change_varcets(
variants=variants, facets=facets, noexecute=noexecute,
be_name=be_name, new_be=new_be, repos=repos,
be_activate=be_activate):
continue
return not self.planned_nothingtodo()
def gen_plan_change_varcets(self, facets=None, variants=None,
backup_be=None, backup_be_name=None, be_activate=True, be_name=None,
li_ignore=None, li_parent_sync=True, new_be=None, noexecute=False,
refresh_catalogs=True, reject_list=misc.EmptyI, repos=None,
update_index=True):
"""This is a generator function that yields a PlanDescription
object. If parsable_version is set, it also yields dictionaries
containing plan information for child images.
Creates a plan to change the specified variants and/or
facets for the image. Once an operation has been planned, it
may be executed by first calling prepare(), and then
execute_plan(). After execution of a plan, or to abandon a
plan, reset() should be called.
Callers should pass all arguments by name assignment and
not by positional order.
'facets' is a dict of the facets to change the values of.
'variants' is a dict of the variants to change the values of.
For all other parameters, refer to the 'gen_plan_install'
function for an explanation of their usage and effects."""
# An empty facets dictionary is allowed because that's how to
# unset all set facets.
if not variants and facets is None:
raise ValueError, "Nothing to do"
if variants:
op = API_OP_CHANGE_VARIANT
else:
op = API_OP_CHANGE_FACET
return self.__plan_op(op, _backup_be=backup_be,
_backup_be_name=backup_be_name, _be_activate=be_activate,
_be_name=be_name, _li_ignore=li_ignore,
_li_parent_sync=li_parent_sync, _new_be=new_be,
_noexecute=noexecute, _refresh_catalogs=refresh_catalogs,
_repos=repos,
_update_index=update_index, variants=variants,
facets=facets, reject_list=reject_list)
def plan_revert(self, args, tagged=False, noexecute=True, be_name=None,
new_be=None, be_activate=True):
"""DEPRECATED. use gen_plan_revert()."""
for pd in self.gen_plan_revert(
args=args, tagged=tagged, noexecute=noexecute,
be_name=be_name, new_be=new_be, be_activate=be_activate):
continue
return not self.planned_nothingtodo()
def gen_plan_revert(self, args, backup_be=None, backup_be_name=None,
be_activate=True, be_name=None, new_be=None, noexecute=True,
tagged=False):
"""This is a generator function that yields a PlanDescription
object. If parsable_version is set, it also yields dictionaries
containing plan information for child images.
Plan to revert either files or all files tagged with
specified values. Args contains either path names or tag
names to be reverted, tagged is True if args contains tags.
Once an operation has been planned, it may be executed by
first calling prepare(), and then execute_plan(). After
execution of a plan, or to abandon a plan, reset() should be
called.
For all other parameters, refer to the 'gen_plan_install'
function for an explanation of their usage and effects."""
op = API_OP_REVERT
return self.__plan_op(op, _be_activate=be_activate,
_backup_be=backup_be, _backup_be_name=backup_be_name,
_be_name=be_name, _li_ignore=[], _new_be=new_be,
_noexecute=noexecute, _refresh_catalogs=False,
_update_index=False, args=args, tagged=tagged)
def attach_linked_child(self, lin, li_path, li_props=None,
accept=False, allow_relink=False, force=False, li_md_only=False,
li_pkg_updates=True, noexecute=False,
refresh_catalogs=True, reject_list=misc.EmptyI,
show_licenses=False, update_index=True):
"""Attach an image as a child to the current image (the
current image will become a parent image. This operation
results in attempting to sync the child image with the parent
image.
'lin' is the name of the child image
'li_path' is the path to the child image
'li_props' optional linked image properties to apply to the
child image.
'allow_relink' indicates whether we should allow linking of a
child image that is already linked (the child may already
be a child or a parent image).
'force' indicates whether we should allow linking of a child
image even if the specified linked image type doesn't support
attaching of children.
'li_md_only' indicates whether we should only update linked
image metadata and not actually try to sync the child image.
'li_pkg_updates' indicates whether we should disallow pkg
updates during the child image sync.
'noexecute' indicates if we should actually make any changes
rather or just simulate the operation.
'refresh_catalogs' controls whether the catalogs will
automatically be refreshed.
'reject_list' is a list of patterns not to be permitted
in solution; installed packages matching these patterns
are removed.
'update_index' determines whether client search indexes will
be updated in the child after the sync operation completes.
This function returns a tuple of the format (rv, err) where rv
is a pkg.client.pkgdefs return value and if an error was
encountered err is an exception object which describes the
error."""
return self._img.linked.attach_child(lin, li_path, li_props,
accept=accept, allow_relink=allow_relink, force=force,
li_md_only=li_md_only, li_pkg_updates=li_pkg_updates,
noexecute=noexecute,
progtrack=self.__progresstracker,
refresh_catalogs=refresh_catalogs, reject_list=reject_list,
show_licenses=show_licenses, update_index=update_index)
def detach_linked_children(self, li_list, force=False,
li_md_only=False, li_pkg_updates=True, noexecute=False):
"""Detach one or more children from the current image. This
operation results in the removal of any constraint package
from the child images.
'li_list' a list of linked image name objects which specified
which children to operate on. If the list is empty then we
operate on all children.
For all other parameters, refer to the 'attach_linked_child'
function for an explanation of their usage and effects.
This function returns a dictionary where the keys are linked
image name objects and the values are the result of the
specified operation on the associated child image. The result
is a tuple of the format (rv, err) where rv is a
pkg.client.pkgdefs return value and if an error was
encountered err is an exception object which describes the
error."""
return self._img.linked.detach_children(li_list,
force=force, li_md_only=li_md_only,
li_pkg_updates=li_pkg_updates,
noexecute=noexecute)
def detach_linked_rvdict2rv(self, rvdict):
"""Convenience function that takes a dictionary returned from
an operations on multiple children and merges the results into
a single return code."""
return self._img.linked.detach_rvdict2rv(rvdict)
def sync_linked_children(self, li_list,
accept=False, li_md_only=False,
li_pkg_updates=True, noexecute=False,
refresh_catalogs=True, show_licenses=False, update_index=True):
"""Sync one or more children of the current image.
For all other parameters, refer to the 'attach_linked_child'
and 'detach_linked_children' functions for an explanation of
their usage and effects.
For a description of the return value, refer to the
'detach_linked_children' function."""
rvdict = self._img.linked.sync_children(li_list,
accept=accept, li_md_only=li_md_only,
li_pkg_updates=li_pkg_updates, noexecute=noexecute,
progtrack=self.__progresstracker,
refresh_catalogs=refresh_catalogs,
show_licenses=show_licenses, update_index=update_index)
return rvdict
def sync_linked_rvdict2rv(self, rvdict):
"""Convenience function that takes a dictionary returned from
an operations on multiple children and merges the results into
a single return code."""
return self._img.linked.sync_rvdict2rv(rvdict)
def audit_linked_children(self, li_list):
"""Audit one or more children of the current image to see if
they are in sync with this image.
For all parameters, refer to the 'detach_linked_children'
functions for an explanation of their usage and effects.
For a description of the return value, refer to the
'detach_linked_children' function."""
rvdict = self._img.linked.audit_children(li_list)
return rvdict
def audit_linked_rvdict2rv(self, rvdict):
"""Convenience function that takes a dictionary returned from
an operations on multiple children and merges the results into
a single return code."""
return self._img.linked.audit_rvdict2rv(rvdict)
def audit_linked(self, li_parent_sync=True):
"""If the current image is a child image, this function
audits the current image to see if it's in sync with it's
parent.
For a description of the return value, refer to the
'detach_linked_children' function."""
lin = self._img.linked.child_name
rvdict = {}
if li_parent_sync:
# refresh linked image data from parent image.
rvdict[lin] = self._img.linked.syncmd_from_parent(
catch_exception=True)
if rvdict[lin] is not None:
return rvdict
rvdict[lin] = self._img.linked.audit_self()
return rvdict
def ischild(self):
"""Indicates whether the current image is a child image."""
return self._img.linked.ischild()
def isparent(self, li_ignore=None):
"""Indicates whether the current image is a parent image."""
return self._img.linked.isparent(li_ignore)
@staticmethod
def __utc_format(time_str, utc_now):
"""Given a local time value string, formatted with
"%Y-%m-%dT%H:%M:%S, return a UTC representation of that value,
formatted with %Y%m%dT%H%M%SZ. This raises a ValueError if the
time was incorrectly formatted. If the time_str is "now", it
returns the value of utc_now"""
if time_str == "now":
return utc_now
try:
local_dt = datetime.datetime.strptime(time_str,
"%Y-%m-%dT%H:%M:%S")
secs = time.mktime(local_dt.timetuple())
utc_dt = datetime.datetime.utcfromtimestamp(secs)
return utc_dt.strftime("%Y%m%dT%H%M%SZ")
except ValueError, e:
raise apx.HistoryRequestException(e)
def __get_history_paths(self, time_val, utc_now):
"""Given a local timestamp, either as a discrete value, or a
range of values, formatted as '<timestamp>-<timestamp>', and a
path to find history xml files, return an array of paths that
match that timestamp. utc_now is the current time expressed in
UTC"""
files = []
if len(time_val) > 20 or time_val.startswith("now-"):
if time_val.startswith("now-"):
start = utc_now
finish = self.__utc_format(time_val[4:],
utc_now)
else:
# our ranges are 19 chars of timestamp, a '-',
# and another timestamp
start = self.__utc_format(time_val[:19],
utc_now)
finish = self.__utc_format(time_val[20:],
utc_now)
if start > finish:
raise apx.HistoryRequestException(_("Start "
"time must be older than finish time: "
"%s") % time_val)
files = self.__get_history_range(start, finish)
else:
# there can be multiple event files per timestamp
prefix = self.__utc_format(time_val, utc_now)
files = glob.glob(os.path.join(self._img.history.path,
"%s*" % prefix))
if not files:
raise apx.HistoryRequestException(_("No history "
"entries found for %s") % time_val)
return files
def __get_history_range(self, start, finish):
"""Given a start and finish date, formatted as UTC date strings
as per __utc_format(), return a list of history filenames that
fall within that date range. A range of two equal dates is
the equivalent of just retrieving history for that single date
string."""
entries = []
all_entries = sorted(os.listdir(self._img.history.path))
for entry in all_entries:
# our timestamps are always 16 character datestamps
basename = os.path.basename(entry)[:16]
if basename >= start:
if basename > finish:
# we can stop looking now.
break
entries.append(entry)
return entries
def gen_history(self, limit=None, times=misc.EmptyI):
"""A generator function that returns History objects up to the
limit specified matching the times specified.
'limit' is an optional integer value specifying the maximum
number of entries to return.
'times' is a list of timestamp or timestamp range strings to
restrict the returned entries to."""
# Make entries a set to cope with multiple overlapping ranges or
# times.
entries = set()
utc_now = datetime.datetime.utcnow().strftime("%Y%m%dT%H%M%SZ")
for time_val in times:
# Ranges are 19 chars of timestamp, a '-', and
# another timestamp.
if len(time_val) > 20 or time_val.startswith("now-"):
if time_val.startswith("now-"):
start = utc_now
finish = self.__utc_format(time_val[4:],
utc_now)
else:
start = self.__utc_format(time_val[:19],
utc_now)
finish = self.__utc_format(
time_val[20:], utc_now)
if start > finish:
raise apx.HistoryRequestException(
_("Start time must be older than "
"finish time: %s") % time_val)
files = self.__get_history_range(start, finish)
else:
# There can be multiple entries per timestamp.
prefix = self.__utc_format(time_val, utc_now)
files = glob.glob(os.path.join(
self._img.history.path, "%s*" % prefix))
try:
files = self.__get_history_paths(time_val,
utc_now)
entries.update(files)
except ValueError:
raise apx.HistoryRequestException(_("Invalid "
"time format '%s'. Please use "
"%%Y-%%m-%%dT%%H:%%M:%%S or\n"
"%%Y-%%m-%%dT%%H:%%M:%%S-"
"%%Y-%%m-%%dT%%H:%%M:%%S") % time_val)
if not times:
try:
entries = os.listdir(self._img.history.path)
except EnvironmentError, e:
if e.errno == errno.ENOENT:
# No history to list.
return
raise apx._convert_error(e)
entries = sorted(entries)
if limit:
limit *= -1
entries = entries[limit:]
try:
uuid_be_dic = bootenv.BootEnv.get_uuid_be_dic()
except apx.ApiException, e:
uuid_be_dic = {}
for entry in entries:
# Yield each history entry object as it is loaded.
try:
yield history.History(
root_dir=self._img.history.root_dir,
filename=entry, uuid_be_dic=uuid_be_dic)
except apx.HistoryLoadException, e:
if e.parse_failure:
# Ignore corrupt entries.
continue
raise
def get_linked_name(self):
"""If the current image is a child image, this function
returns a linked image name object which represents the name
of the current image."""
return self._img.linked.child_name
def get_linked_props(self, lin=None):
"""Return a dictionary which represents the linked image
properties associated with a linked image.
'lin' is the name of the child image. If lin is None then
the current image is assumed to be a linked image and it's
properties are returned."""
return self._img.linked.child_props(lin=lin)
def list_linked(self, li_ignore=None):
"""Returns a list of linked images associated with the
current image. This includes both child and parent images.
For all parameters, refer to the 'gen_plan_install' function
for an explanation of their usage and effects.
The returned value is a list of tuples where each tuple
contains (<li name>, <relationship>, <li path>)."""
return self._img.linked.list_related(li_ignore=li_ignore)
def parse_linked_name(self, li_name, allow_unknown=False):
"""Given a string representing a linked image child name,
returns linked image name object representing the same name.
'allow_unknown' indicates whether the name must represent
actual children or simply be syntactically correct."""
return self._img.linked.parse_name(li_name, allow_unknown)
def parse_linked_name_list(self, li_name_list, allow_unknown=False):
"""Given a list of strings representing linked image child
names, returns a list of linked image name objects
representing the same names.
For all other parameters, refer to the 'parse_linked_name'
function for an explanation of their usage and effects."""
return [
self.parse_linked_name(li_name, allow_unknown)
for li_name in li_name_list
]
def describe(self):
"""Returns None if no plan is ready yet, otherwise returns
a PlanDescription."""
return self.__plan_desc
def prepare(self):
"""Takes care of things which must be done before the plan can
be executed. This includes downloading the packages to disk and
preparing the indexes to be updated during execution. Should
only be called once a gen_plan_*() method has been called. If
a plan is abandoned after calling this method, reset() should
be called."""
self._acquire_activity_lock()
try:
self._img.lock()
except:
self._activity_lock.release()
raise
try:
if not self._img.imageplan:
raise apx.PlanMissingException()
if not self.__planned_children:
# if we never planned children images then we
# didn't finish planning.
raise apx.PlanMissingException()
if self.__prepared:
raise apx.AlreadyPreparedException()
self._enable_cancel()
try:
self._img.imageplan.preexecute()
except search_errors.ProblematicPermissionsIndexException, e:
raise apx.ProblematicPermissionsIndexException(e)
except:
raise
self._disable_cancel()
self.__prepared = True
except apx.CanceledException, e:
self._cancel_done()
if self._img.history.operation_name:
# If an operation is in progress, log
# the error and mark its end.
self.log_operation_end(error=e)
raise
except Exception, e:
self._cancel_cleanup_exception()
if self._img.history.operation_name:
# If an operation is in progress, log
# the error and mark its end.
self.log_operation_end(error=e)
raise
except:
# Handle exceptions that are not subclasses of
# Exception.
self._cancel_cleanup_exception()
if self._img.history.operation_name:
# If an operation is in progress, log
# the error and mark its end.
exc_type, exc_value, exc_traceback = \
sys.exc_info()
self.log_operation_end(error=exc_type)
raise
finally:
self._img.cleanup_downloads()
self._img.unlock()
try:
if int(os.environ.get("PKG_DUMP_STATS", 0)) > 0:
self._img.transport.stats.dump()
except ValueError:
# Don't generate stats if an invalid value
# is supplied.
pass
self._activity_lock.release()
self._img.linked.api_recurse_prepare(self.__progresstracker)
def execute_plan(self):
"""Executes the plan. This is uncancelable once it begins.
Should only be called after the prepare method has been
called. After plan execution, reset() should be called."""
self._acquire_activity_lock()
try:
self._disable_cancel()
self._img.lock()
except:
self._activity_lock.release()
raise
try:
if not self._img.imageplan:
raise apx.PlanMissingException()
if not self.__prepared:
raise apx.PrematureExecutionException()
if self.__executed:
raise apx.AlreadyExecutedException()
try:
be = bootenv.BootEnv(self._img)
except RuntimeError:
be = bootenv.BootEnvNull(self._img)
self._img.bootenv = be
if self.__new_be == False and \
self._img.imageplan.reboot_needed() and \
self._img.is_liveroot():
e = apx.RebootNeededOnLiveImageException()
self.log_operation_end(error=e)
raise e
# Before proceeding, create a backup boot environment if
# requested.
if self.__backup_be == True:
try:
be.create_backup_be(
be_name=self.__backup_be_name)
except Exception, e:
self.log_operation_end(error=e)
raise
except:
# Handle exceptions that are not
# subclasses of Exception.
exc_type, exc_value, exc_traceback = \
sys.exc_info()
self.log_operation_end(error=exc_type)
raise
# After (possibly) creating backup be, determine if
# operation should execute on a clone of current BE.
if self.__new_be == True:
try:
be.init_image_recovery(self._img,
self.__be_name)
except Exception, e:
self.log_operation_end(error=e)
raise
except:
# Handle exceptions that are not
# subclasses of Exception.
exc_type, exc_value, exc_traceback = \
sys.exc_info()
self.log_operation_end(error=exc_type)
raise
# check if things gained underneath us
if self._img.is_liveroot():
e = apx.UnableToCopyBE()
self.log_operation_end(error=e)
raise e
raise_later = None
# we're about to execute a plan so change our current
# working directory to / so that we won't fail if we
# try to remove our current working directory
os.chdir(os.sep)
try:
try:
self._img.imageplan.execute()
except apx.WrapIndexingException, e:
raise_later = e
if not self._img.linked.nothingtodo():
self._img.linked.syncmd()
except RuntimeError, e:
if self.__new_be == True:
be.restore_image()
else:
be.restore_install_uninstall()
# Must be done after bootenv restore.
self.log_operation_end(error=e)
raise
except search_errors.IndexLockedException, e:
error = apx.IndexLockedException(e)
self.log_operation_end(error=error)
raise error
except search_errors.ProblematicPermissionsIndexException, e:
error = apx.ProblematicPermissionsIndexException(e)
self.log_operation_end(error=error)
raise error
except search_errors.InconsistentIndexException, e:
error = apx.CorruptedIndexException(e)
self.log_operation_end(error=error)
raise error
except NonzeroExitException, e:
# Won't happen during update
be.restore_install_uninstall()
error = apx.ActuatorException(e)
self.log_operation_end(error=error)
raise error
except Exception, e:
if self.__new_be == True:
be.restore_image()
else:
be.restore_install_uninstall()
# Must be done after bootenv restore.
self.log_operation_end(error=e)
raise
except:
# Handle exceptions that are not subclasses of
# Exception.
exc_type, exc_value, exc_traceback = \
sys.exc_info()
if self.__new_be == True:
be.restore_image()
else:
be.restore_install_uninstall()
# Must be done after bootenv restore.
self.log_operation_end(error=exc_type)
raise
self._img.linked.api_recurse_execute(
self.__progresstracker)
self.__finished_execution(be)
if raise_later:
raise raise_later
finally:
self._img.cleanup_downloads()
if self._img.locked:
self._img.unlock()
self._activity_lock.release()
def __finished_execution(self, be):
if self._img.imageplan.state != plandesc.EXECUTED_OK:
if self.__new_be == True:
be.restore_image()
else:
be.restore_install_uninstall()
error = apx.ImageplanStateException(
self._img.imageplan.state)
# Must be done after bootenv restore.
self.log_operation_end(error=error)
raise error
if self._img.imageplan.boot_archive_needed() or \
self.__new_be:
be.update_boot_archive()
if self.__new_be == True:
be.activate_image(set_active=self.__be_activate)
else:
be.activate_install_uninstall()
self._img.cleanup_cached_content()
# If the end of the operation wasn't already logged
# by one of the previous operations, then log it as
# ending now.
if self._img.history.operation_name:
self.log_operation_end(release_notes=
self._img.imageplan.pd.release_notes_name)
self.__executed = True
def set_plan_license_status(self, pfmri, plicense, accepted=None,
displayed=None):
"""Sets the license status for the given package FMRI and
license entry.
'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"""
self._acquire_activity_lock()
try:
try:
self._disable_cancel()
except apx.CanceledException:
self._cancel_done()
raise
if not self._img.imageplan:
raise apx.PlanMissingException()
for pp in self.__plan_desc.pkg_plans:
if pp.destination_fmri == pfmri:
pp.set_license_status(plicense,
accepted=accepted,
displayed=displayed)
break
finally:
self._activity_lock.release()
def refresh(self, full_refresh=False, pubs=None, immediate=False):
"""Refreshes the metadata (e.g. catalog) for one or more
publishers.
'full_refresh' is an optional boolean value indicating whether
a full retrieval of publisher metadata (e.g. catalogs) or only
an update to the existing metadata should be performed. When
True, 'immediate' is also set to True.
'pubs' is a list of publisher prefixes or publisher objects
to refresh. Passing an empty list or using the default value
implies all publishers.
'immediate' is an optional boolean value indicating whether
a refresh should occur now. If False, a publisher's selected
repository will only be checked for updates if the update
interval period recorded in the image configuration has been
exceeded.
Currently returns an image object, allowing existing code to
work while the rest of the API is put into place."""
self._acquire_activity_lock()
try:
self._disable_cancel()
self._img.lock()
try:
self.__refresh(full_refresh=full_refresh,
pubs=pubs, immediate=immediate)
return self._img
finally:
self._img.unlock()
self._img.cleanup_downloads()
except apx.CanceledException:
self._cancel_done()
raise
finally:
try:
if int(os.environ.get("PKG_DUMP_STATS", 0)) > 0:
self._img.transport.stats.dump()
except ValueError:
# Don't generate stats if an invalid value
# is supplied.
pass
self._activity_lock.release()
def __refresh(self, full_refresh=False, pubs=None, immediate=False):
"""Private refresh method; caller responsible for locking and
cleanup."""
self._img.refresh_publishers(full_refresh=full_refresh,
immediate=immediate, pubs=pubs,
progtrack=self.__progresstracker)
def __licenses(self, pfmri, mfst, alt_pub=None):
"""Private function. Returns the license info from the
manifest mfst."""
license_lst = []
for lic in mfst.gen_actions_by_type("license"):
license_lst.append(LicenseInfo(pfmri, lic,
img=self._img, alt_pub=alt_pub))
return license_lst
@_LockedCancelable()
def get_pkg_categories(self, installed=False, pubs=misc.EmptyI,
repos=None):
"""Returns an ordered list of tuples of the form (scheme,
category) containing the names of all categories in use by
the last version of each unique package in the catalog on a
per-publisher basis.
'installed' is an optional boolean value indicating whether
only the categories used by currently installed packages
should be returned. If False, the categories used by the
latest vesion of every known package will be returned
instead.
'pubs' is an optional list of publisher prefixes to restrict
the results to.
'repos' is a list of URI strings or RepositoryURI objects that
represent the locations of package repositories to list packages
for.
"""
if installed:
excludes = misc.EmptyI
else:
excludes = self._img.list_excludes()
if repos:
ignored, ignored, known_cat, inst_cat = \
self.__get_alt_pkg_data(repos)
if installed:
pkg_cat = inst_cat
else:
pkg_cat = known_cat
elif installed:
pkg_cat = self._img.get_catalog(
self._img.IMG_CATALOG_INSTALLED)
else:
pkg_cat = self._img.get_catalog(
self._img.IMG_CATALOG_KNOWN)
return sorted(pkg_cat.categories(excludes=excludes, pubs=pubs))
def __map_installed_newest(self, pubs, known_cat=None):
"""Private function. Maps incorporations and publisher
relationships for installed packages and returns them
as a tuple of (pub_ranks, inc_stems, inc_vers, inst_stems,
ren_stems, ren_inst_stems).
"""
img_cat = self._img.get_catalog(
self._img.IMG_CATALOG_INSTALLED)
cat_info = frozenset([img_cat.DEPENDENCY])
inst_stems = {}
ren_inst_stems = {}
ren_stems = {}
inc_stems = {}
inc_vers = {}
pub_ranks = self._img.get_publisher_ranks()
# The incorporation list should include all installed,
# incorporated packages from all publishers.
for t in img_cat.entry_actions(cat_info):
(pub, stem, ver), entry, actions = t
inst_stems[stem] = ver
pkgr = False
targets = set()
try:
for a in actions:
if a.name == "set" and \
a.attrs["name"] == "pkg.renamed":
pkgr = True
continue
elif a.name != "depend":
continue
if a.attrs["type"] == "require":
# Because the actions are not
# returned in a guaranteed
# order, the dependencies will
# have to be recorded for
# evaluation later.
targets.add(a.attrs["fmri"])
elif a.attrs["type"] == "incorporate":
# Record incorporated packages.
tgt = fmri.PkgFmri(
a.attrs["fmri"])
tver = tgt.version
# incorporates without a version
# should be ignored.
if not tver:
continue
over = inc_vers.get(
tgt.pkg_name, None)
# In case this package has been
# incorporated more than once,
# use the newest version.
if over is not None and \
over > tver:
continue
inc_vers[tgt.pkg_name] = tver
except apx.InvalidPackageErrors:
# For mapping purposes, ignore unsupported
# (and invalid) actions. This is necessary so
# that API consumers can discover new package
# data that may be needed to perform an upgrade
# so that the API can understand them.
pass
if pkgr:
for f in targets:
tgt = fmri.PkgFmri(f)
ren_stems[tgt.pkg_name] = stem
ren_inst_stems.setdefault(stem,
set())
ren_inst_stems[stem].add(
tgt.pkg_name)
def check_stem(t, entry):
pub, stem, ver = t
if stem in inst_stems:
iver = inst_stems[stem]
if stem in ren_inst_stems or \
ver == iver:
# The package has been renamed
# or the entry is for the same
# version as that which is
# installed, so doesn't need
# to be checked.
return False
# The package may have been renamed in
# a newer version, so must be checked.
return True
elif stem in inc_vers:
# Package is incorporated, but not
# installed, so should be checked.
return True
tgt = ren_stems.get(stem, None)
while tgt is not None:
# This seems counter-intuitive, but
# for performance and other reasons,
# this stem should only be checked
# for a rename if it is incorporated
# or installed using a previous name.
if tgt in inst_stems or \
tgt in inc_vers:
return True
tgt = ren_stems.get(tgt, None)
# Package should not be checked.
return False
if not known_cat:
known_cat = self._img.get_catalog(
self._img.IMG_CATALOG_KNOWN)
# Find terminal rename entry for all known packages not
# rejected by check_stem().
for t, entry, actions in known_cat.entry_actions(cat_info,
cb=check_stem, last=True):
pkgr = False
targets = set()
try:
for a in actions:
if a.name == "set" and \
a.attrs["name"] == "pkg.renamed":
pkgr = True
continue
if a.name != "depend":
continue
if a.attrs["type"] != "require":
continue
# Because the actions are not
# returned in a guaranteed
# order, the dependencies will
# have to be recorded for
# evaluation later.
targets.add(a.attrs["fmri"])
except apx.InvalidPackageErrors:
# For mapping purposes, ignore unsupported
# (and invalid) actions. This is necessary so
# that API consumers can discover new package
# data that may be needed to perform an upgrade
# so that the API can understand them.
pass
if pkgr:
pub, stem, ver = t
for f in targets:
tgt = fmri.PkgFmri(f)
ren_stems[tgt.pkg_name] = stem
# Determine highest ranked publisher for package stems
# listed in installed incorporations.
def pub_order(a, b):
return cmp(pub_ranks[a][0], pub_ranks[b][0])
for p in sorted(pub_ranks, cmp=pub_order):
if pubs and p not in pubs:
continue
for stem in known_cat.names(pubs=[p]):
if stem in inc_vers:
inc_stems.setdefault(stem, p)
return (pub_ranks, inc_stems, inc_vers, inst_stems, ren_stems,
ren_inst_stems)
def __get_temp_repo_pubs(self, repos):
"""Private helper function to retrieve publisher information
from list of temporary repositories. Caller is responsible
for locking."""
ret_pubs = []
for repo_uri in repos:
if isinstance(repo_uri, basestring):
repo = publisher.RepositoryURI(repo_uri)
else:
# Already a RepositoryURI.
repo = repo_uri
pubs = None
try:
pubs = self._img.transport.get_publisherdata(
repo, ccancel=self.__check_cancel)
except apx.UnsupportedRepositoryOperation:
raise apx.RepoPubConfigUnavailable(
location=str(repo))
if not pubs:
# Empty repository configuration.
raise apx.RepoPubConfigUnavailable(
location=str(repo))
for p in pubs:
psrepo = p.repository
if not psrepo:
# Repository configuration info wasn't
# provided, so assume origin is
# repo_uri.
p.repository = publisher.Repository(
origins=[repo_uri])
elif not psrepo.origins:
# Repository configuration was provided,
# but without an origin. Assume the
# repo_uri is the origin.
psrepo.add_origin(repo_uri)
elif repo not in psrepo.origins:
# If the repo_uri used is not
# in the list of sources, then
# add it as the first origin.
psrepo.origins.insert(0, repo)
ret_pubs.extend(pubs)
return sorted(ret_pubs)
def __get_alt_pkg_data(self, repos):
"""Private helper function to retrieve composite known and
installed catalog and package repository map for temporary
set of package repositories. Returns (pkg_pub_map, alt_pubs,
known_cat, inst_cat)."""
repos = set(repos)
eid = ",".join(sorted(map(str, repos)))
try:
return self.__alt_sources[eid]
except KeyError:
# Need to cache new set of alternate sources.
pass
img_inst_cat = self._img.get_catalog(
self._img.IMG_CATALOG_INSTALLED)
op_time = datetime.datetime.utcnow()
pubs = self.__get_temp_repo_pubs(repos)
progtrack = self.__progresstracker
# Create temporary directories.
tmpdir = tempfile.mkdtemp()
pkg_repos = {}
pkg_pub_map = {}
try:
progtrack.refresh_start(len(pubs), full_refresh=False)
failed = []
pub_cats = []
for pub in pubs:
# Assign a temporary meta root to each
# publisher.
meta_root = os.path.join(tmpdir, str(id(pub)))
misc.makedirs(meta_root)
pub.meta_root = meta_root
pub.transport = self._img.transport
repo = pub.repository
pkg_repos[id(repo)] = repo
# Retrieve each publisher's catalog.
progtrack.refresh_start_pub(pub)
try:
pub.refresh()
except apx.PermissionsException, e:
failed.append((pub, e))
# No point in continuing since no data
# can be written.
break
except apx.ApiException, e:
failed.append((pub, e))
continue
finally:
progtrack.refresh_end_pub(pub)
pub_cats.append((
pub.prefix,
repo,
pub.catalog
))
progtrack.refresh_done()
if failed:
total = len(pub_cats) + len(failed)
e = apx.CatalogRefreshException(failed, total,
len(pub_cats))
raise e
# Determine upgradability.
newest = {}
for pfx, repo, cat in [(None, None, img_inst_cat)] + \
pub_cats:
if pfx:
pkg_list = cat.fmris(last=True,
pubs=[pfx])
else:
pkg_list = cat.fmris(last=True)
for f in pkg_list:
nver, snver = newest.get(f.pkg_name,
(None, None))
if f.version > nver:
newest[f.pkg_name] = (f.version,
str(f.version))
# Build list of installed packages.
inst_stems = {}
for t, entry in img_inst_cat.tuple_entries():
states = entry["metadata"]["states"]
if pkgdefs.PKG_STATE_INSTALLED not in states:
continue
pub, stem, ver = t
inst_stems.setdefault(pub, {})
inst_stems[pub].setdefault(stem, {})
inst_stems[pub][stem][ver] = False
# Now create composite known and installed catalogs.
compicat = pkg.catalog.Catalog(batch_mode=True,
sign=False)
compkcat = pkg.catalog.Catalog(batch_mode=True,
sign=False)
sparts = (
(pfx, cat, repo, name, cat.get_part(name, must_exist=True))
for pfx, repo, cat in pub_cats
for name in cat.parts
)
excludes = self._img.list_excludes()
proc_stems = {}
for pfx, cat, repo, name, spart in sparts:
# 'spart' is the source part.
if spart is None:
# Client hasn't retrieved this part.
continue
# New known part.
nkpart = compkcat.get_part(name)
nipart = compicat.get_part(name)
base = name.startswith("catalog.base.")
# Avoid accessor overhead since these will be
# used for every entry.
cat_ver = cat.version
dp = cat.get_part("catalog.dependency.C",
must_exist=True)
for t, sentry in spart.tuple_entries(pubs=[pfx]):
pub, stem, ver = t
pkg_pub_map.setdefault(pub, {})
pkg_pub_map[pub].setdefault(stem, {})
pkg_pub_map[pub][stem].setdefault(ver,
set())
pkg_pub_map[pub][stem][ver].add(
id(repo))
if pub in proc_stems and \
stem in proc_stems[pub] and \
ver in proc_stems[pub][stem]:
if id(cat) != proc_stems[pub][stem][ver]:
# Already added from another
# catalog.
continue
else:
proc_stems.setdefault(pub, {})
proc_stems[pub].setdefault(stem,
{})
proc_stems[pub][stem][ver] = \
id(cat)
installed = False
if pub in inst_stems and \
stem in inst_stems[pub] and \
ver in inst_stems[pub][stem]:
installed = True
inst_stems[pub][stem][ver] = \
True
# copy() is too slow here and catalog
# entries are shallow so this should be
# sufficient.
entry = dict(sentry.iteritems())
if not base:
# Nothing else to do except add
# the entry for non-base catalog
# parts.
nkpart.add(metadata=entry,
op_time=op_time, pub=pub,
stem=stem, ver=ver)
if installed:
nipart.add(
metadata=entry,
op_time=op_time,
pub=pub, stem=stem,
ver=ver)
continue
# Only the base catalog part stores
# package state information and/or
# other metadata.
mdata = entry["metadata"] = {}
states = [pkgdefs.PKG_STATE_KNOWN,
pkgdefs.PKG_STATE_ALT_SOURCE]
if cat_ver == 0:
states.append(
pkgdefs.PKG_STATE_V0)
else:
# Assume V1 catalog source.
states.append(
pkgdefs.PKG_STATE_V1)
if installed:
states.append(
pkgdefs.PKG_STATE_INSTALLED)
nver, snver = newest.get(stem,
(None, None))
if snver is not None and ver != snver:
states.append(
pkgdefs.PKG_STATE_UPGRADABLE)
# Determine if package is obsolete or
# has been renamed and mark with
# appropriate state.
dpent = None
if dp is not None:
dpent = dp.get_entry(pub=pub,
stem=stem, ver=ver)
if dpent is not None:
for a in dpent["actions"]:
# Constructing action
# objects for every
# action would be a lot
# slower, so a simple
# string match is done
# first so that only
# interesting actions
# get constructed.
if not a.startswith("set"):
continue
if not ("pkg.obsolete" in a or \
"pkg.renamed" in a):
continue
try:
act = pkg.actions.fromstr(a)
except pkg.actions.ActionError:
# If the action can't be
# parsed or is not yet
# supported, continue.
continue
if act.attrs["value"].lower() != "true":
continue
if act.attrs["name"] == "pkg.obsolete":
states.append(
pkgdefs.PKG_STATE_OBSOLETE)
elif act.attrs["name"] == "pkg.renamed":
if not act.include_this(
excludes):
continue
states.append(
pkgdefs.PKG_STATE_RENAMED)
mdata["states"] = states
# Add base entries.
nkpart.add(metadata=entry,
op_time=op_time, pub=pub, stem=stem,
ver=ver)
if installed:
nipart.add(metadata=entry,
op_time=op_time, pub=pub,
stem=stem, ver=ver)
pub_map = {}
for pub in pubs:
try:
opub = pub_map[pub.prefix]
except KeyError:
nrepo = publisher.Repository()
opub = publisher.Publisher(pub.prefix,
catalog=compkcat, repository=nrepo)
pub_map[pub.prefix] = opub
rid_map = {}
for pub in pkg_pub_map:
for stem in pkg_pub_map[pub]:
for ver in pkg_pub_map[pub][stem]:
rids = tuple(sorted(
pkg_pub_map[pub][stem][ver]))
if not rids in rid_map:
# Create a publisher and
# repository for this
# unique set of origins.
origins = []
map(origins.extend, [
pkg_repos.get(rid).origins
for rid in rids
])
npub = \
copy.copy(pub_map[pub])
nrepo = npub.repository
nrepo.origins = origins
assert npub.catalog == \
compkcat
rid_map[rids] = npub
pkg_pub_map[pub][stem][ver] = \
rid_map[rids]
# Now consolidate all origins for each publisher under
# a single repository object for the caller.
for pub in pubs:
npub = pub_map[pub.prefix]
nrepo = npub.repository
for o in pub.repository.origins:
if not nrepo.has_origin(o):
nrepo.add_origin(o)
assert npub.catalog == compkcat
for compcat in (compicat, compkcat):
compcat.batch_mode = False
compcat.finalize()
compcat.read_only = True
# Cache these for future callers.
self.__alt_sources[eid] = (pkg_pub_map,
sorted(pub_map.values()), compkcat, compicat)
return self.__alt_sources[eid]
finally:
shutil.rmtree(tmpdir, ignore_errors=True)
self._img.cleanup_downloads()
@_LockedGenerator()
def get_pkg_list(self, pkg_list, cats=None, collect_attrs=False,
patterns=misc.EmptyI, pubs=misc.EmptyI, raise_unmatched=False,
ranked=False, repos=None, return_fmris=False, variants=False):
"""A generator function that produces tuples of the form:
(
(
pub, - (string) the publisher of the package
stem, - (string) the name of the package
version - (string) the version of the package
),
summary, - (string) the package summary
categories, - (list) string tuples of (scheme, category)
states, - (list) PackageInfo states
attributes - (dict) package attributes
)
Results are always sorted by stem, publisher, and then in
descending version order.
'pkg_list' is one of the following constant values indicating
what base set of package data should be used for results:
LIST_ALL
All known packages.
LIST_INSTALLED
Installed packages.
LIST_INSTALLED_NEWEST
Installed packages and the newest
versions of packages not installed.
Renamed packages that are listed in
an installed incorporation will be
excluded unless they are installed.
LIST_NEWEST
The newest versions of all known packages
that match the provided patterns and
other criteria.
LIST_UPGRADABLE
Packages that are installed and upgradable.
'cats' is an optional list of package category tuples of the
form (scheme, cat) to restrict the results to. If a package
is assigned to any of the given categories, it will be
returned. A value of [] will return packages not assigned
to any package category. A value of None indicates that no
package category filtering should be applied.
'collect_attrs' is an optional boolean that indicates whether
all package attributes should be collected and returned in the
fifth element of the return tuple. If False, that element will
be an empty dictionary.
'patterns' is an optional list of FMRI wildcard strings to
filter results by.
'pubs' is an optional list of publisher prefixes to restrict
the results to.
'raise_unmatched' is an optional boolean value that indicates
whether an InventoryException should be raised if any patterns
(after applying all other filtering and returning all results)
didn't match any packages.
'ranked' is an optional boolean value that indicates whether
only the matching package versions from the highest-ranked
publisher should be returned. This option is ignored for
patterns that explicitly specify the publisher to match.
'repos' is a list of URI strings or RepositoryURI objects that
represent the locations of package repositories to list packages
for.
'return_fmris' is an optional boolean value that indicates that
an FMRI object should be returned in place of the (pub, stem,
ver) tuple that is normally returned.
'variants' is an optional boolean value that indicates that
packages that are for arch or zone variants not applicable to
this image should be returned.
Please note that this function may invoke network operations
to retrieve the requested package information."""
return self.__get_pkg_list(pkg_list, cats=cats,
collect_attrs=collect_attrs, patterns=patterns, pubs=pubs,
raise_unmatched=raise_unmatched, ranked=ranked, repos=repos,
return_fmris=return_fmris, variants=variants)
def __get_pkg_list(self, pkg_list, cats=None, collect_attrs=False,
inst_cat=None, known_cat=None, patterns=misc.EmptyI,
pubs=misc.EmptyI, raise_unmatched=False, ranked=False, repos=None,
return_fmris=False, variants=False):
"""This is the implementation of get_pkg_list. The other
function is a wrapper that uses locking. The separation was
necessary because of API functions that already perform locking
but need to use get_pkg_list(). This is a generator
function."""
installed = inst_newest = newest = upgradable = False
if pkg_list == self.LIST_INSTALLED:
installed = True
elif pkg_list == self.LIST_INSTALLED_NEWEST:
inst_newest = True
elif pkg_list == self.LIST_NEWEST:
newest = True
elif pkg_list == self.LIST_UPGRADABLE:
upgradable = True
# Each pattern in patterns can be a partial or full FMRI, so
# extract the individual components for use in filtering.
illegals = []
pat_tuples = {}
pat_versioned = False
latest_pats = set()
seen = set()
npatterns = set()
for pat, error, pfmri, matcher in self.parse_fmri_patterns(
patterns):
if error:
illegals.append(error)
continue
# Duplicate patterns are ignored.
sfmri = str(pfmri)
if sfmri in seen:
# A different form of the same pattern
# was specified already; ignore this
# one (e.g. pkg:/network/ping,
# /network/ping).
continue
# Track used patterns.
seen.add(sfmri)
npatterns.add(pat)
if "@" in pat:
# Mark that a pattern contained version
# information. This is used for a listing
# optimization later on.
pat_versioned = True
if getattr(pfmri.version, "match_latest", None):
latest_pats.add(pat)
pat_tuples[pat] = (pfmri.tuple(), matcher)
patterns = npatterns
del npatterns, seen
if illegals:
raise apx.InventoryException(illegal=illegals)
if repos:
ignored, ignored, known_cat, inst_cat = \
self.__get_alt_pkg_data(repos)
# For LIST_INSTALLED_NEWEST, installed packages need to be
# determined and incorporation and publisher relationships
# mapped.
if inst_newest:
pub_ranks, inc_stems, inc_vers, inst_stems, ren_stems, \
ren_inst_stems = self.__map_installed_newest(
pubs, known_cat=known_cat)
else:
pub_ranks = inc_stems = inc_vers = inst_stems = \
ren_stems = ren_inst_stems = misc.EmptyDict
if installed or upgradable:
if inst_cat:
pkg_cat = inst_cat
else:
pkg_cat = self._img.get_catalog(
self._img.IMG_CATALOG_INSTALLED)
# Don't need to perform variant filtering if only
# listing installed packages.
variants = True
elif known_cat:
pkg_cat = known_cat
else:
pkg_cat = self._img.get_catalog(
self._img.IMG_CATALOG_KNOWN)
cat_info = frozenset([pkg_cat.DEPENDENCY, pkg_cat.SUMMARY])
# Keep track of when the newest version has been found for
# each incorporated stem.
slist = set()
# Keep track of listed stems for all other packages on a
# per-publisher basis.
nlist = collections.defaultdict(int)
def check_state(t, entry):
states = entry["metadata"]["states"]
pkgi = pkgdefs.PKG_STATE_INSTALLED in states
pkgu = pkgdefs.PKG_STATE_UPGRADABLE in states
pub, stem, ver = t
if upgradable:
# If package is marked upgradable, return it.
return pkgu
elif pkgi:
# Nothing more to do here.
return True
elif stem in inst_stems:
# Some other version of this package is
# installed, so this one should not be
# returned.
return False
# Attempt to determine if this package is installed
# under a different name or constrained under a
# different name.
tgt = ren_stems.get(stem, None)
while tgt is not None:
if tgt in inc_vers:
# Package is incorporated under a
# different name, so allow this
# to fallthrough to the incoporation
# evaluation.
break
elif tgt in inst_stems:
# Package is installed under a
# different name, so skip it.
return False
tgt = ren_stems.get(tgt, None)
# Attempt to find a suitable version to return.
if stem in inc_vers:
# For package stems that are incorporated, only
# return the newest successor version based on
# publisher rank.
if stem in slist:
# Newest version already returned.
return False
if stem in inc_stems and \
pub != inc_stems[stem]:
# This entry is for a lower-ranked
# publisher.
return False
# XXX version should not require build release.
ever = pkg.version.Version(ver)
# If the entry's version is a successor to
# the incorporated version, then this is the
# 'newest' version of this package since
# entries are processed in descending version
# order.
iver = inc_vers[stem]
if ever.is_successor(iver,
pkg.version.CONSTRAINT_AUTO):
slist.add(stem)
return True
return False
pkg_stem = "!".join((pub, stem))
if pkg_stem in nlist:
# A newer version has already been listed for
# this stem and publisher.
return False
return True
filter_cb = None
if inst_newest or upgradable:
# Filtering needs to be applied.
filter_cb = check_state
excludes = self._img.list_excludes()
img_variants = self._img.get_variants()
matched_pats = set()
pkg_matching_pats = None
# Retrieve only the newest package versions for LIST_NEWEST if
# none of the patterns have version information and variants are
# included. (This cuts down on the number of entries that have
# to be filtered.)
use_last = newest and not pat_versioned and variants
if ranked:
# If caller requested results to be ranked by publisher,
# then the list of publishers to return must be passed
# to entry_actions() in rank order.
pub_ranks = self._img.get_publisher_ranks()
if not pubs:
# It's important that the list of possible
# publishers is gleaned from the catalog
# directly and not image configuration so
# that temporary sources (archives, etc.)
# work as expected.
pubs = pkg_cat.publishers()
for p in pubs:
pub_ranks.setdefault(p, (99, (p, False, False)))
def pub_order(a, b):
res = cmp(pub_ranks[a], pub_ranks[b])
if res != 0:
return res
return cmp(a, b)
pubs = sorted(pubs, cmp=pub_order)
ranked_stems = {}
for t, entry, actions in pkg_cat.entry_actions(cat_info,
cb=filter_cb, excludes=excludes, last=use_last,
ordered=True, pubs=pubs):
pub, stem, ver = t
omit_ver = False
omit_package = None
pkg_stem = "!".join((pub, stem))
if newest and pkg_stem in nlist:
# A newer version has already been listed, so
# any additional entries need to be marked for
# omission before continuing.
omit_package = True
elif ranked and not patterns and \
ranked_stems.get(stem, pub) != pub:
# A different version from a higher-ranked
# publisher has been returned already, so skip
# this one. This can only be done safely at
# this point if no patterns have been specified,
# since publisher-specific patterns override
# ranking behaviour.
omit_package = True
else:
nlist[pkg_stem] += 1
if raise_unmatched:
pkg_matching_pats = set()
if not omit_package:
ever = None
for pat in patterns:
(pat_pub, pat_stem, pat_ver), matcher = \
pat_tuples[pat]
if pat_pub is not None and \
pub != pat_pub:
# Publisher doesn't match.
if omit_package is None:
omit_package = True
continue
elif ranked and not pat_pub and \
ranked_stems.get(stem, pub) != pub:
# A different version from a
# higher-ranked publisher has
# been returned already, so skip
# this one since no publisher
# was specified for the pattern.
if omit_package is None:
omit_package = True
continue
if matcher == self.MATCH_EXACT:
if pat_stem != stem:
# Stem doesn't match.
if omit_package is None:
omit_package = \
True
continue
elif matcher == self.MATCH_FMRI:
if not ("/" + stem).endswith(
"/" + pat_stem):
# Stem doesn't match.
if omit_package is None:
omit_package = \
True
continue
elif matcher == self.MATCH_GLOB:
if not fnmatch.fnmatchcase(stem,
pat_stem):
# Stem doesn't match.
if omit_package is None:
omit_package = \
True
continue
if pat_ver is not None:
if ever is None:
# Avoid constructing a
# version object more
# than once for each
# entry.
ever = pkg.version.Version(ver)
if not ever.is_successor(pat_ver,
pkg.version.CONSTRAINT_AUTO):
if omit_package is None:
omit_package = \
True
omit_ver = True
continue
if pat in latest_pats and \
nlist[pkg_stem] > 1:
# Package allowed by pattern,
# but isn't the "latest"
# version.
if omit_package is None:
omit_package = True
omit_ver = True
continue
# If this entry matched at least one
# pattern, then ensure it is returned.
omit_package = False
if not raise_unmatched:
# It's faster to stop as soon
# as a match is found.
break
# If caller has requested other match
# cases be raised as an exception, then
# all patterns must be tested for every
# entry. This is slower, so only done
# if necessary.
pkg_matching_pats.add(pat)
if omit_package:
# Package didn't match critera; skip it.
if (filter_cb is not None or (newest and
pat_versioned)) and omit_ver and \
nlist[pkg_stem] == 1:
# If omitting because of version, and
# no other versions have been returned
# yet for this stem, then discard
# tracking entry so that other
# versions will be listed.
del nlist[pkg_stem]
slist.discard(stem)
continue
# Perform image arch and zone variant filtering so
# that only packages appropriate for this image are
# returned, but only do this for packages that are
# not installed.
pcats = []
pkgr = False
unsupported = False
summ = None
targets = set()
omit_var = False
states = entry["metadata"]["states"]
pkgi = pkgdefs.PKG_STATE_INSTALLED in states
ddm = lambda: collections.defaultdict(list)
attrs = collections.defaultdict(ddm)
try:
for a in actions:
if a.name == "depend" and \
a.attrs["type"] == "require":
targets.add(a.attrs["fmri"])
continue
if a.name != "set":
continue
atname = a.attrs["name"]
atvalue = a.attrs["value"]
if collect_attrs:
atvlist = a.attrlist("value")
# XXX Need to describe this data
# structure sanely somewhere.
mods = tuple(
(k, tuple(sorted(a.attrlist(k))))
for k in sorted(a.attrs.iterkeys())
if k not in ("name", "value")
)
attrs[atname][mods].extend(atvlist)
if atname == "pkg.summary":
summ = atvalue
continue
if atname == "description":
if summ is None:
# Historical summary
# field.
summ = atvalue
# pylint: disable=W0106
collect_attrs and \
attrs["pkg.summary"] \
[mods]. \
extend(atvlist)
continue
if atname == "info.classification":
pcats.extend(
a.parse_category_info())
if pkgi:
# No filtering for installed
# packages.
continue
# Rename filtering should only be
# performed for incorporated packages
# at this point.
if atname == "pkg.renamed":
if stem in inc_vers:
pkgr = True
continue
if variants or \
not atname.startswith("variant."):
# No variant filtering required.
continue
# For all variants explicitly set in the
# image, elide packages that are not for
# a matching variant value.
is_list = type(atvalue) == list
for vn, vv in img_variants.iteritems():
if vn == atname and \
((is_list and
vv not in atvalue) or \
(not is_list and
vv != atvalue)):
omit_package = True
omit_var = True
break
except apx.InvalidPackageErrors:
# Ignore errors for packages that have invalid
# or unsupported metadata. This is necessary so
# that API consumers can discover new package
# data that may be needed to perform an upgrade
# so that the API can understand them.
states = set(states)
states.add(PackageInfo.UNSUPPORTED)
unsupported = True
if not pkgi and pkgr and stem in inc_vers:
# If the package is not installed, but this is
# the terminal version entry for the stem and
# it is an incorporated package, then omit the
# package if it has been installed or is
# incorporated using one of the new names.
for e in targets:
tgt = e
while tgt is not None:
if tgt in ren_inst_stems or \
tgt in inc_vers:
omit_package = True
break
tgt = ren_stems.get(tgt, None)
if omit_package:
# Package didn't match criteria; skip it.
if (filter_cb is not None or newest) and \
omit_var and nlist[pkg_stem] == 1:
# If omitting because of variant, and
# no other versions have been returned
# yet for this stem, then discard
# tracking entry so that other
# versions will be listed.
del nlist[pkg_stem]
slist.discard(stem)
continue
if cats is not None:
if not cats:
if pcats:
# Only want packages with no
# categories.
continue
elif not [sc for sc in cats if sc in pcats]:
# Package doesn't match specified
# category criteria.
continue
# Return the requested package data.
if not unsupported:
# Prevent modification of state data.
states = frozenset(states)
if raise_unmatched:
# Only after all other filtering has been
# applied are the patterns that the package
# matched considered "matching".
matched_pats.update(pkg_matching_pats)
if ranked:
# Only after all other filtering has been
# applied is the stem considered to have been
# a "ranked" match.
ranked_stems.setdefault(stem, pub)
if return_fmris:
pfmri = fmri.PkgFmri(name=stem, publisher=pub,
version=ver)
yield (pfmri, summ, pcats, states, attrs)
else:
yield (t, summ, pcats, states, attrs)
if raise_unmatched:
# Caller has requested that non-matching patterns or
# patterns that match multiple packages cause an
# exception to be raised.
notfound = set(pat_tuples.keys()) - matched_pats
if raise_unmatched and notfound:
raise apx.InventoryException(notfound=notfound)
@_LockedCancelable()
def info(self, fmri_strings, local, info_needed, ranked=False,
repos=None):
"""Gathers information about fmris. fmri_strings is a list
of fmri_names for which information is desired. local
determines whether to retrieve the information locally
(if possible). It returns a dictionary of lists. The keys
for the dictionary are the constants specified in the class
definition. The values are lists of PackageInfo objects or
strings.
'ranked' is an optional boolean value that indicates whether
only the matching package versions from the highest-ranked
publisher should be returned. This option is ignored for
patterns that explicitly specify the publisher to match.
'repos' is a list of URI strings or RepositoryURI objects that
represent the locations of packages to return information for.
"""
bad_opts = info_needed - PackageInfo.ALL_OPTIONS
if bad_opts:
raise apx.UnrecognizedOptionsToInfo(bad_opts)
self.log_operation_start("info")
# Common logic for image and temp repos case.
if local:
ilist = self.LIST_INSTALLED
else:
# Verify validity of certificates before attempting
# network operations.
self.__cert_verify(log_op_end=[apx.CertificateError])
ilist = self.LIST_NEWEST
# The pkg_pub_map is only populated when temp repos are
# specified and maps packages to the repositories that
# contain them for manifest retrieval.
pkg_pub_map = None
known_cat = None
inst_cat = None
if repos:
pkg_pub_map, ignored, known_cat, inst_cat = \
self.__get_alt_pkg_data(repos)
if local:
pkg_cat = inst_cat
else:
pkg_cat = known_cat
elif local:
pkg_cat = self._img.get_catalog(
self._img.IMG_CATALOG_INSTALLED)
if not fmri_strings and pkg_cat.package_count == 0:
self.log_operation_end(
result=RESULT_NOTHING_TO_DO)
raise apx.NoPackagesInstalledException()
else:
pkg_cat = self._img.get_catalog(
self._img.IMG_CATALOG_KNOWN)
excludes = self._img.list_excludes()
# Set of options that can use catalog data.
cat_opts = frozenset([PackageInfo.DESCRIPTION,
PackageInfo.DEPENDENCIES])
# Set of options that require manifest retrieval.
act_opts = PackageInfo.ACTION_OPTIONS - \
frozenset([PackageInfo.DEPENDENCIES])
collect_attrs = PackageInfo.ALL_ATTRIBUTES in info_needed
pis = []
rval = {
self.INFO_FOUND: pis,
self.INFO_MISSING: misc.EmptyI,
self.INFO_ILLEGALS: misc.EmptyI,
}
try:
for pfmri, summary, cats, states, attrs in self.__get_pkg_list(
ilist, collect_attrs=collect_attrs,
inst_cat=inst_cat, known_cat=known_cat,
patterns=fmri_strings, raise_unmatched=True,
ranked=ranked, return_fmris=True, variants=True):
release = build_release = branch = \
packaging_date = None
pub, name, version = pfmri.tuple()
alt_pub = None
if pkg_pub_map:
alt_pub = \
pkg_pub_map[pub][name][str(version)]
if PackageInfo.IDENTITY in info_needed:
release = version.release
build_release = version.build_release
branch = version.branch
packaging_date = \
version.get_timestamp().strftime(
"%c")
else:
pub = name = version = None
links = hardlinks = files = dirs = \
csize = size = licenses = cat_info = \
description = None
if PackageInfo.CATEGORIES in info_needed:
cat_info = [
PackageCategory(scheme, cat)
for scheme, cat in cats
]
ret_cat_data = cat_opts & info_needed
dependencies = None
unsupported = False
if ret_cat_data:
try:
ignored, description, ignored, \
dependencies = \
_get_pkg_cat_data(pkg_cat,
ret_cat_data,
excludes=excludes,
pfmri=pfmri)
except apx.InvalidPackageErrors:
# If the information can't be
# retrieved because the manifest
# can't be parsed, mark it and
# continue.
unsupported = True
if dependencies is None:
dependencies = misc.EmptyI
mfst = None
if not unsupported and \
(frozenset([PackageInfo.SIZE,
PackageInfo.LICENSES]) | act_opts) & \
info_needed:
try:
mfst = self._img.get_manifest(
pfmri, alt_pub=alt_pub)
except apx.InvalidPackageErrors:
# If the information can't be
# retrieved because the manifest
# can't be parsed, mark it and
# continue.
unsupported = True
if mfst is not None:
if PackageInfo.LICENSES in info_needed:
licenses = self.__licenses(pfmri,
mfst, alt_pub=alt_pub)
if PackageInfo.SIZE in info_needed:
size, csize = mfst.get_size(
excludes=excludes)
if act_opts & info_needed:
if PackageInfo.LINKS in info_needed:
links = list(
mfst.gen_key_attribute_value_by_type(
"link", excludes))
if PackageInfo.HARDLINKS in info_needed:
hardlinks = list(
mfst.gen_key_attribute_value_by_type(
"hardlink", excludes))
if PackageInfo.FILES in info_needed:
files = list(
mfst.gen_key_attribute_value_by_type(
"file", excludes))
if PackageInfo.DIRS in info_needed:
dirs = list(
mfst.gen_key_attribute_value_by_type(
"dir", excludes))
elif PackageInfo.SIZE in info_needed:
size = csize = 0
# Trim response set.
if PackageInfo.STATE in info_needed:
if unsupported is True and \
PackageInfo.UNSUPPORTED not in states:
# Mark package as
# unsupported so that
# caller can decide
# what to do.
states = set(states)
states.add(
PackageInfo.UNSUPPORTED)
else:
states = misc.EmptyI
if PackageInfo.CATEGORIES not in info_needed:
cats = None
if PackageInfo.SUMMARY in info_needed:
if summary is None:
summary = ""
else:
summary = None
pis.append(PackageInfo(pkg_stem=name,
summary=summary, category_info_list=cat_info,
states=states, publisher=pub, version=release,
build_release=build_release, branch=branch,
packaging_date=packaging_date, size=size,
csize=csize, pfmri=pfmri, licenses=licenses,
links=links, hardlinks=hardlinks, files=files,
dirs=dirs, dependencies=dependencies,
description=description, attrs=attrs))
except apx.InventoryException, e:
if e.illegal:
self.log_operation_end(
result=RESULT_FAILED_BAD_REQUEST)
rval[self.INFO_MISSING] = e.notfound
rval[self.INFO_ILLEGALS] = e.illegal
else:
if pis:
self.log_operation_end()
else:
self.log_operation_end(
result=RESULT_NOTHING_TO_DO)
return rval
def can_be_canceled(self):
"""Returns true if the API is in a cancelable state."""
return self.__can_be_canceled
def _disable_cancel(self):
"""Sets_can_be_canceled to False in a way that prevents missed
wakeups. This may raise CanceledException, if a
cancellation is pending."""
self.__cancel_lock.acquire()
if self.__canceling:
self.__cancel_lock.release()
self._img.transport.reset()
raise apx.CanceledException()
else:
self.__set_can_be_canceled(False)
self.__cancel_lock.release()
def _enable_cancel(self):
"""Sets can_be_canceled to True while grabbing the cancel
locks. The caller must still hold the activity lock while
calling this function."""
self.__cancel_lock.acquire()
self.__set_can_be_canceled(True)
self.__cancel_lock.release()
def __set_can_be_canceled(self, status):
"""Private method. Handles the details of changing the
cancelable state."""
assert self.__cancel_lock._is_owned()
# If caller requests a change to current state there is
# nothing to do.
if self.__can_be_canceled == status:
return
if status == True:
# Callers must hold activity lock for operations
# that they will make cancelable.
assert self._activity_lock._is_owned()
# In any situation where the caller holds the activity
# lock and wants to set cancelable to true, a cancel
# should not already be in progress. This is because
# it should not be possible to invoke cancel until
# this routine has finished. Assert that we're not
# canceling.
assert not self.__canceling
self.__can_be_canceled = status
if self.__cancel_state_callable:
self.__cancel_state_callable(self.__can_be_canceled)
def reset(self):
"""Resets the API back the the initial state. Note:
this does not necessarily return the disk to its initial state
since the indexes or download cache may have been changed by
the prepare method."""
self._acquire_activity_lock()
self.__reset_unlock()
self._activity_lock.release()
def __reset_unlock(self):
"""Private method. Provides a way to reset without taking the
activity lock. Should only be called by a thread which already
holds the activity lock."""
assert self._activity_lock._is_owned()
# This needs to be done first so that find_root can use it.
self.__progresstracker.reset()
# Ensure alternate sources are always cleared in an
# exception scenario.
self.__set_img_alt_sources(None)
self.__alt_sources = {}
self._img.cleanup_downloads()
self._img.transport.shutdown()
# Recreate the image object using the path the api
# object was created with instead of the current path.
self._img = image.Image(self._img_path,
progtrack=self.__progresstracker,
user_provided_dir=True,
cmdpath=self.cmdpath)
self._img.blocking_locks = self.__blocking_locks
lin = None
if self._img.linked.ischild():
lin = self._img.linked.child_name
self.__progresstracker.set_linked_name(lin)
self.__plan_desc = None
self.__planned_children = False
self.__plan_type = None
self.__prepared = False
self.__executed = False
self.__be_name = None
self._cancel_cleanup_exception()
def __check_cancel(self):
"""Private method. Provides a callback method for internal
code to use to determine whether the current action has been
canceled."""
return self.__canceling
def _cancel_cleanup_exception(self):
"""A private method that is called from exception handlers.
This is not needed if the method calls reset unlock,
which will call this method too. This catches the case
where a caller might have called cancel and gone to sleep,
but the requested operation failed with an exception before
it could raise a CanceledException."""
self.__cancel_lock.acquire()
self.__set_can_be_canceled(False)
self.__canceling = False
# Wake up any threads that are waiting on this aborted
# operation.
self.__cancel_cv.notify_all()
self.__cancel_lock.release()
def _cancel_done(self):
"""A private method that wakes any threads that have been
sleeping, waiting for a cancellation to finish."""
self.__cancel_lock.acquire()
if self.__canceling:
self.__canceling = False
self.__cancel_cv.notify_all()
self.__cancel_lock.release()
def cancel(self):
"""Used for asynchronous cancelation. It returns the API
to the state it was in prior to the current method being
invoked. Canceling during a plan phase returns the API to
its initial state. Canceling during prepare puts the API
into the state it was in just after planning had completed.
Plan execution cannot be canceled. A call to this method blocks
until the cancellation has happened. Note: this does not
necessarily return the disk to its initial state since the
indexes or download cache may have been changed by the
prepare method."""
self.__cancel_lock.acquire()
if not self.__can_be_canceled:
self.__cancel_lock.release()
return False
self.__set_can_be_canceled(False)
self.__canceling = True
# Wait until the cancelled operation wakes us up.
self.__cancel_cv.wait()
self.__cancel_lock.release()
return True
def clear_history(self):
"""Discard history information about in-progress operations."""
self._img.history.clear()
def __set_history_PlanCreationException(self, e):
if e.unmatched_fmris or e.multiple_matches or \
e.missing_matches or e.illegal:
self.log_operation_end(error=e,
result=RESULT_FAILED_BAD_REQUEST)
else:
self.log_operation_end(error=e)
@_LockedGenerator()
def local_search(self, query_lst):
"""local_search takes a list of Query objects and performs
each query against the installed packages of the image."""
l = query_p.QueryLexer()
l.build()
qp = query_p.QueryParser(l)
ssu = None
for i, q in enumerate(query_lst):
try:
query = qp.parse(q.text)
query_rr = qp.parse(q.text)
if query_rr.remove_root(self._img.root):
query.add_or(query_rr)
if q.return_type == \
query_p.Query.RETURN_PACKAGES:
query.propagate_pkg_return()
except query_p.BooleanQueryException, e:
raise apx.BooleanQueryException(e)
except query_p.ParseError, e:
raise apx.ParseError(e)
self._img.update_index_dir()
assert self._img.index_dir
try:
query.set_info(num_to_return=q.num_to_return,
start_point=q.start_point,
index_dir=self._img.index_dir,
get_manifest_path=\
self._img.get_manifest_path,
gen_installed_pkg_names=\
self._img.gen_installed_pkg_names,
case_sensitive=q.case_sensitive)
res = query.search(
self._img.gen_installed_pkgs,
self._img.get_manifest_path,
self._img.list_excludes())
except search_errors.InconsistentIndexException, e:
raise apx.InconsistentIndexException(e)
# i is being inserted to track which query the results
# are for. None is being inserted since there is no
# publisher being searched against.
try:
for r in res:
yield i, None, r
except apx.SlowSearchUsed, e:
ssu = e
if ssu:
raise ssu
@staticmethod
def __parse_v_0(line, pub, v):
"""This function parses the string returned by a version 0
search server and puts it into the expected format of
(query_number, publisher, (version, return_type, (results))).
"query_number" in the return value is fixed at 0 since search
v0 servers cannot accept multiple queries in a single HTTP
request."""
line = line.strip()
fields = line.split(None, 3)
return (0, pub, (v, Query.RETURN_ACTIONS, (fields[:4])))
@staticmethod
def __parse_v_1(line, pub, v):
"""This function parses the string returned by a version 1
search server and puts it into the expected format of
(query_number, publisher, (version, return_type, (results)))
If it receives a line it can't parse, it raises a
ServerReturnError."""
fields = line.split(None, 2)
if len(fields) != 3:
raise apx.ServerReturnError(line)
try:
return_type = int(fields[1])
query_num = int(fields[0])
except ValueError:
raise apx.ServerReturnError(line)
if return_type == Query.RETURN_ACTIONS:
subfields = fields[2].split(None, 2)
pfmri = fmri.PkgFmri(subfields[0])
return pfmri, (query_num, pub, (v, return_type,
(pfmri, urllib.unquote(subfields[1]),
subfields[2])))
elif return_type == Query.RETURN_PACKAGES:
pfmri = fmri.PkgFmri(fields[2])
return pfmri, (query_num, pub, (v, return_type, pfmri))
else:
raise apx.ServerReturnError(line)
@_LockedGenerator()
def remote_search(self, query_str_and_args_lst, servers=None,
prune_versions=True):
"""This function takes a list of Query objects, and optionally
a list of servers to search against. It performs each query
against each server and yields the results in turn. If no
servers are provided, the search is conducted against all
active servers known by the image.
The servers argument is a list of servers in two possible
forms: the old deprecated form of a publisher, in a
dictionary, or a Publisher object.
A call to this function returns a generator that holds
API locks. Callers must either iterate through all of the
results, or call close() on the resulting object. Otherwise
it is possible to get deadlocks or NRLock reentrance
exceptions."""
failed = []
invalid = []
unsupported = []
if not servers:
servers = self._img.gen_publishers()
new_qs = []
l = query_p.QueryLexer()
l.build()
qp = query_p.QueryParser(l)
for q in query_str_and_args_lst:
try:
query = qp.parse(q.text)
query_rr = qp.parse(q.text)
if query_rr.remove_root(self._img.root):
query.add_or(query_rr)
if q.return_type == \
query_p.Query.RETURN_PACKAGES:
query.propagate_pkg_return()
new_qs.append(query_p.Query(str(query),
q.case_sensitive, q.return_type,
q.num_to_return, q.start_point))
except query_p.BooleanQueryException, e:
raise apx.BooleanQueryException(e)
except query_p.ParseError, e:
raise apx.ParseError(e)
query_str_and_args_lst = new_qs
incorp_info, inst_stems = self.get_incorp_info()
slist = []
for entry in servers:
if isinstance(entry, dict):
origin = entry["origin"]
try:
pub = self._img.get_publisher(
origin=origin)
pub_uri = publisher.RepositoryURI(
origin)
repo = publisher.Repository(
origins=[pub_uri])
except apx.UnknownPublisher:
pub = publisher.RepositoryURI(origin)
repo = publisher.Repository(
origins=[pub])
slist.append((pub, repo, origin))
continue
# Must be a publisher object.
osets = entry.get_origin_sets()
if not osets:
unsupported.append((entry.prefix,
apx.NoPublisherRepositories(
entry.prefix)))
continue
for repo in osets:
slist.append((entry, repo, entry.prefix))
for pub, alt_repo, descriptive_name in slist:
if self.__canceling:
raise apx.CanceledException()
try:
res = self._img.transport.do_search(pub,
query_str_and_args_lst,
ccancel=self.__check_cancel,
alt_repo=alt_repo)
except apx.CanceledException:
raise
except apx.NegativeSearchResult:
continue
except (apx.InvalidDepotResponseException,
apx.TransportError), e:
# Alternate source failed portal test or can't
# be contacted at all.
failed.append((descriptive_name, e))
continue
except apx.UnsupportedSearchError, e:
unsupported.append((descriptive_name, e))
continue
except apx.MalformedSearchRequest, e:
ex = self._validate_search(
query_str_and_args_lst)
if ex:
raise ex
failed.append((descriptive_name, e))
continue
try:
if not self.validate_response(res, 1):
invalid.append(descriptive_name)
continue
for line in res:
pfmri, ret = self.__parse_v_1(line, pub,
1)
pstem = pfmri.pkg_name
pver = pfmri.version
# Skip this package if a newer version
# is already installed and version
# pruning is enabled.
if prune_versions and \
pstem in inst_stems and \
pver < inst_stems[pstem]:
continue
# Return this result if version pruning
# is disabled, the package is not
# incorporated, or the version of the
# package matches the incorporation.
if not prune_versions or \
pstem not in incorp_info or \
pfmri.version.is_successor(
incorp_info[pstem],
pkg.version.CONSTRAINT_AUTO):
yield ret
except apx.CanceledException:
raise
except apx.TransportError, e:
failed.append((descriptive_name, e))
continue
if failed or invalid or unsupported:
raise apx.ProblematicSearchServers(failed,
invalid, unsupported)
def get_incorp_info(self):
"""This function returns a mapping of package stems to the
version at which they are incorporated, if they are
incorporated, and the version at which they are installed, if
they are installed."""
# This maps fmris to the version at which they're incorporated.
inc_vers = {}
inst_stems = {}
img_cat = self._img.get_catalog(
self._img.IMG_CATALOG_INSTALLED)
cat_info = frozenset([img_cat.DEPENDENCY])
# The incorporation list should include all installed,
# incorporated packages from all publishers.
for pfmri, actions in img_cat.actions(cat_info):
inst_stems[pfmri.pkg_name] = pfmri.version
for a in actions:
if a.name != "depend" or \
a.attrs["type"] != "incorporate":
continue
# Record incorporated packages.
tgt = fmri.PkgFmri(a.attrs["fmri"])
tver = tgt.version
# incorporates without a version should be
# ignored.
if not tver:
continue
over = inc_vers.get(
tgt.pkg_name, None)
# In case this package has been
# incorporated more than once,
# use the newest version.
if over > tver:
continue
inc_vers[tgt.pkg_name] = tver
return inc_vers, inst_stems
@staticmethod
def __unconvert_return_type(v):
return v == query_p.Query.RETURN_ACTIONS
def _validate_search(self, query_str_lst):
"""Called by remote search if server responds that the
request was invalid. In this case, parse the query on
the client-side and determine what went wrong."""
for q in query_str_lst:
l = query_p.QueryLexer()
l.build()
qp = query_p.QueryParser(l)
try:
query = qp.parse(q.text)
except query_p.BooleanQueryException, e:
return apx.BooleanQueryException(e)
except query_p.ParseError, e:
return apx.ParseError(e)
return None
def rebuild_search_index(self):
"""Rebuilds the search indexes. Removes all
existing indexes and replaces them from scratch rather than
performing the incremental update which is usually used.
This is useful for times when the index for the client has
been corrupted."""
self._img.update_index_dir()
self.log_operation_start("rebuild-index")
if not os.path.isdir(self._img.index_dir):
self._img.mkdirs()
try:
ind = indexer.Indexer(self._img, self._img.get_manifest,
self._img.get_manifest_path,
self.__progresstracker, self._img.list_excludes())
ind.rebuild_index_from_scratch(
self._img.gen_installed_pkgs())
except search_errors.ProblematicPermissionsIndexException, e:
error = apx.ProblematicPermissionsIndexException(e)
self.log_operation_end(error=error)
raise error
else:
self.log_operation_end()
def get_manifest(self, pfmri, all_variants=True, repos=None):
"""Returns the Manifest object for the given package FMRI.
'all_variants' is an optional boolean value indicating whther
the manifest should include metadata for all variants and
facets.
'repos' is a list of URI strings or RepositoryURI objects that
represent the locations of additional sources of package data to
use during the planned operation.
"""
alt_pub = None
if repos:
pkg_pub_map, ignored, known_cat, inst_cat = \
self.__get_alt_pkg_data(repos)
alt_pub = pkg_pub_map.get(pfmri.publisher, {}).get(
pfmri.pkg_name, {}).get(str(pfmri.version), None)
return self._img.get_manifest(pfmri,
ignore_excludes=all_variants, alt_pub=alt_pub)
@staticmethod
def validate_response(res, v):
"""This function is used to determine whether the first
line returned from a server is expected. This ensures that
search is really communicating with a search-enabled server."""
try:
s = res.next()
return s == Query.VALIDATION_STRING[v]
except StopIteration:
return False
def add_publisher(self, pub, refresh_allowed=True,
approved_cas=misc.EmptyI, revoked_cas=misc.EmptyI,
search_after=None, search_before=None, search_first=None,
unset_cas=misc.EmptyI):
"""Add the provided publisher object to the image
configuration."""
try:
self._img.add_publisher(pub,
refresh_allowed=refresh_allowed,
progtrack=self.__progresstracker,
approved_cas=approved_cas, revoked_cas=revoked_cas,
search_after=search_after,
search_before=search_before,
search_first=search_first,
unset_cas=unset_cas)
finally:
self._img.cleanup_downloads()
def get_highest_ranked_publisher(self):
"""Returns the highest ranked publisher object for the image."""
return self._img.get_highest_ranked_publisher()
def get_publisher(self, prefix=None, alias=None, duplicate=False):
"""Retrieves a publisher object matching the provided prefix
(name) or alias.
'duplicate' is an optional boolean value indicating whether
a copy of the publisher object should be returned instead
of the original.
"""
pub = self._img.get_publisher(prefix=prefix, alias=alias)
if duplicate:
# Never return the original so that changes to the
# retrieved object are not reflected until
# update_publisher is called.
return copy.copy(pub)
return pub
@_LockedCancelable()
def get_publisherdata(self, pub=None, repo=None):
"""Attempts to retrieve publisher configuration information from
the specified publisher's repository or the provided repository.
If successful, it will either return an empty list (in the case
that the repository supports the operation, but doesn't offer
configuration information) or a list of Publisher objects.
If this operation is not supported by the publisher or the
specified repository, an UnsupportedRepositoryOperation
exception will be raised.
'pub' is an optional Publisher object.
'repo' is an optional RepositoryURI object.
Either 'pub' or 'repo' must be provided."""
assert (pub or repo) and not (pub and repo)
# Transport accepts either type of object, but a distinction is
# made in the client API for clarity.
pub = max(pub, repo)
return self._img.transport.get_publisherdata(pub,
ccancel=self.__check_cancel)
def get_publishers(self, duplicate=False):
"""Returns a list of the publisher objects for the current
image.
'duplicate' is an optional boolean value indicating whether
copies of the publisher objects should be returned instead
of the originals.
"""
res = self._img.get_sorted_publishers()
if duplicate:
return [copy.copy(p) for p in res]
return res
def get_publisher_last_update_time(self, prefix=None, alias=None):
"""Returns a datetime object representing the last time the
catalog for a publisher was modified or None."""
if alias:
pub = self.get_publisher(alias=alias)
else:
pub = self.get_publisher(prefix=prefix)
if pub.disabled:
return None
dt = None
self._acquire_activity_lock()
try:
self._enable_cancel()
try:
dt = pub.catalog.last_modified
except:
self.__reset_unlock()
raise
try:
self._disable_cancel()
except apx.CanceledException:
self._cancel_done()
raise
finally:
self._activity_lock.release()
return dt
def has_publisher(self, prefix=None, alias=None):
"""Returns a boolean value indicating whether a publisher using
the given prefix or alias exists."""
return self._img.has_publisher(prefix=prefix, alias=alias)
def remove_publisher(self, prefix=None, alias=None):
"""Removes a publisher object matching the provided prefix
(name) or alias."""
self._img.remove_publisher(prefix=prefix, alias=alias,
progtrack=self.__progresstracker)
def update_publisher(self, pub, refresh_allowed=True, search_after=None,
search_before=None, search_first=None):
"""Replaces an existing publisher object with the provided one
using the _source_object_id identifier set during copy.
'refresh_allowed' is an optional boolean value indicating
whether a refresh of publisher metadata (such as its catalog)
should be performed if transport information is changed for a
repository, mirror, or origin. If False, no attempt will be
made to retrieve publisher metadata."""
self._acquire_activity_lock()
try:
self._disable_cancel()
with self._img.locked_op("update-publisher"):
return self.__update_publisher(pub,
refresh_allowed=refresh_allowed,
search_after=search_after,
search_before=search_before,
search_first=search_first)
except apx.CanceledException, e:
self._cancel_done()
raise
finally:
self._img.cleanup_downloads()
self._activity_lock.release()
def __update_publisher(self, pub, refresh_allowed=True,
search_after=None, search_before=None, search_first=None):
"""Private publisher update method; caller responsible for
locking."""
assert (not search_after and not search_before) or \
(not search_after and not search_first) or \
(not search_before and not search_first)
# Before continuing, validate SSL information.
try:
self._img.check_cert_validity(pubs=[pub])
except apx.ExpiringCertificate, e:
logger.error(str(e))
def origins_changed(oldr, newr):
old_origins = set([
(o.uri, o.ssl_cert,
o.ssl_key, tuple(sorted(o.proxies)))
for o in oldr.origins
])
new_origins = set([
(o.uri, o.ssl_cert,
o.ssl_key, tuple(sorted(o.proxies)))
for o in newr.origins
])
return (new_origins - old_origins), \
new_origins.symmetric_difference(old_origins)
def need_refresh(oldo, newo):
if newo.disabled:
# The publisher is disabled, so no refresh
# should be performed.
return False
if oldo.disabled and not newo.disabled:
# The publisher has been re-enabled, so
# retrieve the catalog.
return True
oldr = oldo.repository
newr = newo.repository
if newr._source_object_id != id(oldr):
# Selected repository has changed.
return True
# If any were added or removed, refresh.
return len(origins_changed(oldr, newr)[1]) != 0
refresh_catalog = False
disable = False
orig_pub = None
# Configuration must be manipulated directly.
publishers = self._img.cfg.publishers
# First, attempt to match the updated publisher object to an
# existing one using the object id that was stored during
# copy().
for key, old in publishers.iteritems():
if pub._source_object_id == id(old):
# Store the new publisher's id and the old
# publisher object so it can be restored if the
# update operation fails.
orig_pub = (id(pub), old)
break
if not orig_pub:
# If a matching publisher couldn't be found and
# replaced, something is wrong (client api usage
# error).
raise apx.UnknownPublisher(pub)
# Next, be certain that the publisher's prefix and alias
# are not already in use by another publisher.
for key, old in publishers.iteritems():
if pub._source_object_id == id(old):
# Don't check the object we're replacing.
continue
if pub.prefix == old.prefix or \
pub.prefix == old.alias or \
pub.alias and (pub.alias == old.alias or
pub.alias == old.prefix):
raise apx.DuplicatePublisher(pub)
# Next, determine what needs updating and add the updated
# publisher.
for key, old in publishers.iteritems():
if pub._source_object_id == id(old):
old = orig_pub[-1]
if need_refresh(old, pub):
refresh_catalog = True
if not old.disabled and pub.disabled:
disable = True
# Now remove the old publisher object using the
# iterator key if the prefix has changed.
if key != pub.prefix:
del publishers[key]
# Prepare the new publisher object.
pub.meta_root = \
self._img._get_publisher_meta_root(
pub.prefix)
pub.transport = self._img.transport
# Finally, add the new publisher object.
publishers[pub.prefix] = pub
break
def cleanup():
new_id, old_pub = orig_pub
for new_pfx, new_pub in publishers.iteritems():
if id(new_pub) == new_id:
publishers[old_pub.prefix] = old_pub
break
repo = pub.repository
validate = origins_changed(orig_pub[-1].repository,
pub.repository)[0]
try:
if disable or (not repo.origins and
orig_pub[-1].repository.origins):
# Remove the publisher's metadata (such as
# catalogs, etc.). This only needs to be done
# in the event that a publisher is disabled or
# has transitioned from having origins to not
# having any at all; in any other case (the
# origins changing, etc.), refresh() will do the
# right thing.
self._img.remove_publisher_metadata(pub)
elif not pub.disabled and not refresh_catalog:
refresh_catalog = pub.needs_refresh
if refresh_catalog and refresh_allowed:
# One of the publisher's repository origins may
# have changed, so the publisher needs to be
# revalidated.
if validate:
self._img.transport.valid_publisher_test(
pub)
# Validate all new origins against publisher
# configuration.
for uri, ssl_cert, ssl_key, proxies in validate:
repo = publisher.RepositoryURI(uri,
ssl_cert=ssl_cert, ssl_key=ssl_key,
proxies=proxies)
pub.validate_config(repo)
self.__refresh(pubs=[pub], immediate=True)
elif refresh_catalog:
# Something has changed (such as a repository
# origin) for the publisher, so a refresh should
# occur, but isn't currently allowed. As such,
# clear the last_refreshed time so that the next
# time the client checks to see if a refresh is
# needed and is allowed, one will be performed.
pub.last_refreshed = None
except Exception, e:
# If any of the above fails, the original publisher
# information needs to be restored so that state is
# consistent.
cleanup()
raise
except:
# If any of the above fails, the original publisher
# information needs to be restored so that state is
# consistent.
cleanup()
raise
if search_first:
self._img.set_highest_ranked_publisher(
prefix=pub.prefix)
elif search_before:
self._img.pub_search_before(pub.prefix, search_before)
elif search_after:
self._img.pub_search_after(pub.prefix, search_after)
# Successful; so save configuration.
self._img.save_config()
def log_operation_end(self, error=None, result=None,
release_notes=None):
"""Marks the end of an operation to be recorded in image
history.
'result' should be a pkg.client.history constant value
representing the outcome of an operation. If not provided,
and 'error' is provided, the final result of the operation will
be based on the class of 'error' and 'error' will be recorded
for the current operation. If 'result' and 'error' is not
provided, success is assumed."""
self._img.history.log_operation_end(error=error, result=result,
release_notes=release_notes)
def log_operation_error(self, error):
"""Adds an error to the list of errors to be recorded in image
history for the current opreation."""
self._img.history.log_operation_error(error)
def log_operation_start(self, name):
"""Marks the start of an operation to be recorded in image
history."""
# If an operation is going to be discarded, then don't take the
# performance hit of actually getting the BE info.
if name in history.DISCARDED_OPERATIONS:
be_name, be_uuid = None, None
else:
be_name, be_uuid = bootenv.BootEnv.get_be_name(
self._img.root)
self._img.history.log_operation_start(name,
be_name=be_name, be_uuid=be_uuid)
def parse_liname(self, name, unknown_ok=False):
"""Parse a linked image name string and return a
LinkedImageName object. If "unknown_ok" is true then
liname must correspond to an existing linked image. If
"unknown_ok" is false and liname doesn't correspond to
an existing linked image then liname must be a
syntactically valid and fully qualified linked image
name."""
return self._img.linked.parse_name(name, unknown_ok=unknown_ok)
def parse_p5i(self, data=None, fileobj=None, location=None):
"""Reads the pkg(5) publisher JSON formatted data at 'location'
or from the provided file-like object 'fileobj' and returns a
list of tuples of the format (publisher object, pkg_names).
pkg_names is a list of strings representing package names or
FMRIs. If any pkg_names not specific to a publisher were
provided, the last tuple returned will be of the format (None,
pkg_names).
'data' is an optional string containing the p5i data.
'fileobj' is an optional file-like object that must support a
'read' method for retrieving data.
'location' is an optional string value that should either start
with a leading slash and be pathname of a file or a URI string.
If it is a URI string, supported protocol schemes are 'file',
'ftp', 'http', and 'https'.
'data' or 'fileobj' or 'location' must be provided."""
return p5i.parse(data=data, fileobj=fileobj, location=location)
def parse_fmri_patterns(self, patterns):
"""A generator function that yields a list of tuples of the form
(pattern, error, fmri, matcher) based on the provided patterns,
where 'error' is any exception encountered while parsing the
pattern, 'fmri' is the resulting FMRI object, and 'matcher' is
one of the following constant values:
MATCH_EXACT
Indicates that the name portion of the pattern
must match exactly and the version (if provided)
must be considered a successor or equal to the
target FMRI.
MATCH_FMRI
Indicates that the name portion of the pattern
must be a proper subset and the version (if
provided) must be considered a successor or
equal to the target FMRI.
MATCH_GLOB
Indicates that the name portion of the pattern
uses fnmatch rules for pattern matching (shell-
style wildcards) and that the version can either
match exactly, match partially, or contain
wildcards.
"""
for pat in patterns:
error = None
matcher = None
npat = None
try:
parts = pat.split("@", 1)
pat_stem = parts[0]
pat_ver = None
if len(parts) > 1:
pat_ver = parts[1]
if "*" in pat_stem or "?" in pat_stem:
matcher = self.MATCH_GLOB
elif pat_stem.startswith("pkg:/") or \
pat_stem.startswith("/"):
matcher = self.MATCH_EXACT
else:
matcher = self.MATCH_FMRI
if matcher == self.MATCH_GLOB:
npat = fmri.MatchingPkgFmri(pat_stem)
else:
npat = fmri.PkgFmri(pat_stem)
if not pat_ver:
# Do nothing.
pass
elif "*" in pat_ver or "?" in pat_ver or \
pat_ver == "latest":
npat.version = \
pkg.version.MatchingVersion(pat_ver)
else:
npat.version = \
pkg.version.Version(pat_ver)
except (fmri.FmriError, pkg.version.VersionError), e:
# Whatever the error was, return it.
error = e
yield (pat, error, npat, matcher)
def purge_history(self):
"""Deletes all client history."""
be_name, be_uuid = bootenv.BootEnv.get_be_name(self._img.root)
self._img.history.purge(be_name=be_name, be_uuid=be_uuid)
def update_format(self):
"""Attempt to update the on-disk format of the image to the
newest version. Returns a boolean indicating whether any action
was taken."""
self._acquire_activity_lock()
try:
self._disable_cancel()
self._img.allow_ondisk_upgrade = True
return self._img.update_format(
progtrack=self.__progresstracker)
except apx.CanceledException, e:
self._cancel_done()
raise
finally:
self._activity_lock.release()
def write_p5i(self, fileobj, pkg_names=None, pubs=None):
"""Writes the publisher, repository, and provided package names
to the provided file-like object 'fileobj' in JSON p5i format.
'fileobj' is only required to have a 'write' method that accepts
data to be written as a parameter.
'pkg_names' is a dict of lists, tuples, or sets indexed by
publisher prefix that contain package names, FMRI strings, or
package info objects. A prefix of "" can be used for packages
that are not specific to a publisher.
'pubs' is an optional list of publisher prefixes or Publisher
objects. If not provided, the information for all publishers
(excluding those disabled) will be output."""
if not pubs:
plist = [
p for p in self.get_publishers()
if not p.disabled
]
else:
plist = []
for p in pubs:
if not isinstance(p, publisher.Publisher):
plist.append(self._img.get_publisher(
prefix=p, alias=p))
else:
plist.append(p)
# Transform PackageInfo object entries into PkgFmri entries
# before passing them to the p5i module.
new_pkg_names = {}
for pub in pkg_names:
pkglist = []
for p in pkg_names[pub]:
if isinstance(p, PackageInfo):
pkglist.append(p.fmri)
else:
pkglist.append(p)
new_pkg_names[pub] = pkglist
p5i.write(fileobj, plist, pkg_names=new_pkg_names)
def write_syspub(self, path, prefixes, version):
"""Write the syspub/version response to the provided path."""
if version != 0:
raise apx.UnsupportedP5SVersion(version)
pubs = [
p for p in self.get_publishers()
if p.prefix in prefixes
]
fd, fp = tempfile.mkstemp()
try:
fh = os.fdopen(fd, "wb")
p5s.write(fh, pubs, self._img.cfg)
fh.close()
portable.rename(fp, path)
except:
if os.path.exists(fp):
portable.remove(fp)
raise
class Query(query_p.Query):
"""This class is the object used to pass queries into the api functions.
It encapsulates the possible options available for a query as well as
the text of the query itself."""
def __init__(self, text, case_sensitive, return_actions=True,
num_to_return=None, start_point=None):
if return_actions:
return_type = query_p.Query.RETURN_ACTIONS
else:
return_type = query_p.Query.RETURN_PACKAGES
try:
query_p.Query.__init__(self, text, case_sensitive,
return_type, num_to_return, start_point)
except query_p.QueryLengthExceeded, e:
raise apx.ParseError(e)
def get_default_image_root(orig_cwd=None):
"""Returns a tuple of (root, exact_match) where 'root' is the absolute
path of the default image root based on current environment given the
client working directory and platform defaults, and 'exact_match' is a
boolean specifying how the default should be treated by ImageInterface.
Note that the root returned may not actually be the valid root of an
image; it is merely the default location a client should use when
initializing an ImageInterface (e.g. '/' is not a valid image on Solaris
10).
The ImageInterface object will use the root provided as a starting point
to find an image, searching upwards through each parent directory until
'/' is reached based on the value of exact_match.
'orig_cwd' should be the original current working directory at the time
of client startup. This value is assumed to be valid if provided,
although permission and access errors will be gracefully handled.
"""
# If an image location wasn't explicitly specified, check $PKG_IMAGE in
# the environment.
root = os.environ.get("PKG_IMAGE")
exact_match = True
if not root:
if os.environ.get("PKG_FIND_IMAGE") or \
portable.osname != "sunos":
# If no image location was found in the environment,
# then see if user enabled finding image or if current
# platform isn't Solaris. If so, attempt to find the
# image starting with the working directory.
root = orig_cwd
if root:
exact_match = False
if not root:
# If no image directory has been determined based on
# request or environment, default to live root.
root = misc.liveroot()
return root, exact_match
def image_create(pkg_client_name, version_id, root, imgtype, is_zone,
cancel_state_callable=None, facets=misc.EmptyDict, force=False,
mirrors=misc.EmptyI, origins=misc.EmptyI, prefix=None, refresh_allowed=True,
repo_uri=None, ssl_cert=None, ssl_key=None, user_provided_dir=False,
progtrack=None, variants=misc.EmptyDict, props=misc.EmptyDict,
cmdpath=None):
"""Creates an image at the specified location.
'pkg_client_name' is a string containing the name of the client,
such as "pkg".
'version_id' indicates the version of the api the client is
expecting to use.
'root' is the absolute path of the directory where the image will
be created. If it does not exist, it will be created.
'imgtype' is an IMG_TYPE constant representing the type of image
to create.
'is_zone' is a boolean value indicating whether the image being
created is for a zone.
'cancel_state_callable' is an optional function reference that will
be called if the cancellable status of an operation changes.
'facets' is a dictionary of facet names and values to set during
the image creation process.
'force' is an optional boolean value indicating that if an image
already exists at the specified 'root' that it should be overwritten.
'mirrors' is an optional list of URI strings that should be added to
all publishers configured during image creation as mirrors.
'origins' is an optional list of URI strings that should be added to
all publishers configured during image creation as origins.
'prefix' is an optional publisher prefix to configure as a publisher
for the new image if origins is provided, or to restrict which publisher
will be configured if 'repo_uri' is provided. If this prefix does not
match the publisher configuration retrieved from the repository, an
UnknownRepositoryPublishers exception will be raised. If not provided,
'refresh_allowed' cannot be False.
'props' is an optional dictionary mapping image property names to values
to be set while creating the image.
'refresh_allowed' is an optional boolean value indicating whether
publisher configuration data and metadata can be retrieved during
image creation. If False, 'repo_uri' cannot be specified and
a 'prefix' must be provided.
'repo_uri' is an optional URI string of a package repository to
retrieve publisher configuration information from. If the target
repository supports this, all publishers found will be added to the
image and any origins or mirrors will be added to all of those
publishers. If the target repository does not support this, and a
prefix was not specified, an UnsupportedRepositoryOperation exception
will be raised. If the target repository supports the operation, but
does not provide complete configuration information, a
RepoPubConfigUnavailable exception will be raised.
'ssl_cert' is an optional pathname of an SSL Certificate file to
configure all publishers with and to use when retrieving publisher
configuration information. If provided, 'ssl_key' must also be
provided. The certificate file must be pem-encoded.
'ssl_key' is an optional pathname of an SSL Key file to configure all
publishers with and to use when retrieving publisher configuration
information. If provided, 'ssl_cert' must also be provided. The
key file must be pem-encoded.
'user_provided_dir' is an optional boolean value indicating that the
provided 'root' was user-supplied and that additional error handling
should be enforced. This primarily affects cases where a relative
root has been provided or the root was based on the current working
directory.
'progtrack' is an optional ProgressTracker object.
'variants' is a dictionary of variant names and values to set during
the image creation process.
Callers must provide one of the following when calling this function:
* no 'prefix' and no 'origins'
* a 'prefix' and 'repo_uri' (origins and mirrors are optional)
* no 'prefix' and a 'repo_uri' (origins and mirrors are optional)
* a 'prefix' and 'origins'
"""
# Caller must provide a prefix and repository, or no prefix and a
# repository, or a prefix and origins, or no prefix and no origins.
assert (prefix and repo_uri) or (not prefix and repo_uri) or (prefix and
origins or (not prefix and not origins))
# If prefix isn't provided and refresh isn't allowed, then auto-config
# cannot be done.
assert (prefix or refresh_allowed) or not repo_uri
destroy_root = False
try:
destroy_root = not os.path.exists(root)
except EnvironmentError, e:
if e.errno == errno.EACCES:
raise apx.PermissionsException(
e.filename)
raise
# The image object must be created first since transport may be
# needed to retrieve publisher configuration information.
img = image.Image(root, force=force, imgtype=imgtype,
progtrack=progtrack, should_exist=False,
user_provided_dir=user_provided_dir, cmdpath=cmdpath,
props=props)
api_inst = ImageInterface(img, version_id,
progtrack, cancel_state_callable, pkg_client_name,
cmdpath=cmdpath)
pubs = []
try:
if repo_uri:
# Assume auto configuration.
if ssl_cert:
misc.validate_ssl_cert(ssl_cert, prefix=prefix,
uri=repo_uri)
repo = publisher.RepositoryURI(repo_uri,
ssl_cert=ssl_cert, ssl_key=ssl_key)
pubs = None
try:
pubs = api_inst.get_publisherdata(repo=repo)
except apx.UnsupportedRepositoryOperation:
if not prefix:
raise apx.RepoPubConfigUnavailable(
location=repo_uri)
# For a v0 repo where a prefix was specified,
# fallback to manual configuration.
if not origins:
origins = [repo_uri]
repo_uri = None
if not prefix and not pubs:
# Empty repository configuration.
raise apx.RepoPubConfigUnavailable(
location=repo_uri)
if repo_uri:
for p in pubs:
psrepo = p.repository
if not psrepo:
# Repository configuration info
# was not provided, so assume
# origin is repo_uri.
p.repository = \
publisher.Repository(
origins=[repo_uri])
elif not psrepo.origins:
# Repository configuration was
# provided, but without an
# origin. Assume the repo_uri
# is the origin.
psrepo.add_origin(repo_uri)
elif repo not in psrepo.origins:
# If the repo_uri used is not
# in the list of sources, then
# add it as the first origin.
psrepo.origins.insert(0, repo)
if prefix and not repo_uri:
# Auto-configuration not possible or not requested.
if ssl_cert:
misc.validate_ssl_cert(ssl_cert, prefix=prefix,
uri=origins[0])
repo = publisher.Repository()
for o in origins:
repo.add_origin(o) # pylint: disable=E1103
for m in mirrors:
repo.add_mirror(m) # pylint: disable=E1103
pub = publisher.Publisher(prefix,
repository=repo)
pubs = [pub]
if prefix and prefix not in pubs:
# If publisher prefix requested isn't found in the list
# of publishers at this point, then configuration isn't
# possible.
known = [p.prefix for p in pubs]
raise apx.UnknownRepositoryPublishers(
known=known, unknown=[prefix], location=repo_uri)
elif prefix:
# Filter out any publishers that weren't requested.
pubs = [
p for p in pubs
if p.prefix == prefix
]
# Add additional origins and mirrors that weren't found in the
# publisher configuration if provided.
for p in pubs:
pr = p.repository
for o in origins:
if not pr.has_origin(o):
pr.add_origin(o)
for m in mirrors:
if not pr.has_mirror(m):
pr.add_mirror(m)
# Set provided SSL Cert/Key for all configured publishers.
for p in pubs:
repo = p.repository
for o in repo.origins:
if o.scheme not in publisher.SSL_SCHEMES:
continue
o.ssl_cert = ssl_cert
o.ssl_key = ssl_key
for m in repo.mirrors:
if m.scheme not in publisher.SSL_SCHEMES:
continue
m.ssl_cert = ssl_cert
m.ssl_key = ssl_key
img.create(pubs, facets=facets, is_zone=is_zone,
progtrack=progtrack, refresh_allowed=refresh_allowed,
variants=variants, props=props)
except EnvironmentError, e:
if e.errno == errno.EACCES:
raise apx.PermissionsException(
e.filename)
if e.errno == errno.EROFS:
raise apx.ReadOnlyFileSystemException(
e.filename)
raise
except:
# Ensure a defunct image isn't left behind.
img.destroy()
if destroy_root and \
os.path.abspath(root) != "/" and \
os.path.exists(root):
# Root didn't exist before create and isn't '/',
# so remove it.
shutil.rmtree(root, True)
raise
img.cleanup_downloads()
return api_inst