PSARC/2016/131 IPS image modification notification
authorShawn Walker-Salas <shawn.walker@oracle.com>
Thu, 25 Feb 2016 18:47:28 -0800
changeset 3316 97e1801e1f2c
parent 3315 e2377565405f
child 3317 81fca2c09539
PSARC/2016/131 IPS image modification notification 21535356 pkg should provide a file as public interface to track changes
src/modules/client/image.py
src/modules/client/imageplan.py
src/tests/cli/t_pkg_modified.py
--- a/src/modules/client/image.py	Sun Feb 28 16:24:18 2016 -0800
+++ b/src/modules/client/image.py	Thu Feb 25 18:47:28 2016 -0800
@@ -21,7 +21,7 @@
 #
 
 #
-# Copyright (c) 2007, 2015, Oracle and/or its affiliates. All rights reserved.
+# Copyright (c) 2007, 2016, Oracle and/or its affiliates. All rights reserved.
 #
 
 import M2Crypto as m2
@@ -667,6 +667,22 @@
                                         setattr(u, prop, os.path.join(ssl_dir,
                                            os.path.basename(pval)))
 
+        def update_last_modified(self):
+                """Update $imgdir/modified timestamp for image; should be
+                called after any image modification has completed.  This
+                provides a public interface for programs that want to monitor
+                the image for modifications via event ports, etc."""
+
+                # This is usually /var/pkg/modified.
+                fname = os.path.join(self.imgdir, "modified")
+                try:
+                        with os.fdopen(
+                            os.open(fname, os.O_CREAT|os.O_NOFOLLOW,
+                                misc.PKG_FILE_MODE)) as f:
+                                os.utime(fname, None)
+                except EnvironmentError as e:
+                        raise apx._convert_error(e)
+
         def save_config(self):
                 # First, create the image directories if they haven't been, so
                 # the configuration file can be written.
@@ -674,6 +690,7 @@
 
                 self.__store_publisher_ssl()
                 self.cfg.write()
+                self.update_last_modified()
                 self.__load_publisher_ssl()
 
                 # Remove the old the pkg.sysrepo(1M) cache, if present.
@@ -3251,6 +3268,7 @@
                 # Ensure in-memory catalogs get reloaded.
                 self.__init_catalogs()
 
+                self.update_last_modified()
                 progtrack.cache_catalogs_done()
                 self.history.log_operation_end()
 
@@ -4717,12 +4735,13 @@
                         json.dump((self.__AVOID_SET_VERSION, d), tf)
                         tf.close()
                         portable.rename(tmp_file, state_file)
-
                 except Exception as e:
                         logger.warn("Cannot save avoid list: {0}".format(
                             str(e)))
                         return
 
+                self.update_last_modified()
+
                 self.__avoid_set_altered = False
 
         # frozen dict implementation uses simplejson to store a dictionary of
@@ -4773,6 +4792,7 @@
                                 json.dump(
                                     (self.__FROZEN_DICT_VERSION, new_dict), tf)
                         portable.rename(tmp_file, state_file)
+                        self.update_last_modified()
                 except EnvironmentError as e:
                         raise apx._convert_error(e)
                 self.__rebuild_image_catalogs()
--- a/src/modules/client/imageplan.py	Sun Feb 28 16:24:18 2016 -0800
+++ b/src/modules/client/imageplan.py	Thu Feb 25 18:47:28 2016 -0800
@@ -4780,6 +4780,11 @@
                                         self.image.cfg.set_property("property",
                                             "dehydrated", self.operations_pubs)
                                         self.image.save_config()
