--- a/doc/client_api_versions.txt Mon Jun 25 07:48:23 2012 +0530
+++ b/doc/client_api_versions.txt Mon Jun 25 09:51:09 2012 -0700
@@ -1,3 +1,11 @@
+Version 74:
+Compatible with clients using version 73, 72.
+ The PlanDescription now has interfaces to
+ determine whether or not release notes were generated
+ for this operation, whether or not they must be displayed,
+ and a method of retrieving the release notes line by
+ line.
+
Version 73:
Compatible with clients using version 72.
--- a/src/client.py Mon Jun 25 07:48:23 2012 +0530
+++ b/src/client.py Mon Jun 25 09:51:09 2012 -0700
@@ -58,6 +58,7 @@
import re
import socket
import sys
+ import tempfile
import textwrap
import time
import traceback
@@ -90,7 +91,7 @@
import sys
sys.exit(1)
-CLIENT_API_VERSION = 73
+CLIENT_API_VERSION = 74
PKG_CLIENT_NAME = "pkg"
JUST_UNKNOWN = 0
@@ -302,7 +303,7 @@
adv_usage["unset-publisher"] = _("publisher ...")
adv_usage["publisher"] = _("[-HPn] [-F format] [publisher ...]")
- adv_usage["history"] = _("[-Hl] [-t [time|time-time],...] [-n number] [-o column,...]")
+ adv_usage["history"] = _("[-HNl] [-t [time|time-time],...] [-n number] [-o column,...]")
adv_usage["purge-history"] = ""
adv_usage["rebuild-index"] = ""
adv_usage["update-format"] = ""
@@ -820,7 +821,7 @@
result = list(res)
if result:
api_inst.progresstracker.verify_start(len(result))
-
+
for entry in result:
pfmri = entry[0]
entries = []
@@ -919,16 +920,20 @@
display_plan_options = ["basic", "fmris", "variants/facets", "services",
"actions", "boot-archive"]
-def __display_plan(api_inst, verbose):
+def __display_plan(api_inst, verbose, noexecute):
"""Helper function to display plan to the desired degree.
Verbose can either be a numerical value, or a list of
items to display"""
if isinstance(verbose, int):
disp = ["basic"]
+
+ if verbose == 0 and noexecute:
+ disp.append("release-notes")
if verbose > 0:
disp.extend(["fmris", "mediators", "services",
- "variants/facets", "boot-archive"])
+ "variants/facets", "boot-archive",
+ "release-notes"])
if verbose > 1:
disp.append("actions")
if verbose > 2:
@@ -941,6 +946,9 @@
plan = api_inst.describe()
+ if plan.must_display_notes():
+ disp.append("release-notes")
+
# If we're a recursive invocation (indicated by client_output_progfd),
# we want to elide messages related to BE management.
recursive_child = \
@@ -1112,6 +1120,35 @@
for a in plan.get_actions():
logger.info(" %s" % a)
+
+ if plan.has_release_notes():
+ if "release-notes" in disp:
+ logger.info("Release Notes:")
+ for a in plan.get_release_notes():
+ logger.info(" %s", a)
+ else:
+ if not plan.new_be:
+ logger.info(_("Release notes can be viewed with 'pkg history -n 1 -N'"))
+ else:
+ tmp_path = __write_tmp_release_notes(plan)
+ if tmp_path:
+ logger.info(_("Release notes can be found in %s before rebooting.")
+ % tmp_path)
+ logger.info(_("After rebooting, use 'pkg history -n 1 -N' to view release notes."))
+
+def __write_tmp_release_notes(plan):
+ """write release notes out to a file in /tmp and return the name"""
+ if plan.has_release_notes:
+ try:
+ fd, path = tempfile.mkstemp(suffix=".txt", prefix="release-notes")
+ tmpfile = os.fdopen(fd, "w+b")
+ for a in plan.get_release_notes():
+ tmpfile.write(a)
+ tmpfile.close()
+ return path
+ except Exception:
+ pass
+
def __display_parsable_plan(api_inst, parsable_version, child_images=None):
"""Display the parsable version of the plan."""
@@ -1138,6 +1175,7 @@
licenses = []
if child_images is None:
child_images = []
+ release_notes = []
if plan:
for rem, add in plan.get_changes():
@@ -1166,6 +1204,10 @@
space_required = plan.bytes_added
services_affected = plan.services
mediators_changed = plan.mediators
+
+ for n in plan.get_release_notes():
+ release_notes.append(n)
+
for dfmri, src_li, dest_li, acc, disp in \
plan.get_licenses():
src_tup = None
@@ -1199,6 +1241,7 @@
"change-variants": sorted(variants_changed),
"affect-services": sorted(services_affected),
"change-mediators": sorted(mediators_changed),
+ "release-notes": release_notes,
"image-name": None,
"child-images": child_images,
"version": parsable_version,
@@ -1264,7 +1307,7 @@
display_plan_licenses(api_inst, show_all=show_licenses)
if not quiet:
- __display_plan(api_inst, verbose)
+ __display_plan(api_inst, verbose, noexecute)
if parsable_version is not None:
__display_parsable_plan(api_inst, parsable_version,
child_image_plans)
@@ -1468,7 +1511,7 @@
if e_type == api_errors.ConflictingActionErrors:
error("\n" + str(e), cmd=op)
if verbose:
- __display_plan(api_inst, verbose)
+ __display_plan(api_inst, verbose, noexecute)
return EXIT_OOPS
if e_type in (api_errors.InvalidPlanError,
api_errors.ReadOnlyFileSystemException,
@@ -2798,7 +2841,7 @@
if not stuff_to_do:
if verbose:
- __display_plan(api_inst, verbose)
+ __display_plan(api_inst, verbose, noexecute)
if parsable_version is not None:
try:
__display_parsable_plan(api_inst,
@@ -2811,7 +2854,7 @@
return EXIT_NOP
if not quiet:
- __display_plan(api_inst, verbose)
+ __display_plan(api_inst, verbose, noexecute)
if parsable_version is not None:
try:
__display_parsable_plan(api_inst, parsable_version)
@@ -2873,7 +2916,7 @@
if not stuff_to_do:
if verbose:
- __display_plan(api_inst, verbose)
+ __display_plan(api_inst, verbose, noexecute)
if parsable_version is not None:
try:
__display_parsable_plan(api_inst,
@@ -2886,7 +2929,7 @@
return EXIT_NOP
if not quiet:
- __display_plan(api_inst, verbose)
+ __display_plan(api_inst, verbose, noexecute)
if parsable_version is not None:
try:
__display_parsable_plan(api_inst, parsable_version)
@@ -5818,7 +5861,7 @@
"""Display history about the current image.
"""
# define column name, header, field width and <History> attribute name
- # we compute 'reason' and 'time' columns ourselves
+ # we compute 'reason', 'time' and 'release_note' columns ourselves
history_cols = {
"be": (_("BE"), "%-20s", "operation_be"),
"be_uuid": (_("BE UUID"), "%-41s", "operation_be_uuid"),
@@ -5832,6 +5875,7 @@
"operation": (_("OPERATION"), "%-25s", "operation_name"),
"outcome": (_("OUTCOME"), "%-12s", "operation_result"),
"reason": (_("REASON"), "%-10s", None),
+ "release_notes": (_("RELEASE NOTES"), "%-12s", None),
"snapshot": (_("SNAPSHOT"), "%-20s", "operation_snapshot"),
"start": (_("START"), "%-25s", "operation_start_time"),
"time": (_("TIME"), "%-10s", None),
@@ -5843,14 +5887,17 @@
omit_headers = False
long_format = False
column_format = False
+ show_notes = False
display_limit = None # Infinite
time_vals = [] # list of timestamps for which we want history events
columns = ["start", "operation", "client", "outcome"]
- opts, pargs = getopt.getopt(args, "Hln:o:t:")
+ opts, pargs = getopt.getopt(args, "HNln:o:t:")
for opt, arg in opts:
if opt == "-H":
omit_headers = True
+ elif opt == "-N":
+ show_notes = True
elif opt == "-l":
long_format = True
elif opt == "-n":
@@ -5909,9 +5956,15 @@
if time_vals and display_limit:
usage(_("-n and -t may not be combined"), cmd="history")
+ if column_format and show_notes:
+ usage(_("-o and -N may not be combined"), cmd="history")
+
+ if long_format and show_notes:
+ usage(_("-l and -N may not be combined"), cmd="history")
+
history_fmt = None
- if not long_format:
+ if not long_format and not show_notes:
headers = []
# build our format string
for col in columns:
@@ -5939,6 +5992,21 @@
error(str(e), cmd="history")
sys.exit(EXIT_OOPS)
+ if show_notes:
+ for he in gen_entries():
+ start_time = misc.timestamp_to_time(
+ he.operation_start_time)
+ start_time = datetime.datetime.fromtimestamp(
+ start_time).isoformat()
+ if he.operation_release_notes:
+ msg(_("%s: Release notes:") % start_time)
+ for a in he.notes:
+ msg(" %s" % a)
+ else:
+ msg(_("%s: Release notes: None") % start_time)
+
+ return EXIT_OK
+
for he in gen_entries():
# populate a dictionary containing our output
output = {}
@@ -5998,6 +6066,11 @@
else:
output["new_be"] = "%s" % he.operation_new_be
+ if he.operation_release_notes:
+ output["release_notes"] = _("Yes")
+ else:
+ output["release_notes"] = _("No")
+
outcome, reason = he.operation_result_text
output["outcome"] = outcome
output["reason"] = reason
@@ -6088,6 +6161,7 @@
data.append((_("End Time"), hist_info["finish"]))
data.append((_("Total Time"), hist_info["time"]))
data.append((_("Command"), hist_info["command"]))
+ data.append((_("Release Notes"), hist_info["release_notes"]))
state = he.operation_start_state
if state:
--- a/src/modules/client/actuator.py Mon Jun 25 07:48:23 2012 +0530
+++ b/src/modules/client/actuator.py Mon Jun 25 09:51:09 2012 -0700
@@ -25,6 +25,7 @@
#
import pkg.smf as smf
+import pkg.actions
import os
import pkg.misc
@@ -32,67 +33,19 @@
from pkg.client.debugvalues import DebugValues
from pkg.client.imagetypes import IMG_USER, IMG_ENTIRE
-class GenericActuator(object):
+
+class Actuator(object):
"""Actuators are action attributes that cause side effects
on live images when those actions are updated, installed
or removed. Since no side effects are caused when the
affected image isn't the current root image, the OS may
need to cause the equivalent effect during boot.
- """
-
- actuator_attrs = set()
-
- def __init__(self):
- self.install = {}
- self.removal = {}
- self.update = {}
-
- def __nonzero__(self):
- return bool(self.install or self.removal or self.update)
-
- def scan_install(self, attrs):
- self.__scan(self.install, attrs)
-
- def scan_removal(self, attrs):
- self.__scan(self.removal, attrs)
-
- def scan_update(self, attrs):
- self.__scan(self.update, attrs)
-
- def __scan(self, dictionary, attrs):
- for a in set(attrs.keys()) & self.actuator_attrs:
- values = attrs[a]
-
- if not isinstance(values, list):
- values = [values]
-
- dictionary.setdefault(a, set()).update(values)
-
- def reboot_needed(self):
- return False
-
- def exec_prep(self, image):
- pass
-
- def exec_pre_actuators(self, image):
- pass
-
- def exec_post_actuators(self, image):
- pass
-
- def exec_fail_actuators(self, image):
- pass
-
- def __str__(self):
- return "Removals: %s\nInstalls: %s\nUpdates: %s\n" % \
- (self.removal, self.install, self.update)
-
-
-class Actuator(GenericActuator):
- """Solaris specific Actuator implementation..."""
+ This is Solaris specific for now. """
actuator_attrs = set([
"reboot-needed", # have to reboot to update this file
+ "release-note", # conditionally include this file
+ # in release notes
"refresh_fmri", # refresh this service on any change
"restart_fmri", # restart this service on any change
"suspend_fmri", # suspend this service during update
@@ -104,6 +57,7 @@
"disable_fmri": set(),
"reboot-needed": set(),
"refresh_fmri": set(),
+ "release-note": [(pkg.actions.generic.NSG, pkg.fmri.PkgFmri)],
"restart_fmri": set(),
"suspend_fmri": set(),
},
@@ -111,6 +65,7 @@
"disable_fmri": set(),
"reboot-needed": set(),
"refresh_fmri": set(),
+ "release-note": [(pkg.actions.generic.NSG, pkg.fmri.PkgFmri)],
"restart_fmri": set(),
"suspend_fmri": set(),
},
@@ -118,13 +73,16 @@
"disable_fmri": set(),
"reboot-needed": set(),
"refresh_fmri": set(),
+ "release-note": [(pkg.actions.generic.NSG, pkg.fmri.PkgFmri)],
"restart_fmri": set(),
"suspend_fmri": set(),
},
}
def __init__(self):
- GenericActuator.__init__(self)
+ self.install = {}
+ self.removal = {}
+ self.update = {}
self.suspend_fmris = None
self.tmp_suspend_fmris = None
self.do_nothing = True
@@ -163,12 +121,40 @@
def __bool__(self):
return self.install or self.removal or self.update
+ def __nonzero__(self):
+ return bool(self.install or self.removal or self.update)
+
+ # scan_* functions take ActionPlan arguments (see imageplan.py)
+ def scan_install(self, ap):
+ self.__scan(self.install, ap.dst, ap.p.destination_fmri)
+
+ def scan_removal(self, ap):
+ self.__scan(self.removal, ap.src, ap.p.origin_fmri)
+
+ def scan_update(self, ap):
+ if ap.src:
+ self.__scan(self.update, ap.src, ap.p.destination_fmri)
+ self.__scan(self.update, ap.dst, ap.p.destination_fmri)
+
+ def __scan(self, dictionary, act, fmri):
+ attrs = act.attrs
+ for a in set(attrs.keys()) & self.actuator_attrs:
+ if a != "release-note":
+ values = attrs[a]
+ if not isinstance(values, list):
+ values = [values]
+ dictionary.setdefault(a, set()).update(values)
+ else:
+ if act.name == "file": # ignore for non-files
+ dictionary.setdefault(a, list()).append(
+ (act, fmri))
+
def get_list(self):
"""Returns a list of actuator value pairs, suitable for printing"""
def check_val(dfmri):
# For actuators which are a single, global function that
# needs to get executed, simply print true.
- if callable(dfmri):
+ if callable(dfmri) or isinstance(dfmri, list):
return [ "true" ]
else:
return dfmri
@@ -188,6 +174,11 @@
for smf in merge[fmri]
]
+ def get_release_note_info(self):
+ """Returns a list of tuples of possible release notes"""
+ return self.update.get("release-note", []) + \
+ self.install.get("release-note", [])
+
def get_services_list(self):
"""Returns a list of services that would be restarted"""
return [(fmri, smf) for fmri, smf in self.get_list()
@@ -210,7 +201,7 @@
return bool("true" in self.update.get("reboot-needed", [])) or \
bool("true" in self.removal.get("reboot-needed", []))
- def exec_prep(self, image):
+ def exec_prep(self, image):
if not image.is_liveroot():
# we're doing off-line pkg ops; we need
# to support self-assembly milestone
--- a/src/modules/client/api.py Mon Jun 25 07:48:23 2012 +0530
+++ b/src/modules/client/api.py Mon Jun 25 09:51:09 2012 -0700
@@ -103,8 +103,8 @@
# things like help(pkg.client.api.PlanDescription)
from pkg.client.plandesc import PlanDescription # pylint: disable-msg=W0611
-CURRENT_API_VERSION = 73
-COMPATIBLE_API_VERSIONS = frozenset([72, CURRENT_API_VERSION])
+CURRENT_API_VERSION = 74
+COMPATIBLE_API_VERSIONS = frozenset([72, 73, CURRENT_API_VERSION])
CURRENT_P5I_VERSION = 1
# Image type constants.
@@ -2393,7 +2393,8 @@
# by one of the previous operations, then log it as
# ending now.
if self._img.history.operation_name:
- self.log_operation_end()
+ 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,
@@ -4745,7 +4746,8 @@
# Successful; so save configuration.
self._img.save_config()
- def log_operation_end(self, error=None, result=None):
+ def log_operation_end(self, error=None, result=None,
+ release_notes=None):
"""Marks the end of an operation to be recorded in image
history.
@@ -4755,7 +4757,8 @@
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)
+ 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
--- a/src/modules/client/history.py Mon Jun 25 07:48:23 2012 +0530
+++ b/src/modules/client/history.py Mon Jun 25 09:51:09 2012 -0700
@@ -144,7 +144,8 @@
for attr in ("name", "start_time", "end_time", "start_state",
"end_state", "username", "userid", "be", "be_exists",
"be_uuid", "current_be", "current_new_be", "new_be",
- "new_be_exists", "new_be_uuid", "result", "snapshot"):
+ "new_be_exists", "new_be_uuid", "result", "release_notes",
+ "snapshot"):
setattr(h, attr, getattr(self, attr))
h.errors = [copy.copy(e) for e in self.errors]
return h
@@ -177,12 +178,13 @@
Operation New Boot Env. Current: %s
Operation New Boot Env. UUID: %s
Operation Snapshot: %s
+Operation Release Notes: %s
Operation Errors:
%s
""" % (self.name, self.result, self.start_time, self.end_time,
self.start_state, self.end_state, self.username, self.userid,
self.be, self.current_be, self.be_uuid, self.new_be, self.current_new_be,
- self.new_be_uuid, self.snapshot, self.errors)
+ self.new_be_uuid, self.snapshot, self.release_notes, self.errors)
# All "time" values should be in UTC, using ISO 8601 as the format.
# Name of the operation performed (e.g. install, update, etc.).
@@ -214,6 +216,8 @@
# The uuid of the boot environment that was created as a result of the
# operation
new_be_uuid = None
+ # The name of the file containing the release notes, or None.
+ release_notes = None
# The snapshot that was created while running this operation
# set to None if no snapshot was taken, or destroyed after successful
@@ -278,6 +282,7 @@
operation_snapshot = None
operation_errors = None
operation_result = None
+ operation_release_notes = None
def __copy__(self):
h = History()
@@ -425,6 +430,23 @@
"%s-01.xml" % ops[-1]["operation"].start_time)
return pathname
+ @property
+ def notes(self):
+ """Generates the lines of release notes for this operation.
+ If no release notes are present, no output occurs."""
+
+ if not self.operation_release_notes:
+ return
+ try:
+ rpath = os.path.join(self.root_dir,
+ "notes",
+ self.operation_release_notes)
+ for a in file(rpath, "r"):
+ yield a.rstrip()
+
+ except Exception, e:
+ raise apx.HistoryLoadException(e)
+
def clear(self):
"""Discards all information related to the current history
object.
@@ -490,6 +512,8 @@
if op.new_be_uuid:
op.current_new_be = uuid_be_dic.get(
op.new_be_uuid, op.new_be)
+ if node.hasAttribute("release-notes"):
+ op.release_notes = node.getAttribute("release-notes")
def get_node_values(parent_name, child_name=None):
try:
@@ -611,6 +635,8 @@
self.operation_new_be_uuid)
if self.operation_snapshot:
op.setAttribute("snapshot", self.operation_snapshot)
+ if self.operation_release_notes:
+ op.setAttribute("release-notes", self.operation_release_notes)
root.appendChild(op)
@@ -755,7 +781,7 @@
self.operation_be = be_name
self.operation_be_uuid = be_uuid
- def log_operation_end(self, error=None, result=None):
+ def log_operation_end(self, error=None, result=None, release_notes=None):
"""Marks the end of an operation to be recorded in image
history.
@@ -789,6 +815,8 @@
elif not result:
# Assume success if no error and no result.
result = RESULT_SUCCEEDED
+ if release_notes:
+ self.operation_release_notes = release_notes
self.operation_result = result
def log_operation_error(self, error):
--- a/src/modules/client/imageplan.py Mon Jun 25 07:48:23 2012 +0530
+++ b/src/modules/client/imageplan.py Mon Jun 25 09:51:09 2012 -0700
@@ -33,6 +33,7 @@
import os
import simplejson as json
import sys
+import tempfile
import traceback
import weakref
@@ -1969,6 +1970,7 @@
self.evaluate_pkg_plans()
self.merge_actions()
+ self.compile_release_notes()
for p in self.pd.pkg_plans:
cpbytes, pbytes = p.get_bytes_added()
@@ -2016,6 +2018,7 @@
def __update_avail_space(self):
"""Update amount of available space on FS"""
+
self.pd._cbytes_avail = misc.spaceavail(
self.image.write_cache_path)
@@ -2024,6 +2027,93 @@
if self.pd._cbytes_avail < 0:
self.pd._cbytes_avail = self.pd._bytes_avail
+ def __include_note(self, installed_dict, act, containing_fmri):
+ """Decide if a release note should be shown/included. If
+ feature/pkg/self is fmri, fmri is containing package;
+ if version is then 0, this is note is displayed on initial
+ install only. Otherwise, if version earlier than specified
+ fmri is present in code, display release note."""
+
+ for fmristr in act.attrlist("release-note"):
+ try:
+ pfmri = pkg.fmri.PkgFmri(fmristr, "5.11")
+ except pkg.fmri.FmriError:
+ continue # skip malformed fmris
+ # any special handling here?
+ if pfmri.pkg_name == "feature/pkg/self":
+ if str(pfmri.version) == "0,5.11" \
+ and containing_fmri.pkg_name \
+ not in installed_dict:
+ return True
+ else:
+ pfmri.pkg_name = \
+ containing_fmri.pkg_name
+ if pfmri.pkg_name not in installed_dict:
+ continue
+ installed_fmri = installed_dict[pfmri.pkg_name]
+ # if neither is successor they are equal
+ if pfmri.is_successor(installed_fmri):
+ return True
+ return False
+
+ def __get_note_text(self, act, pfmri):
+ """Retrieve text for release note from repo"""
+ try:
+ pub = self.image.get_publisher(pfmri.publisher)
+ return self.image.transport.get_content(pub, act.hash,
+ fmri=pfmri)
+ finally:
+ self.image.cleanup_downloads()
+
+ def compile_release_notes(self):
+ """Figure out what release notes need to be displayed"""
+ release_notes = self.pd._actuators.get_release_note_info()
+ must_display = False
+ notes = []
+
+ def do_decode(s):
+ """convert non-ascii strings to unicode;
+ replace non-convertable chars"""
+ try:
+ # this will fail if any 8 bit chars in string
+ # this is a nop if string is ascii.
+ s = s.encode("ascii")
+ except ValueError:
+ # this will encode 8 bit strings into unicode
+ s = s.decode("utf-8", "replace")
+ return s
+
+ if release_notes:
+ installed_dict = ImagePlan.__fmris2dict(
+ self.image.gen_installed_pkgs())
+ for act, pfmri in release_notes:
+ if self.__include_note(installed_dict, act,
+ pfmri):
+ if act.attrs.get("must-display",
+ "false") == "true":
+ must_display = True
+ for l in self.__get_note_text(
+ act, pfmri).splitlines():
+ notes.append(do_decode(l))
+
+ self.pd.release_notes = (must_display, notes)
+
+ def save_release_notes(self):
+ """Save a copy of the release notes and store the file name"""
+ if self.pd.release_notes[1]:
+ # create a file in imgdir/notes
+ dpath = os.path.join(self.image.imgdir, "notes")
+ misc.makedirs(dpath)
+ fd, path = tempfile.mkstemp(suffix=".txt",
+ dir=dpath, prefix="release-notes-")
+ tmpfile = os.fdopen(fd, "wb")
+ for note in self.pd.release_notes[1]:
+ if isinstance(note, unicode):
+ note = note.encode("utf-8")
+ print >>tmpfile, note
+ tmpfile.close()
+ self.pd.release_notes_name = os.path.basename(path)
+
def evaluate_pkg_plans(self):
"""Internal helper function that does the work of converting
fmri changes into pkg plans."""
@@ -2693,7 +2783,7 @@
fname = None
attrs = re = None
- self.pd._actuators.scan_removal(ap.src.attrs)
+ self.pd._actuators.scan_removal(ap)
if self.pd._need_boot_archive is None:
if ap.src.attrs.get("path", "").startswith(
ramdisk_prefixes):
@@ -2791,7 +2881,7 @@
pp_needs_trimming.add(ap.p)
nkv = index = ra = None
- self.pd._actuators.scan_install(ap.dst.attrs)
+ self.pd._actuators.scan_install(ap)
if self.pd._need_boot_archive is None:
if ap.dst.attrs.get("path", "").startswith(
ramdisk_prefixes):
@@ -2894,9 +2984,7 @@
# scan both old and new actions
# repairs may result in update action w/o orig action
- if a[1]:
- self.pd._actuators.scan_update(a[1].attrs)
- self.pd._actuators.scan_update(a[2].attrs)
+ self.pd._actuators.scan_update(a)
if self.pd._need_boot_archive is None:
if a[2].attrs.get("path", "").startswith(
ramdisk_prefixes):
@@ -3332,6 +3420,7 @@
self.pd._actuators.exec_post_actuators(self.image)
self.image._create_fast_lookups(progtrack=self.__progtrack)
+ self.save_release_notes()
# success
self.pd.state = plandesc.EXECUTED_OK
--- a/src/modules/client/plandesc.py Mon Jun 25 07:48:23 2012 +0530
+++ b/src/modules/client/plandesc.py Mon Jun 25 09:51:09 2012 -0700
@@ -133,6 +133,7 @@
"li_ppkgs": frozenset([ pkg.fmri.PkgFmri ]),
"li_props": { li.PROP_NAME: li.LinkedImageName },
"pkg_plans": [ pkg.client.pkgplan.PkgPlan ],
+ "release_notes": (bool, []),
"removal_actions": [ _ActionPlan ],
"removed_groups": { str: pkg.fmri.PkgFmri },
"removed_users": { str: pkg.fmri.PkgFmri },
@@ -189,6 +190,8 @@
self.added_users = {}
self.removed_groups = {}
self.removed_users = {}
+ # release notes that are part of this operation
+ self.release_notes = (False, [])
# plan properties
self._cbytes_added = 0 # size of compressed files
self._bytes_added = 0 # size of files added
@@ -206,6 +209,7 @@
# Properties set when state >= EXECUTED_OK
#
self._salvaged = []
+ self.release_notes_name = None
#
# Set by imageplan.set_be_options()
@@ -514,6 +518,19 @@
# pylint: enable-msg=W0612
yield "%s -> %s" % (o_act, d_act)
+ def has_release_notes(self):
+ """True if there are release notes for this plan"""
+ return bool(self.release_notes[1])
+
+ def must_display_notes(self):
+ """True if the release notes must be displayed"""
+ return self.release_notes[0]
+
+ def get_release_notes(self):
+ """A generator that returns the release notes for this plan"""
+ for notes in self.release_notes[1]:
+ yield notes
+
def get_licenses(self, pfmri=None):
"""A generator function that yields information about the
licenses related to the current plan in tuples of the form
--- a/src/modules/misc.py Mon Jun 25 07:48:23 2012 +0530
+++ b/src/modules/misc.py Mon Jun 25 09:51:09 2012 -0700
@@ -1977,6 +1977,7 @@
# we don't need to do anything for basic types
if desc_type in json_types_immediates:
+ rv = None
return jd_return(name, data, desc, finish, jd_state)
# decode elements nested in a dictionary
--- a/src/tests/cli/t_actuators.py Mon Jun 25 07:48:23 2012 +0530
+++ b/src/tests/cli/t_actuators.py Mon Jun 25 09:51:09 2012 -0700
@@ -1,5 +1,5 @@
#!/usr/bin/python
-#
+# -*- coding: utf-8
# CDDL HEADER START
#
# The contents of this file are subject to the terms of the
@@ -30,7 +30,7 @@
import pkg5unittest
import unittest
-class TestPkgActuators(pkg5unittest.SingleDepotTestCase):
+class TestPkgSMFActuators(pkg5unittest.SingleDepotTestCase):
# Only start/stop the depot once (instead of for every test)
persistent_setup = True
@@ -425,5 +425,137 @@
"svc:/system/test_multi_svc2:default")
os.unlink(svcadm_output)
+class TestPkgReleaseNotes(pkg5unittest.SingleDepotTestCase):
+ # Only start/stop the depot once (instead of for every test)
+ persistent_setup = True
+
+ foo10 = """
+ open [email protected],5.11-0
+ add file tmp/release-note-1 mode=0644 owner=root group=bin path=/usr/share/doc/release-notes/release-note-1 release-note=feature/pkg/self@0
+ close """
+
+ foo11 = """
+ open [email protected],5.11-0
+ add file tmp/release-note-2 mode=0644 owner=root group=root path=/usr/share/doc/release-notes/release-note-2 release-note=feature/pkg/[email protected]
+ close """
+
+ foo12 = """
+ open [email protected],5.11-0
+ add file tmp/release-note-3 mode=0644 owner=root group=root path=/usr/share/doc/release-notes/release-note-3 release-note=feature/pkg/[email protected] must-display=true
+ close """
+
+ foo13 = """
+ open [email protected],5.11-0
+ add file tmp/release-note-4 mode=0644 owner=root group=root path=/usr/share/doc/release-notes/release-note-4 release-note=feature/pkg/[email protected]
+ close """
+
+ bar10 = """
+ open [email protected],5.11-0
+ add dir path=/usr mode=0755 owner=root group=root release-note=feature/pkg/self@0
+ close """
+
+ bar11 = """
+ open [email protected],5.11-0
+ close """
+
+ baz10 = """
+ open [email protected],5.11-0
+ add file tmp/release-note-5 mode=0644 owner=root group=root path=/usr/share/doc/release-notes/release-note-5 [email protected]
+ close """
+
+ hovercraft = """
+ open [email protected],5.10-0
+ add file tmp/release-note-6 mode=0644 owner=root group=root path=/usr/share/doc/release-notes/release-note-6 release-note=feature/pkg/self@0
+ close """
+
+ misc_files = {
+ "tmp/release-note-1":"bobcats are fun!",
+ "tmp/release-note-2":"wombats are fun!",
+ "tmp/release-note-3":"no animals were hurt...",
+ "tmp/release-note-4":"no vegetables were hurt...",
+ "tmp/release-note-5":"multi-line release notes\nshould work too,\nwe'll see if they do.",
+ "tmp/release-note-6":u"Eels are best smoked\nМоё судно на воздушной подушке полно угрей\nHovercraft can be smoked, too.\n",
+ }
+
+ def setUp(self):
+ pkg5unittest.SingleDepotTestCase.setUp(self)
+ self.make_misc_files(self.misc_files)
+ self.pkgsend_bulk(self.rurl, self.foo10 + self.foo11 +
+ self.foo12 + self.foo13 + self.bar10 + self.bar11 + self.baz10 +
+ self.hovercraft)
+ self.image_create(self.rurl)
+
+ def test_release_note_1(self):
+ # make sure release note gets printed on original install
+ self.pkg("install -v [email protected]")
+ self.output.index("bobcats are fun!")
+ # check update case
+ self.pkg("update -v [email protected]")
+ self.output.index("wombats are fun!")
+ # check must display case
+ self.pkg("update [email protected]")
+ self.output.index("no animals")
+ # check that no output is seen w/o must-display and -v,
+ # but that user is prompted that notes are available.
+ self.pkg("update [email protected]")
+ assert self.output.find("no vegetables") == -1
+
+ def test_release_note_2(self):
+ self.pkg("uninstall '*'")
+ # check that release notes are printed with just -n
+ self.pkg("install -vn [email protected]")
+ self.output.index("bobcats are fun!")
+ # retrieve release notes with pkg history after actual install
+ self.pkg("install [email protected]")
+ # make sure we note that release notes are available
+ self.output.index("Release notes")
+ # check that we list them in the -l output
+ self.pkg("history -n 1 -l")
+ self.output.index("Release Notes")
+ # retrieve notes and look for felines
+ self.pkg("history -n 1 -N")
+ self.output.index("bobcats are fun!")
+ # check that we say yes that release notes are available
+ self.pkg("history -Hn 1 -o release_notes")
+ self.output.index("Yes")
+
+ def test_release_note_3(self):
+ # check that release notes are printed properly
+ # when needed and dependency is on other pkg
+ self.pkg("uninstall '*'")
+ self.pkg("install [email protected]")
+ self.pkg("install -v [email protected]")
+ self.output.index("multi-line release notes")
+ self.output.index("should work too,")
+ self.output.index("we'll see if they do.")
+ # should not see notes again
+ self.pkg("update -v bar")
+ assert self.output.find("Release notes") == -1
+ self.pkg("uninstall '*'")
+ # no output expected here since [email protected] isn't part of original image.
+ self.pkg("install [email protected] [email protected]")
+ assert self.output.find("multi-line release notes") == -1
+
+ def test_release_note_4(self):
+ # make sure that parseable option works properly
+ self.pkg("uninstall '*'")
+ self.pkg("install [email protected]")
+ self.pkg("install --parsable 0 [email protected]")
+ self.output.index("multi-line release notes")
+ self.output.index("should work too,")
+ self.output.index("we'll see if they do.")
+ self.pkg("uninstall '*'")
+ # test unicode character in files
+ self.pkg("install -n [email protected]")
+ unicode(self.output, "utf-8").index(u"Моё судно на воздушной подушке полно угрей")
+ unicode(self.output, "utf-8").index(u"Eels are best smoked")
+ self.pkg("install -v [email protected]")
+ unicode(self.output, "utf-8").index(u"Моё судно на воздушной подушке полно угрей")
+ unicode(self.output, "utf-8").index(u"Eels are best smoked")
+ self.pkg("uninstall '*'")
+ self.pkg("install --parsable 0 [email protected]")
+ self.pkg("history -n 1 -N")
+ unicode(self.output, "utf-8").index(u"Моё судно на воздушной подушке полно угрей")
+ unicode(self.output, "utf-8").index(u"Eels are best smoked")
if __name__ == "__main__":
unittest.main()
--- a/src/tests/pkg5unittest.py Mon Jun 25 07:48:23 2012 +0530
+++ b/src/tests/pkg5unittest.py Mon Jun 25 09:51:09 2012 -0700
@@ -398,6 +398,8 @@
ins = " [+%d lines...]" % (len(lines) - 1)
else:
ins = ""
+ if isinstance(lines[0], unicode):
+ lines[0] = lines[0].encode("utf-8")
self.debugcmd(
"echo '%s%s' > %s" % (lines[0], ins, path))
@@ -803,7 +805,7 @@
change_facets=EmptyI, change_packages=EmptyI,
change_mediators=EmptyI, change_variants=EmptyI,
child_images=EmptyI, create_backup_be=False, create_new_be=False,
- image_name=None, licenses=EmptyI, remove_packages=EmptyI,
+ image_name=None, licenses=EmptyI, remove_packages=EmptyI, release_notes=EmptyI,
version=0):
"""Check that the parsable output in 'output' is what is
expected."""