plandesc.py revision 3366
#
# 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
# 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
#
#
#
"""
PlanDescription and _ActionPlan classes
These classes are part of the public API, and any changes here may require
bumping CURRENT_API_VERSION in pkg.api
The PlanDescription class is a public interface which contains all the data
associated with an image-modifying operation.
The _ActionPlan class is a private interface used to keep track of actions
modified within an image during an image-modifying operation.
"""
import collections
import itertools
import operator
import simplejson as json
import six
"""A named tuple used to keep track of all the actions that will be
executed during an image-modifying procecure."""
# Class has no __init__ method; pylint: disable=W0232
# Use __slots__ on an old style class; pylint: disable=E1001
__slots__ = []
__state__desc = tuple([
])
"""Returns the serialized state of this object in a format
that that can be easily stored using JSON, pickle, etc."""
"""Allocate a new object using previously serialized state
obtained via getstate()."""
# Access to protected member; pylint: disable=W0212
# get the name of the object we're dealing with
# decode serialized state into python objects
return _ActionPlan(*state)
class PlanDescription(object):
"""A class which describes the changes the plan will make."""
__state__desc = {
"_cfg_mediators": {
str: {
}
},
# avoid, implicit-avoid, obsolete
str: {
}
}),
"_preserved": {
"removed": [[str]],
"installed": [[str]],
"updated": [[str]],
},
# Messaging looks like:
# {"item_id": {"sub_item_id": [], "messages": []}}
"install_actions": [ _ActionPlan ],
"release_notes": (bool, []),
"removal_actions": [ _ActionPlan ],
"update_actions": [ _ActionPlan ],
}
])
#
# Properties set when state >= EVALUATED_PKGS
#
self._cfg_mediators = {}
self._new_variants = None
self._old_facets = None
self._new_facets = None
# remove (oldfmri, None)
# update (oldfmri, newfmri|oldfmri)
self._preserved = {
"moved": [],
"removed": [],
"installed": [],
"updated": [],
}
self._solver_summary = []
self._solver_errors = None
#
# Properties set when state >= EVALUATED_OK
#
# raw actions
# merged actions
self.removal_actions = []
self.update_actions = []
self.install_actions = []
# smf and other actuators (driver actions get added during
# execution stage).
# Used to track users and groups that are part of operation.
self.added_groups = {}
self.added_users = {}
self.removed_groups = {}
self.removed_users = {}
# release notes that are part of this operation
# plan properties
self._need_boot_archive = None
# child properties
self.child_op_vectors = []
self.children_ignored = None
self.children_planned = []
self.children_nop = []
# driver aliases to remove
self._rm_aliases = {}
#
# Properties set when state >= EXECUTED_OK
#
self.release_notes_name = None
#
# Set by imageplan.set_be_options()
#
self._backup_be = None
self._backup_be_name = None
# Accessed via imageplan.update_index
# stats about the current image
# Pkg actuators
self._pkg_actuators = {}
"""Returns the serialized state of this object in a format
that that can be easily stored using JSON, pickle, etc."""
# Access to protected member; pylint: disable=W0212
if reset_volatiles:
# backup and clear volatiles
# add a state version encoding identifier
if reset_volatiles:
return state
"""Update the state of this object using previously serialized
state obtained via getstate()."""
# Access to protected member; pylint: disable=W0212
# get the name of the object we're dealing with
# version check and delete the encoding identifier
# decode serialized state into python objects
# bulk update
# clear volatiles
"""Allocate a new object using previously serialized state
obtained via getstate()."""
rv = PlanDescription()
return rv
"""Save a json encoded representation of this plan
description objects into the specified file object."""
try:
except OSError as e:
# Access to protected member; pylint: disable=W0212
raise apx._convert_error(e)
del state
"""Load a json encoded representation of a plan description
from the specified file object."""
try:
except OSError as e:
# Access to protected member; pylint: disable=W0212
raise apx._convert_error(e)
del state
def _executed_ok(self):
"""A private interface used after a plan is successfully
invoked to free up memory."""
# reduce memory consumption
self._fmri_changes = []
self._preserved = {}
# We have to save the timed_out state.
self.added_groups = {}
self.added_users = {}
self.removed_groups = {}
self.removed_users = {}
"""A boolean indicating if we attempted to execute this
plan."""
"""Returns a list of string tuples describing affected services
(action, SMF FMRI)."""
return sorted(
)
"""Returns a list of three-tuples containing information about
the mediators. The first element in the tuple is the name of
the mediator. The second element is a tuple containing the
original version and source and the new version and source of
the mediator. The third element is a tuple containing the
original implementation and source and new implementation and
source."""
ret = []
if not self._mediators_change or \
return ret
def get_mediation(mediators, m):
# Missing docstring; pylint: disable=C0111
mver_source = None
if m in mediators:
"implementation")
"implementation-version")
if mimpl_ver:
mimpl_ver = \
"implementation-source")
if mver:
"version-source")
self._cfg_mediators, m)
orig_ver_source == new_ver_source and \
# Mediation not changed.
continue
out = (m,
((orig_ver, orig_ver_source),
(new_ver, new_ver_source)),
(new_impl, new_impl_source)))
return ret
def get_mediators(self):
"""Returns list of strings describing mediator changes."""
ret = []
((orig_ver, orig_ver_source),
out += " version: {0} ({1} default)" \
elif orig_ver:
out += " version: {0} ({1} default)" \
elif new_ver:
out += " version: None -> " \
out += " implementation: {0} ({1} default)" \
elif orig_impl:
out += " implementation: {0} ({1} default)" \
elif new_impl:
out += " implementation: None -> " \
return ret
"""Get the proposed fmri changes."""
return self._fmri_changes
"""A list of tuples of items that were salvaged during plan
execution. Each tuple is of the form (original_path,
salvage_path). Where 'original_path' is the path of the item
before it was salvaged, and 'salvage_path' is where the item was
moved to. This property is only valid after plan execution
has completed."""
"""Returns a tuple of two lists containing the facet and
variant changes in this plan.
The variant list contains tuples with the following format:
(<variant>, <new-value>)
The facet list contains tuples with the following format:
(<facet>, <new-value>, <old-value>, <source>,
<new-masked>, <old-masked>)
"""
vs = []
if self._new_variants:
# sort results by variant name
fs = []
if self._new_facets is None:
# create new dictionaries that index facets by name and
# source:
# dict[(<facet, src>)] = (<value>, <masked>)
old_facets = dict([
for f in self._old_facets
# W0212 Access to a protected member
# pylint: disable=W0212
])
new_facets = dict([
for f in self._new_facets
# W0212 Access to a protected member
# pylint: disable=W0212
])
# check for removed facets
# check for added facets
# check for changing facets
continue
# sort results by facet name
def get_varcets(self):
"""Returns a formatted list of strings representing the
rv = [
]
masked_str = _(" (masked)")
return rv
def get_changes(self):
"""A generator function that yields tuples of PackageInfo
objects of the form (src_pi, dest_pi).
If 'src_pi' is None, then 'dest_pi' is the package being
installed.
If 'src_pi' is not None, and 'dest_pi' is None, 'src_pi'
is the package being removed.
If 'src_pi' is not None, and 'dest_pi' is not None,
then 'src_pi' is the original version of the package,
and 'dest_pi' is the new version of the package it is
being upgraded to."""
else:
def get_editable_changes(self):
"""This function returns a tuple of generators that yield tuples
of the form (src, dest) of the preserved ("editable") files that
will be installed, moved, removed, or updated. The returned
list of generators is (moved, removed, installed, updated)."""
return (
((None, entry[0])
)
def get_actions(self):
"""A generator function that yields action change descriptions
in the order they will be performed."""
# Unused variable '%s'; pylint: disable=W0612
# pylint: enable=W0612
def has_release_notes(self):
"""True if there are release notes for this plan"""
def must_display_notes(self):
"""True if the release notes must be displayed"""
def get_release_notes(self):
"""A generator that returns the release notes for this plan"""
yield notes
"""A generator function that yields information about the
licenses related to the current plan in tuples of the form
(dest_fmri, src, dest, accepted, displayed) for the given
package FMRI or all packages in the plan. This is only
available for licenses that are being installed or updated.
'dest_fmri' is the FMRI of the package being installed.
'src' is a LicenseInfo object if the license of the related
package is being updated; otherwise it is None.
'dest' is the LicenseInfo object for the license that is being
installed.
'accepted' is a boolean value indicating that the license has
been marked as accepted for the current plan.
'displayed' is a boolean value indicating that the license has
been marked as displayed for the current plan."""
continue
# Unused variable; pylint: disable=W0612
src_li = None
if src:
dest_li = None
if dest:
if pfmri:
break
def get_solver_errors(self):
"""Returns a list of strings for all FMRIs evaluated by the
solver explaining why they were rejected. (All packages
found in solver's trim database.) Only available if
DebugValues["plan"] was set when the plan was created.
"""
# in case this operation doesn't use solver
if self._solver_errors is None:
return []
return self._solver_errors
api_inst=None):
"""Display the parsable version of the plan."""
assert parsable_version == 0, \
# Set the default values.
added_fmris = []
removed_fmris = []
changed_fmris = []
affected_fmris = []
backup_be_name = None
be_name = None
space_available = None
space_required = None
facets_changed = []
variants_changed = []
services_affected = []
mediators_changed = []
editables_changed = []
licenses = []
if child_images is None:
child_images = []
release_notes = []
if self:
# Lists of lists are used here becuase
# json will convert lists of tuples
# into lists of lists anyway.
else:
elif rem is not None:
else:
# Lists of lists are used here to ensure a consistent
# ordering and because tuples will be converted to
# lists anyway; a dictionary would be more logical for
# the top level entries, but would make testing more
# difficult and this is a small, known set anyway.
if emoved:
if eremoved:
if einstalled:
if eupdated:
for n in self.get_release_notes():
self.get_licenses():
src_tup = ()
if src_li:
dest_tup = ()
if dest_li:
# If api_inst is set, mark licenses as
# displayed.
if api_inst:
# The image name for the parent image is always None. If this
# image is a child image, then the image name will be set when
# the parent image processes this dictionary.
ret = {
"activate-be": be_activated,
"backup-be-name": backup_be_name,
"be-name": be_name,
"boot-archive-rebuild": boot_archive_rebuilt,
"change-editables": editables_changed,
"child-images": child_images,
"create-backup-be": backup_be_created,
"create-new-be": new_be_created,
"image-name": None,
"release-notes": release_notes,
"space-available": space_available,
"space-required": space_required,
"version": parsable_version
}
return ret
"""Return parsable item messages."""
return self._item_msgs
"""Add a new message with its time, type and text for an
item."""
if parent:
else:
sub_item = "messages"
# First level messaging looks like:
# {"item_id": {"messages": [msg_payload ...]}}
# Second level messaging looks like:
# {"item_id": {"sub_item_id": [msg_payload ...]}}.
"msg_level": msg_level,
"msg_type": msg_type,
"msg_text": msg_text}
[]).append(msg_payload)
"""Add new messages to an item."""
if parent:
else:
sub_item = "messages"
def __msg_dict2list(msg):
"""Convert a message dictionary to a list."""
msg["msg_text"]]
"""Return all item messages.
'ordered' is an optional boolean value that indicates that
item messages will be sorted by msg_time. If False, item
messages will be in an arbitrary order."""
if ordered:
ordered_list = []
# To make the first level messages come
# relatively earlier.
]["messages"]:
None] +
if si == "messages":
continue
else:
if si == "messages":
yield (item_id, None,
mp["msg_time"],
mp["msg_level"],
mp["msg_type"],
mp["msg_text"])
continue
mp["msg_time"],
mp["msg_level"],
mp["msg_type"],
mp["msg_text"])
"""Set timeout for synchronous actuators."""
"integer."
"""Add a pkg actuator to the plan. The internal dictionary looks
like this:
{ trigger_pkg: {
exec_op : [ changed pkg, ... ],
...
},
...
}
"""
else:
[cpkg]
else:
def gen_pkg_actuators(self):
"""Pkg actuators which got triggered by operation."""
def actuator_timed_out(self):
"""Indicates that a synchronous actuator timed out."""
return self._act_timed_out
"""Return the type of plan that was created (ex:
API_OP_UPDATE)."""
def update_index(self):
"""Boolean indicating if indexes will be updated as part of an
image-modifying operation."""
return self._update_index
"""Either None, True, or False. If None then executing this
plan may create a backup BE. If False, then executing this
plan will not create a backup BE. If True, then executing
this plan will create a backup BE."""
return self._backup_be
"""The name of a new BE that will be created if this plan is
executed."""
def backup_be_name(self):
"""The name of a new backup BE that will be created if this
plan is executed."""
return self._backup_be_name
def activate_be(self):
"""A boolean value indicating whether any new boot environment
will be set active on next boot."""
return self._be_activate
def reboot_needed(self):
"""A boolean value indicating that execution of the plan will
require a restart of the system to take effect if the target
image is an existing boot environment."""
"""A boolean value indicating that execution of the plan will
take place in a clone of the current live environment"""
def update_boot_archive(self):
"""A boolean value indicating whether or not the boot archive
will be rebuilt"""
return self._need_boot_archive
def bytes_added(self):
"""Estimated number of bytes added"""
return self._bytes_added
def cbytes_added(self):
"""Estimated number of download cache bytes added"""
return self._cbytes_added
def bytes_avail(self):
"""Estimated number of bytes available in image /"""
return self._bytes_avail
def cbytes_avail(self):
"""Estimated number of bytes available in download cache"""
return self._cbytes_avail
def new_facets(self):
"""If facets are changing, this is the new set of facets being
applied."""
if self._new_facets is None:
return None