+                                else:
+                                        # Mark image as modified if not calling
+                                        # save_config (which will do it for us).
+                                        self.image.update_last_modified()
+
                         except EnvironmentError as e:
                                 if e.errno == errno.EACCES or \
                                     e.errno == errno.EPERM:
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/tests/cli/t_pkg_modified.py	Thu Feb 25 18:47:28 2016 -0800
@@ -0,0 +1,216 @@
+#!/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) 2016, Oracle and/or its affiliates. All rights reserved.
+
+import testutils
+if __name__ == "__main__":
+        testutils.setup_environment("../../../proto")
+import pkg5unittest
+
+import os
+import unittest
+
+class TestPkgModified(pkg5unittest.SingleDepotTestCase):
+        # Tests in this suite use the read only data directory.
+        need_ro_data = True
+
+        foo10 = """
+            open [email protected],5.11-0
+            add dir mode=0755 owner=root group=bin path=etc
+            add file tmp/cat mode=0644 owner=root group=bin path=etc/motd
+            close """
+
+        misc_files = ["tmp/cat"]
+
+        def setUp(self):
+                pkg5unittest.SingleDepotTestCase.setUp(self)
+                self.make_misc_files(self.misc_files)
+
+        def __assert_image_modified(self, api_inst, omtime, modified):
+                mfpath = os.path.join(api_inst.img.imgdir, "modified")
+                nmtime = os.stat(mfpath).st_mtime
+                if modified:
+                        self.assertNotEqual(omtime, nmtime)
+                else:
+                        self.assertEqual(omtime, nmtime)
+                return nmtime
+
+        def test_modified(self):
+                """Verify that $IMGDIR/modified is updated whenever an
+                image-modifying operation is completed."""
+
+                api_inst = self.image_create(self.rurl)
+                mfpath = os.path.join(api_inst.img.imgdir, "modified")
+
+                # Assert /var/pkg/modified exists.
+                self.file_exists(mfpath)
+                omtime = os.stat(mfpath).st_mtime
+
+                self.pkgsend_bulk(self.rurl, self.foo10)
+
+                # Now perform various operations and assert the image was marked
+                # as modified or not depending on operation.
+                self.__assert_image_modified(api_inst, omtime, False)
+
+                self.pkg("refresh")
+                omtime = self.__assert_image_modified(api_inst, omtime, True)
+
+                #
+                # Operations that should not mark image as modified.
+                #
+                for op, op_ret in (
+                    ("facet", 0),
+                    ("contents", 1),
+                    ("history", 0),
+                    ("info", 1),
+                    ("info -r \*", 0),
+                    ("list", 1),
+                    ("mediator", 0),
+                    ("property", 0),
+                    ("unset-property no-such-property", 1),
+                    ("publisher", 0),
+                    ("refresh no-such-publisher", 1),
+                    ("variant", 0)
+                ):
+                        self.pkg(op, exit=op_ret)
+                        self.__assert_image_modified(api_inst, omtime, False)
+
+                self.pkg("version", use_img_root=False)
+                self.__assert_image_modified(api_inst, omtime, False)
+
+                #
+                # Now perform various combos of operations testing modification.
+                #
+                self.pkg("refresh")
+                omtime = self.__assert_image_modified(api_inst, omtime, False)
+
+                self.pkg("install -nv foo")
+                omtime = self.__assert_image_modified(api_inst, omtime, False)
+
+                self.pkg("install --reject foo foo", exit=1)
+                omtime = self.__assert_image_modified(api_inst, omtime, False)
+
+                self.pkg("install foo")
+                omtime = self.__assert_image_modified(api_inst, omtime, True)
+
+                self.pkg("set-mediator -I postfix sendmail")
+                omtime = self.__assert_image_modified(api_inst, omtime, True)
+
+                self.pkg("unset-mediator -I sendmail")
+                omtime = self.__assert_image_modified(api_inst, omtime, True)
+
+                self.pkg("unset-mediator -I sendmail", exit=4)
+                omtime = self.__assert_image_modified(api_inst, omtime, False)
+
+                # this should not register a modification; compliance tool
+                # relies on that
+                self.pkg("verify foo")
+                omtime = self.__assert_image_modified(api_inst, omtime, False)
+
+                self.pkg("fix foo", exit=4)
+                omtime = self.__assert_image_modified(api_inst, omtime, False)
+
+                # this should not register a modification; compliance tool
+                # relies on that
+                self.pkg("revert -n /etc/motd", exit=4)
+                omtime = self.__assert_image_modified(api_inst, omtime, False)
+
+                # modify etc/motd; fix should register modification
+                self.file_append("etc/motd", "foo")
+                self.pkg("fix foo")
+                omtime = self.__assert_image_modified(api_inst, omtime, True)
+
+                # modify etc/motd; revert should register modification
+                self.file_append("etc/motd", "foo")
+                self.pkg("revert /etc/motd")
+                omtime = self.__assert_image_modified(api_inst, omtime, True)
+
+                self.pkg("revert /etc/motd", exit=4)
+                omtime = self.__assert_image_modified(api_inst, omtime, False)
+
+                self.pkg("fix foo", exit=4)
+                omtime = self.__assert_image_modified(api_inst, omtime, False)
+
+                self.pkg("freeze foo")
+                omtime = self.__assert_image_modified(api_inst, omtime, True)
+
+                self.pkg("unfreeze foo")
+                omtime = self.__assert_image_modified(api_inst, omtime, True)
+
+                self.pkg("uninstall foo")
+                omtime = self.__assert_image_modified(api_inst, omtime, True)
+
+                self.pkg("avoid foo")
+                omtime = self.__assert_image_modified(api_inst, omtime, True)
+
+                self.pkg("unavoid foo")
+                omtime = self.__assert_image_modified(api_inst, omtime, True)
+
+                self.pkg("set-property be-policy always-new")
+                omtime = self.__assert_image_modified(api_inst, omtime, True)
+
+                self.pkg("unset-property be-policy")
+                omtime = self.__assert_image_modified(api_inst, omtime, True)
+
+                self.pkg("change-facet wombat=false")
+                omtime = self.__assert_image_modified(api_inst, omtime, True)
+
+                self.pkg("change-facet wombat=None")
+                omtime = self.__assert_image_modified(api_inst, omtime, True)
+
+                self.pkg("change-variant osnet.debug=true")
+                omtime = self.__assert_image_modified(api_inst, omtime, True)
+
+                self.pkg("change-variant osnet.debug=None")
+                omtime = self.__assert_image_modified(api_inst, omtime, True)
+
+                # Remove the last_refreshed file for one of the publishers so
+                # that it will be seen as needing refresh.
+                pub = api_inst.get_publisher("test")
+                os.remove(os.path.join(pub.meta_root, "last_refreshed"))
+                self.pkgsend_bulk(self.rurl, self.foo10)
+                self.pkg("list -a")
+                omtime = self.__assert_image_modified(api_inst, omtime, True)
+
+                # Add, remove, and modify publishers.
+                self.pkg("unset-publisher test")
+                omtime = self.__assert_image_modified(api_inst, omtime, True)
+
+                self.pkg("set-publisher -p {0}".format(self.rurl))
+                omtime = self.__assert_image_modified(api_inst, omtime, True)
+
+                self.pkg("set-publisher -G {0} test".format(self.rurl))
+                omtime = self.__assert_image_modified(api_inst, omtime, True)
+
+                self.pkg("set-publisher -g {0} test".format(self.rurl))
+                omtime = self.__assert_image_modified(api_inst, omtime, True)
+
+                self.pkg("set-publisher --sticky test")
+                omtime = self.__assert_image_modified(api_inst, omtime, True)
+
+                self.pkg("set-publisher --non-sticky test")
+                omtime = self.__assert_image_modified(api_inst, omtime, True)
+
+
+if __name__ == "__main__":
+        unittest.main()