17938345 need an abandonment mechanism for editable files
authorYiteng Zhang <yiteng.zhang@oracle.com>
Fri, 27 Jun 2014 14:29:07 -0700
changeset 3093 0cf25eddae35
parent 3092 5ed632ebf9b9
child 3094 15de2efea709
17938345 need an abandonment mechanism for editable files
src/man/pkg.5
src/modules/actions/file.py
src/modules/client/imageplan.py
src/tests/cli/t_pkg_install.py
--- a/src/man/pkg.5	Wed Jun 25 14:24:57 2014 -0700
+++ b/src/man/pkg.5	Fri Jun 27 14:29:07 2014 -0700
@@ -1,6 +1,6 @@
 '\" te
 .\" Copyright (c) 2009, 2014, Oracle and/or its affiliates. All rights reserved.
-.TH pkg 5 "10 Apr 2014" "SunOS 5.12" "Standards, Environments, and Macros"
+.TH pkg 5 "28 May 2014" "SunOS 5.12" "Standards, Environments, and Macros"
 .SH NAME
 pkg \- Image Packaging System
 .SH DESCRIPTION
@@ -145,22 +145,22 @@
 .RS 4n
 Specifies when and how files are preserved during package operations.
 .sp
-When a package is initially installed, if a file delivered by the package has a \fBpreserve\fR attribute defined with any value and the file already exists in the image, the existing file is stored in \fB/var/pkg/lost+found\fR and the packaged file is installed.
+When a package is initially installed, if a file delivered by the package has a \fBpreserve\fR attribute defined with any value except \fBabandon\fR and the file already exists in the image, the existing file is stored in \fB/var/pkg/lost+found\fR and the packaged file is installed.
 .sp
 When a package is initially installed, if a file delivered by the package has a \fBpreserve\fR attribute defined and the file does not already exist in the image, whether that file is installed depends on the value of the \fBpreserve\fR attribute:
 .RS +4
 .TP
 .ie t \(bu
 .el o
-If the value of \fBpreserve\fR is \fBlegacy\fR, the packaged file is not installed.
+If the value of \fBpreserve\fR is \fBlegacy\fR or \fBabandon\fR, the packaged file is not installed.
 .RE
 .RS +4
 .TP
 .ie t \(bu
 .el o
-If the value of \fBpreserve\fR is not \fBlegacy\fR, the packaged file is installed.
+If the value of \fBpreserve\fR is not \fBlegacy\fR or \fBabandon\fR, the packaged file is installed.
 .RE
-When a package is downgraded, if a file delivered by the downgraded version of the package has a \fBpreserve\fR attribute defined with any value and all of the following conditions are true, the file that currently exists in the image is renamed with the extension \fB\&.update\fR, and the file from the downgraded package is installed:
+When a package is downgraded, if a file delivered by the downgraded version of the package has a \fBpreserve\fR attribute defined with any value except \fBabandon\fR and all of the following conditions are true, the file that currently exists in the image is renamed with the extension \fB\&.update\fR, and the file from the downgraded package is installed.
 .RS +4
 .TP
 .ie t \(bu
@@ -188,6 +188,12 @@
 .TP
 .ie t \(bu
 .el o
+If the file delivered by the upgraded version of the package has a \fBpreserve\fR value of \fBabandon\fR in the upgraded package, the new file will not be installed and the existing file will not be modified.
+.RE
+.RS +4
+.TP
+.ie t \(bu
+.el o
 If the file does not exist in the image, the new file is installed.
 .RE
 .RS +4
@@ -238,6 +244,8 @@
 .el o
 If the file delivered by the upgraded version of the package exists in the image and has a \fBpreserve\fR value of \fBlegacy\fR in both the upgraded package and the currently installed version of the package, the permissions and timestamp (if present) are reset on the existing file.
 .RE
+.sp
+When a package is uninstalled, if a \fBfile\fR action delivered by the currently installed version of the package has a \fBpreserve\fR value of \fBabandon\fR and the file exists in the image, the file will not be removed.
 .RE
 
 .sp
--- a/src/modules/actions/file.py	Wed Jun 25 14:24:57 2014 -0700
+++ b/src/modules/actions/file.py	Fri Jun 27 14:29:07 2014 -0700
@@ -118,8 +118,10 @@
                         self.makedirs(os.path.dirname(final_path),
                             mode=misc.PKG_DIR_MODE,
                             fmri=pkgplan.destination_fmri)
-                elif not orig and not pkgplan.origin_fmri and \
-                     "preserve" in self.attrs and os.path.isfile(final_path):
+                elif (not orig and not pkgplan.origin_fmri and
+                     "preserve" in self.attrs and
+                     self.attrs["preserve"] != "abandon" and
+                     os.path.isfile(final_path)):
                         # Unpackaged editable file is already present during
                         # initial install; salvage it before continuing.
                         pkgplan.salvage(final_path)
@@ -161,6 +163,8 @@
                         old_path = final_path + ".old"
                 elif pres_type == "renamenew":
                         final_path = final_path + ".new"
+                elif pres_type == "abandon":
+                        return
 
                 # If it is a directory (and not empty) then we should
                 # salvage the contents.
@@ -292,6 +296,9 @@
                 the preserve attribute is not present, that the hashes
                 and other attributes of the file match."""
 
+                if self.attrs.get("preserve") == "abandon":
+                        return [], [], []
+
                 path = self.get_installed_path(img.get_root())
 
                 lstat, errors, warnings, info, abort = \
@@ -453,7 +460,8 @@
                 Returns False if it is, but no preservation is necessary.
                 Returns True for the normal preservation form.  Returns one of
                 the strings 'renameold', 'renameold.update', 'renamenew',
-                or 'legacy' for each of the respective forms of preservation.
+                'legacy', or 'abandon' for each of the respective forms of
+                preservation.
                 """
 
                 # If the logic in this function ever changes, all callers will
@@ -471,6 +479,9 @@
                         # attribute, this will need to be updated.
                         return
 
+                if pres_type == "abandon":
+                        return pres_type
+
                 final_path = self.get_installed_path(pkgplan.image.get_root())
 
                 # 'legacy' preservation is very different than other forms of
@@ -617,7 +628,7 @@
                                 return True
 
                 pres_type = self._check_preserve(orig, pkgplan)
-                if pres_type != None and pres_type != True:
+                if pres_type not in (None, True, "abandon"):
                         # Preserved files only need data if they're being
                         # changed (e.g. "renameold", etc.).
                         return True
@@ -640,6 +651,9 @@
                                 # likely overlaid and is moving).
                                 return
 
+                if self.attrs.get("preserve") == "abandon":
+                        return
+
                 try:
                         # Make file writable so it can be deleted.
                         os.chmod(path, stat.S_IWRITE|stat.S_IREAD)
--- a/src/modules/client/imageplan.py	Wed Jun 25 14:24:57 2014 -0700
+++ b/src/modules/client/imageplan.py	Fri Jun 27 14:29:07 2014 -0700
@@ -2577,6 +2577,10 @@
                                 # Ignore erroneously tagged files.
                                 continue
 
+                        if src.attrs.get("preserve") == "abandon":
+                                # preserve=abandon files are never removed.
+                                continue
+
                         entry = [src.attrs["path"]]
                         save_file = src.attrs.get("save_file")
                         if save_file:
@@ -2631,7 +2635,10 @@
                                 # We can't rely on _check_preserve for this case
                                 # as there's no existing on-disk file at the
                                 # destination path yet.
-                                if dest.attrs.get("preserve") != "legacy":
+                                dpres_type = dest.attrs.get("preserve")
+                                if (dpres_type != "legacy" and
+                                    dpres_type != "abandon"):
+                                        # 'abandon' actions are never delivered;
                                         # 'legacy' actions are only delivered if
                                         # we're updating something already
                                         # installed or moving an existing file.
@@ -2669,6 +2676,10 @@
                         else:
                                 mpath = tpath
 
+                        if pres_type == "abandon":
+                                # newly-tagged preserve=abandon files never
+                                # delivered.
+                                continue
                         if pres_type == "renameold":
                                 moved.append([mpath, tpath + ".old"])
                                 installed.append(entry)
--- a/src/tests/cli/t_pkg_install.py	Wed Jun 25 14:24:57 2014 -0700
+++ b/src/tests/cli/t_pkg_install.py	Fri Jun 27 14:29:07 2014 -0700
@@ -2461,6 +2461,21 @@
             close
         """
 
+        presabandon = """
+            open [email protected]
+            add file tmp/preserve1 path=testme mode=0444 owner=root group=root preserve=true
+            close
+            open [email protected]
+            add file tmp/preserve1 path=testme mode=0644  owner=root group=root preserve=abandon
+            close
+            open [email protected]
+            add file tmp/preserve3 path=testme mode=0444  owner=root group=root preserve=abandon
+            close
+            open [email protected]
+            add file tmp/preserve3 path=testme mode=0644  owner=root group=root preserve=true
+            close
+        """
+
         renpreserve = """
             open [email protected]
             add file tmp/preserve1 path=foo1 mode=0644 owner=root group=root preserve=true
@@ -3464,6 +3479,184 @@
                 self.file_contains("testme.legacy", "preserve1")
                 self.file_contains("newme", "preserve2")
 
+        def test_file_preserve_abandon(self):
+                """Verify that preserve=abandon works as expected."""
+
+                install_cmd = "install"
+                self.pkgsend_bulk(self.rurl, self.presabandon)
+                self.image_create(self.rurl)
+
+                # Ensure directory is empty before testing.
+                api_inst = self.get_img_api_obj()
+                img_inst = api_inst.img
+                sroot = os.path.join(img_inst.imgdir, "lost+found")
+                shutil.rmtree(sroot)
+
+                # Verify that unpackaged files will not be salvaged on initial
+                # install if a package being installed delivers the same file
+                # and that the new file will not be installed.
+                self.file_append("testme", "unpackaged")
+                self.pkg("%s --parsable=0 presabandon@2" % install_cmd)
+                self._assertEditables()
+                self.file_contains("testme", "unpackaged")
+                self.assert_(not os.path.exists(os.path.join(sroot, "testme")))
+                self.file_remove("testme")
+                self.pkg("uninstall presabandon")
+
+                # Verify that an initial install of an action with
+                # preserve=abandon will not install the payload of the action.
+                self.pkg("%s --parsable=0 presabandon@2" % install_cmd)
+                self._assertEditables()
+                self.file_doesnt_exist("testme")
+                self.pkg("uninstall presabandon")
+
+                # If an action delivered by the upgraded version of the package
+                # has a preserve=abandon, the new file will not be installed and
+                # the existing file will not be modified.
+
+                # First with no content change ...
+                self.pkg("%s --parsable=0 presabandon@1" % install_cmd)
+                self._assertEditables(
+                    installed=['testme'],
+                )
+                self.pkg("update --parsable=0 presabandon@2")
+                self._assertEditables()
+                self.file_contains("testme", "preserve1")
+                # The currently installed version of the package has a preserve
+                # value of abandon, so the file will not be removed.
+                self.pkg("uninstall --parsable=0 presabandon")
+                self._assertEditables()
+                self.file_exists("testme")
+
+                # If an action delivered by the downgraded version of the package
+                # has a preserve=abandon, the new file will not be installed and
+                # the existing file will not be modified.
+                self.pkg("%s --parsable=0 presabandon@4" % install_cmd)
+                self._assertEditables(
+                    installed=['testme'],
+                )
+                self.file_contains("testme", "preserve3")
+                self.pkg("verify presabandon")
+                self.pkg("update --parsable=0 presabandon@3")
+                self._assertEditables()
+                self.file_contains("testme", "preserve3")
+                self.pkg("verify presabandon")
+                self.pkg("uninstall --parsable=0 presabandon")
+                self.file_remove("testme")
+
+                # ... and again with content change.
+                self.pkg("%s --parsable=0 presabandon@1" % install_cmd)
+                self.pkg("%s --parsable=0 presabandon@3" % install_cmd)
+                self._assertEditables()
+                self.file_contains("testme", "preserve1")
+                self.pkg("uninstall --parsable=0 presabandon")
+
+                self.pkg("install --parsable=0 presabandon@4")
+                self._assertEditables(
+                    installed=['testme'],
+                )
+                self.file_contains("testme", "preserve3")
+                self.pkg("update --parsable=0 presabandon@2")
+                self._assertEditables()
+                self.file_contains("testme", "preserve3")
+                self.pkg("verify presabandon")
+                self.pkg("uninstall --parsable=0 presabandon")
+                self.file_remove("testme")
+
+                # Modify the file locally and upgrade to a version where the
+                # file has a preserve=abandon attribute and the content changes.
+                self.pkg("%s --parsable=0 presabandon@1" % install_cmd)
+                self.file_append("testme", "junk")
+                self.pkg("%s --parsable=0 presabandon@3" % install_cmd)
+                self._assertEditables()
+                self.file_contains("testme", "preserve1")
+                self.file_contains("testme", "junk")
+                self.file_doesnt_exist("testme.old")
+                self.file_doesnt_exist("testme.new")
+                self.pkg("uninstall --parsable=0 presabandon")
+                self.file_remove("testme")
+
+                # Modify the file locally and downgrade to a version where the
+                # file has a preserve=abandon attribute and the content changes.
+                self.pkg("%s --parsable=0 presabandon@4" % install_cmd)
+                self.file_append("testme", "junk")
+                self.file_contains("testme", "preserve3")
+                self.pkg("update --parsable=0 presabandon@2")
+                self._assertEditables()
+                self.file_contains("testme", "preserve3")
+                self.file_contains("testme", "junk")
+                self.file_doesnt_exist("testme.old")
+                self.file_doesnt_exist("testme.new")
+                self.file_doesnt_exist("testme.update")
+                self.pkg("verify presabandon")
+                self.pkg("uninstall --parsable=0 presabandon")
+                self.file_remove("testme")
+
+                # Modify the file locally and upgrade to a version where the
+                # file has a preserve=abandon attribute and just the mode changes.
+                self.pkg("%s --parsable=0 presabandon@1" % install_cmd)
+                self.file_append("testme", "junk")
+                self.pkg("%s --parsable=0 presabandon@2" % install_cmd)
+                self._assertEditables()
+                self.file_contains("testme", "preserve1")
+                self.file_contains("testme", "junk")
+                self.file_doesnt_exist("testme.old")
+                self.file_doesnt_exist("testme.new")
+                self.pkg("verify presabandon")
+                self.pkg("uninstall --parsable=0 presabandon")
+                self.file_remove("testme")
+
+                # Modify the file locally and downgrade to a version where the
+                # file has a preserve=abandon attribute and just the mode changes.
+                self.pkg("%s --parsable=0 presabandon@4" % install_cmd)
+                self.file_append("testme", "junk")
+                self.pkg("update --parsable=0 presabandon@3")
+                self._assertEditables()
+                self.file_contains("testme", "preserve3")
+                self.file_contains("testme", "junk")
+                self.file_doesnt_exist("testme.old")
+                self.file_doesnt_exist("testme.new")
+                self.file_doesnt_exist("testme.update")
+                self.pkg("verify presabandon")
+                self.pkg("uninstall --parsable=0 presabandon")
+                self.file_remove("testme")
+
+                # Remove the file locally and update the package where the
+                # file has a preserve=abandon attribute; this will not replace
+                # the missing file.
+                self.pkg("%s --parsable=0 presabandon@1" % install_cmd)
+                self.file_remove("testme")
+                self.pkg("%s --parsable=0 presabandon@2" % install_cmd)
+                self._assertEditables()
+                self.file_doesnt_exist("testme")
+                self.pkg("uninstall --parsable=0 presabandon")
+
+                # Remove the file locally and downgrade the package where the
+                # file has a preserve=abandon attribute; this will not replace
+                # the missing file.
+                self.pkg("%s --parsable=0 presabandon@4" % install_cmd)
+                self.file_remove("testme")
+                self.pkg("update --parsable=0 presabandon@3")
+                self._assertEditables()
+
+                # Verify that a package with a missing file that is marked with
+                # the preserve=abandon won't cause uninstall failure.
+                self.file_doesnt_exist("testme")
+                self.pkg("uninstall --parsable=0 presabandon")
+
+                # Verify that if the file for an action marked with
+                # preserve=abandon is removed that the package still
+                # verifies.
+                self.pkg("%s --parsable=0 presabandon@1" % install_cmd)
+                self.pkg("%s --parsable=0 presabandon@2" % install_cmd)
+                self.file_remove("testme")
+                self.pkg("verify -v presabandon")
+
+                # Verify that a file removed for an action marked with
+                # preserve=abandon can be reverted.
+                self.pkg("revert testme")
+                self.file_contains("testme", "preserve1")
+
         def test_directory_salvage(self):
                 """Make sure basic directory salvage works as expected"""