--- a/src/man/pkg.5.txt Wed Jun 08 11:19:13 2011 -0700
+++ b/src/man/pkg.5.txt Wed Jun 08 16:46:31 2011 -0700
@@ -135,6 +135,30 @@
above, such as 'strawberry'), then the existing file will
be left alone, and the new file will not be installed.
+ overlay This specifies whether the action allows other packages to
+ deliver a file at the same location or whether it delivers
+ a file intended to overlay another. This functionality is
+ intended for use with configuration files that don't
+ participate in any self-assembly (e.g. /etc/motd) and that
+ can be safely overwritten.
+
+ If overlay is not specified, multiple packages may not deliver
+ files to the same location.
+
+ If the value of overlay is 'allow', one other package is
+ allowed to deliver a file to the same location. This value
+ has no effect unless the preserve attribute is also set.
+
+ If the value of overlay is 'true', the file delivered by the
+ action will overwrite any other action that has specified
+ 'allow'. Changes to the installed file will be preserved
+ based on the value of the preserve attribute of the overlaying
+ file. On removal, the contents of the file will be preserved
+ if the action being overlayed is still installed regardless of
+ whether the preserve attribute was specified. Only one action
+ may overlay another, and the 'mode', 'owner', and 'group'
+ attributes must match.
+
Files may also be 'tasted', and depending on the flavor, may have
additional interesting attributes. For ELF files, the following
attributes are recognized:
--- a/src/modules/actions/file.py Wed Jun 08 11:19:13 2011 -0700
+++ b/src/modules/actions/file.py Wed Jun 08 16:46:31 2011 -0700
@@ -445,7 +445,7 @@
# system expected it to be, then we preserve the original file
# in some way, depending on the value of preserve. If the
# action is an overlay, then we always overwrite.
- overlay = self.attrs.get("overlay", "false") == "true"
+ overlay = self.attrs.get("overlay") == "true"
if is_file and not overlay:
chash, cdata = misc.get_data_digest(final_path)
if not orig or chash != orig.hash:
--- a/src/modules/client/api_errors.py Wed Jun 08 11:19:13 2011 -0700
+++ b/src/modules/client/api_errors.py Wed Jun 08 16:46:31 2011 -0700
@@ -699,7 +699,8 @@
ua = dict(
(k, v)
for k, v in action.attrs.iteritems()
- if k in action.unique_attrs
+ if k in action.unique_attrs and
+ not (k == "preserve" and "overlay" in action.attrs)
)
action.attrs = ua
return action
--- a/src/modules/client/image.py Wed Jun 08 11:19:13 2011 -0700
+++ b/src/modules/client/image.py Wed Jun 08 16:46:31 2011 -0700
@@ -3220,7 +3220,9 @@
continue
for key in act.attrs.keys():
if (act.unique_attrs and
- key not in act.unique_attrs) or \
+ key not in act.unique_attrs and
+ not (act.name == "file" and
+ key == "overlay")) or \
key.startswith("variant.") or \
key.startswith("facet."):
del act.attrs[key]
--- a/src/modules/client/imageplan.py Wed Jun 08 11:19:13 2011 -0700
+++ b/src/modules/client/imageplan.py Wed Jun 08 16:46:31 2011 -0700
@@ -608,20 +608,31 @@
# look through all the packages, looking for our files
# we could use search for this.
- def peelslash(a):
- if os.path.isabs(a):
- return a[1:]
- return a
- revertpaths = set([peelslash(a) for a in args])
+ revertpaths = set([a.lstrip(os.path.sep) for a in args])
+ overlaypaths = set()
for f in self.image.gen_installed_pkgs():
self.__progtrack.evaluate_progress()
m = self.image.get_manifest(f)
for act in m.gen_actions_by_type("file",
self.__new_excludes):
- if act.attrs["path"] in revertpaths:
- revert_dict[(f,m)].append(act)
- revertpaths.remove(
- act.attrs["path"])
+ path = act.attrs["path"]
+ if path in revertpaths or \
+ path in overlaypaths:
+ revert_dict[(f, m)].append(act)
+ if act.attrs.get("overlay") == \
+ "allow":
+ # Action allows overlay,
+ # all matching actions
+ # must be collected.
+ # The imageplan will
+ # automatically handle
+ # the overlaid action
+ # if an overlaying
+ # action is present.
+ overlaypaths.add(path)
+ revertpaths.discard(path)
+
+ revertpaths.difference_update(overlaypaths)
if revertpaths:
raise api_errors.PlanCreationException(
nofiles=list(revertpaths))
@@ -929,18 +940,45 @@
all(o[0] == n[0] for o, n, in zip(oactions, actions)):
return "nothing", None
- # Some very rare cases require us to allow two actions to be
- # delivered at a single point in their namespace. Don't use
- # this unless you know you're supposed to!
- if DebugValues["allow-overlays"] and \
- any(act[0].attrs.get("overlay") == "true" for act in actions):
- return None
+ # For file actions, delivery of two actions to a single point is
+ # permitted if:
+ # * there are only two actions in conflict
+ # * one action has 'preserve' set and 'overlay=allow'
+ # * the other action has 'overlay=true'
+ if len(actions) == 2:
+ overlayable = overlay = None
+ for act, ignored in actions:
+ if (act.name == "file" and
+ act.attrs.get("overlay") == "allow" and
+ "preserve" in act.attrs):
+ overlayable = act
+ elif (act.name == "file" and
+ act.attrs.get("overlay") == "true"):
+ overlay = act
+ if overlayable and overlay:
+ # Found both an overlayable action and the
+ # action that overlays it.
+ errors = self.__find_inconsistent_attrs(actions,
+ ignore=["preserve"])
+ if errors:
+ # overlay is not permitted if unique
+ # attributes (except 'preserve') are
+ # inconsistent
+ return ("error", actions,
+ api_errors.InconsistentActionAttributeError)
+ return "overlay", None
return "error", actions
@staticmethod
- def __find_inconsistent_attrs(actions):
- """Find all the problem action pairs."""
+ def __find_inconsistent_attrs(actions, ignore=misc.EmptyI):
+ """Find all the problem Action pairs.
+
+ 'ignore' is an optional list of attributes to ignore when
+ checking for inconsistent attributes. By default, all
+ attributes listed in the 'unique_attrs' property of an
+ Action are checked.
+ """
# We iterate over all pairs of actions to see if any conflict
# with the rest. If two actions are "safe" together, then we
@@ -969,7 +1007,10 @@
# If none of the different attributes is one that must
# be identical, then we can skip this action.
- if not any(d for d in diffs if d in a1[0].unique_attrs):
+ if not any(
+ d for d in diffs
+ if (d in a1[0].unique_attrs and
+ d not in ignore)):
seen.add(a2)
continue
@@ -1042,22 +1083,57 @@
if ret is None:
return False
- msg, actions = ret
+ if len(ret) == 3:
+ # Allow checking functions to override default errclass.
+ msg, actions, errclass = ret
+ else:
+ msg, actions = ret
if not isinstance(msg, basestring):
return False
if msg == "nothing":
for i, ap in enumerate(self.removal_actions):
- if ap and ap.src.attrs.get(ap.src.key_attr, None) == key:
+ if ap and ap.src.attrs.get(ap.src.key_attr,
+ None) == key:
self.removal_actions[i] = None
+ elif msg == "overlay":
+ pp_needs_trimming = {}
+ for al in (self.install_actions, self.update_actions):
+ for i, ap in enumerate(al):
+ if not (ap and ap.dst.attrs.get(
+ ap.dst.key_attr, None) == key):
+ continue
+ if ap.dst.attrs.get("overlay") == \
+ "allow":
+ # Remove overlaid actions from
+ # plan.
+ al[i] = None
+ pp_needs_trimming.setdefault(id(ap.p),
+ { "plan": ap.p, "trim": [] })
+ pp_needs_trimming[id(ap.p)]["trim"].append(
+ id(ap.dst))
+ break
+
+ for entry in pp_needs_trimming.values():
+ p = entry["plan"]
+ trim = entry["trim"]
+ # Can't modify the p.actions tuple, so modify
+ # the added member in-place.
+ for prop in ("added", "changed"):
+ pval = getattr(p.actions, prop)
+ pval[:] = [
+ a
+ for a in pval
+ if id(a) not in trim
+ ]
elif msg == "fixup":
self.__propose_fixup(*actions)
elif msg == "error":
errs.append(errclass(actions))
else:
assert False, "%s() returned something other than " \
- "'nothing', 'error', or 'fixup': '%s'" % \
+ "'nothing', 'overlay', 'error', or 'fixup': '%s'" % \
(func.__name__, msg)
return True
@@ -1563,7 +1639,6 @@
for i, ap in enumerate(self.removal_actions):
if ap is None:
continue
-
self.__progtrack.evaluate_progress()
# If the action type needs to be reference-counted, make
@@ -1640,6 +1715,8 @@
new_updates = []
for i, ap in enumerate(self.install_actions):
+ if ap is None:
+ continue
self.__progtrack.evaluate_progress()
# In order to handle editable files that move their path
# or change pkgs, for all new files with original_name
@@ -1745,17 +1822,14 @@
del dest_pkgplans, nu_chg
- self.removal_actions = [
- a
- for a in self.removal_actions
- if a is not None
- ]
-
- self.install_actions = [
- a
- for a in self.install_actions
- if a is not None
- ]
+ for prop in ("removal_actions", "install_actions",
+ "update_actions"):
+ pval = getattr(self, prop)
+ pval[:] = [
+ a
+ for a in pval
+ if a is not None
+ ]
self.__progtrack.evaluate_progress()
# Go over update actions
--- a/src/modules/client/linkedimage/common.py Wed Jun 08 11:19:13 2011 -0700
+++ b/src/modules/client/linkedimage/common.py Wed Jun 08 16:46:31 2011 -0700
@@ -2160,7 +2160,6 @@
# propagate certain debug options
for k in [
- "allow-overlays",
"broken-conflicting-action-handling",
"disp_linked_cmds",
"plan"]:
--- a/src/tests/cli/t_pkg_install.py Wed Jun 08 11:19:13 2011 -0700
+++ b/src/tests/cli/t_pkg_install.py Wed Jun 08 16:46:31 2011 -0700
@@ -5947,7 +5947,14 @@
pkg_overlaid = """
open overlaid@0,5.11-0
- add file tmp/file1 path=etc/pam.conf mode=644 owner=root group=sys preserve=true
+ add file tmp/file1 path=etc/pam.conf mode=644 owner=root group=sys preserve=true overlay=allow
+ close
+ """
+
+ # 'overlay' is ignored unless 'preserve' is also set.
+ pkg_invalid_overlaid = """
+ open invalid-overlaid@0,5.11-0
+ add file tmp/file1 path=etc/pam.conf mode=644 owner=root group=sys overlay=allow
close
"""
@@ -5957,6 +5964,34 @@
close
"""
+ pkg_multi_overlayer = """
+ open multi-overlayer@0,5.11-0
+ add file tmp/file2 path=etc/pam.conf mode=644 owner=root group=sys preserve=true overlay=true
+ close
+ """
+
+ # overlaying file is treated as conflicting file if its mode, owner, and
+ # group attributes don't match the action being overlaid
+ pkg_mismatch_overlayer = """
+ open mismatch-overlayer@0,5.11-0
+ add file tmp/file2 path=etc/pam.conf mode=640 owner=root group=bin preserve=true overlay=true
+ close
+ """
+
+ # overlaying file is treated as conflicting file if it doesn't set overlay=true
+ # even if file being overlaid allows overlay.
+ pkg_invalid_overlayer = """
+ open invalid-overlayer@0,5.11-0
+ add file tmp/file2 path=etc/pam.conf mode=644 owner=root group=sys preserve=true
+ close
+ """
+
+ pkg_unpreserved_overlayer = """
+ open unpreserved-overlayer@0,5.11-0
+ add file tmp/file2 path=etc/pam.conf mode=644 owner=root group=sys overlay=true
+ close
+ """
+
pkgremote_pkg1 = """
open pkg1@0,5.11-0
add file tmp/file1 path=remote mode=644 owner=root group=sys
@@ -6535,30 +6570,14 @@
self.pkg("uninstall dupfilesp2 dupotherfilesp2")
self.pkg("verify")
- # Make sure the overlay hack works.
+ # Re-use the overlay packages for some preserve testing.
self.pkg("install overlaid")
- self.pkg("install overlayer", exit=1)
- self.pkg("-D allow-overlays=1 install overlayer")
- # Verify won't work here, on a preserve=true file!
- self.file_contains("etc/pam.conf", "file2")
- self.pkg("uninstall overlayer")
-
- # If we make a change to the file after installing the
- # overlayer, we shouldn't touch the file on uninstall.
- self.pkg("-D allow-overlays=1 install overlayer")
- self.file_append("etc/pam.conf", "zigit")
- self.pkg("uninstall overlayer")
- self.file_contains("etc/pam.conf", "zigit")
- self.file_contains("etc/pam.conf", "file2")
-
- # Re-use the overlay packages for some preserve testing.
- self.pkg("revert /etc/pam.conf")
- self.file_contains("etc/pam.conf", "file1")
- self.pkg("-D broken-conflicting-action-handling=1 install overlayer")
+ self.pkg("-D broken-conflicting-action-handling=1 install "
+ "invalid-overlayer")
# We may have been able to lay down the package, but because the
# file is marked preserve=true, we didn't actually overwrite
self.file_contains("etc/pam.conf", "file2")
- self.pkg("uninstall overlayer")
+ self.pkg("uninstall invalid-overlayer")
self.file_contains("etc/pam.conf", "file2")
# Make sure we get rid of all implicit directories.
@@ -6597,6 +6616,120 @@
self.pkg("uninstall pkg2", exit=1)
self.pkg("verify pkg2")
+ def test_overlay_files(self):
+ """Test the behaviour of pkg(1) when actions for editable files
+ overlay other actions."""
+
+ # Ensure that overlay is allowed for file actions when one
+ # action has specified preserve attribute and overlay=allow,
+ # and *one* (only) other action has specified overlay=true
+ # (preserve does not have to be set).
+ self.image_create(self.rurl)
+
+ # Should fail because one action specified overlay=allow,
+ # but not preserve (it isn't editable).
+ self.pkg("install invalid-overlaid")
+ self.pkg("install overlayer", exit=1)
+ self.pkg("uninstall invalid-overlaid")
+
+ # Should fail because one action is overlayable but overlaying
+ # action doesn't declare its intent to overlay.
+ self.pkg("install overlaid")
+ self.file_contains("etc/pam.conf", "file1")
+ self.pkg("install invalid-overlayer", exit=1)
+
+ # Should fail because one action is overlayable but overlaying
+ # action mode, owner, and group attributes don't match.
+ self.pkg("install mismatch-overlayer", exit=1)
+
+ # Should succeed because one action is overlayable and
+ # overlaying action declares its intent to overlay.
+ self.pkg("contents -m overlaid")
+ self.pkg("contents -mr overlayer")
+ self.pkg("install overlayer")
+ self.file_contains("etc/pam.conf", "file2")
+
+ # Should fail because multiple actions are not allowed to
+ # overlay a single action.
+ self.pkg("install multi-overlayer", exit=1)
+
+ # Should succeed even though file is different than originally
+ # delivered since original package permits file modification.
+ self.pkg("verify overlaid overlayer")
+
+ # Should succeed because package delivering overlayable file
+ # permits modification and because package delivering overlay
+ # file permits modification.
+ self.file_append("etc/pam.conf", "zigit")
+ self.pkg("verify overlaid overlayer")
+
+ # Verify that the file isn't touched on uninstall of the
+ # overlaying package if package being overlaid is still
+ # installed.
+ self.pkg("uninstall overlayer")
+ self.file_contains("etc/pam.conf", "zigit")
+ self.file_contains("etc/pam.conf", "file2")
+
+ # Verify that removing the last package delivering an overlaid
+ # file removes the file.
+ self.pkg("uninstall overlaid")
+ self.file_doesnt_exist("etc/pam.conf")
+
+ # Verify that installing both packages at the same time results
+ # in only the overlaying file being delivered.
+ self.pkg("install overlaid overlayer")
+ self.file_contains("etc/pam.conf", "file2")
+
+ # Verify that the file isn't touched on uninstall of the
+ # overlaid package if overlaying package is still installed.
+ self.file_append("etc/pam.conf", "zigit")
+ self.pkg("uninstall overlaid")
+ self.file_contains("etc/pam.conf", "file2")
+ self.file_contains("etc/pam.conf", "zigit")
+
+ # Re-install overlaid package and verify that file content
+ # does not change.
+ self.pkg("install overlaid")
+ self.file_contains("etc/pam.conf", "file2")
+ self.file_contains("etc/pam.conf", "zigit")
+ self.pkg("uninstall overlaid overlayer")
+
+ # Should succeed because one action is overlayable and
+ # overlaying action declares its intent to overlay even
+ # though the overlaying action isn't marked with preserve.
+ self.pkg("install overlaid unpreserved-overlayer")
+ self.file_contains("etc/pam.conf", "file2")
+
+ # Should succeed because overlaid action permits modification
+ # and contents matches overlaying action.
+ self.pkg("verify overlaid unpreserved-overlayer")
+
+ # Should succeed even though file has been modified since
+ # overlaid action permits modification.
+ self.file_append("etc/pam.conf", "zigit")
+ self.pkg("verify overlaid")
+
+ # Should fail because overlaying action does not permit
+ # modification.
+ self.pkg("verify unpreserved-overlayer", exit=1)
+
+ # Should revert to content delivered by overlaying action.
+ self.pkg("fix unpreserved-overlayer")
+ self.file_contains("etc/pam.conf", "file2")
+ self.file_doesnt_contain("etc/pam.conf", "zigit")
+
+ # Should revert to content delivered by overlaying action.
+ self.file_append("etc/pam.conf", "zigit")
+ self.pkg("revert /etc/pam.conf")
+ self.file_contains("etc/pam.conf", "file2")
+ self.file_doesnt_contain("etc/pam.conf", "zigit")
+ self.pkg("uninstall unpreserved-overlayer")
+
+ # Should revert to content delivered by overlaid action.
+ self.file_contains("etc/pam.conf", "file2")
+ self.pkg("revert /etc/pam.conf")
+ self.file_contains("etc/pam.conf", "file1")
+
def test_different_types(self):
"""Test the behavior of pkg(1) when multiple actions of
different types deliver to the same pathname."""