17699319 we could make downgrade easier in the case of incorporated packages s12b82
authorErik Trauschke <erik.trauschke@oracle.com>
Thu, 20 Aug 2015 13:26:55 -0700
changeset 3248 30587e6b1c49
parent 3247 780569b617ad
child 3249 255c4e22bb34
17699319 we could make downgrade easier in the case of incorporated packages
src/modules/client/pkg_solver.py
src/tests/cli/t_pkg_install.py
src/tests/cli/t_pkg_linked.py
--- a/src/modules/client/pkg_solver.py	Wed Aug 19 17:55:12 2015 -0700
+++ b/src/modules/client/pkg_solver.py	Thu Aug 20 13:26:55 2015 -0700
@@ -225,6 +225,10 @@
                     },
                 }
 
+                self.__allowed_downgrades = set()  # allowed downrev FMRIs
+                self.__dg_incorp_cache = {}        # cache for downgradable
+                                                   # incorp deps
+
         def __str__(self):
                 s = "Solver: ["
                 if self.__state in [SOLVER_FAIL, SOLVER_SUCCESS]:
@@ -279,6 +283,8 @@
                 self.__dependents = None
                 self.__fmridict = {}
                 self.__firmware = None
+                self.__allowed_downgrades = None
+                self.__dg_incorp_cache = None
 
                 if DebugValues["plan"]:
                         # Remaining data must be kept.
@@ -849,6 +855,43 @@
                 self.__inc_list = inc_list
 
                 self.__start_subphase(2)
+                # generate set of possible fmris
+                #
+                # ensure existing pkgs stay installed; explicitly add in
+                # installed fmris in case publisher change has occurred and
+                # some pkgs aren't part of new publisher
+                possible_set = set()
+                self.__allowed_downgrades = set()
+                for f in self.__installed_fmris - self.__removal_fmris:
+                        possible_set |= self.__comb_newer_fmris(f)[0] | set([f])
+
+                # Add the proposed fmris, populate self.__expl_install_dict and
+                # check for allowed downgrades.
+                self.__expl_install_dict = defaultdict(list)
+                for name, flist in proposed_dict.items():
+                        possible_set.update(flist)
+                        for f in flist:
+                                self.__progress()
+                                self.__allowed_downgrades |= \
+                                    self.__allow_incorp_downgrades(f,
+                                    excludes=excludes)
+                                if self.__is_explicit_install(f):
+                                        self.__expl_install_dict[name].append(f)
+
+                # For linked image sync we have to analyze all pkgs of the
+                # possible_set because no proposed pkgs will be given. However,
+                # that takes more time so only do this for syncs. The relax_all
+                # flag is an indicator of a sync operation.
+                if not proposed_dict.values() and relax_all:
+                        for f in possible_set:
+                                self.__progress()
+                                self.__allowed_downgrades |= \
+                                    self.__allow_incorp_downgrades(f,
+                                    excludes=excludes)
+
+                possible_set |= self.__allowed_downgrades
+
+                self.__start_subphase(3)
                 # If requested, trim any proposed fmris older than those of
                 # corresponding installed packages.
                 # Because we have introduced exact-install where
@@ -898,7 +941,7 @@
                         self.__trim_recursive_incorps(proposed_dict[name],
                             excludes)
 
-                self.__start_subphase(3)
+                self.__start_subphase(4)
                 # now trim pkgs we cannot update due to maintained
                 # incorporations
                 for i, flist in zip(inc_list, con_lists):
@@ -914,18 +957,18 @@
                                     dotrim=False)[1], _TRIM_INSTALLED_INC,
                                     reason)
 
-                self.__start_subphase(4)
+                self.__start_subphase(5)
                 # now trim any pkgs we cannot update due to freezes
                 self.__trim_frozen(existing_freezes)
 
-                self.__start_subphase(5)
+                self.__start_subphase(6)
                 # elide any proposed versions that don't match variants (arch
                 # usually)
                 for name in proposed_dict:
                         for fmri in proposed_dict[name]:
                                 self.__trim_nonmatching_variants(fmri)
 
-                self.__start_subphase(6)
+                self.__start_subphase(7)
                 # remove any versions from proposed_dict that are in trim_dict
                 try:
                         self.__trim_proposed(proposed_dict)
@@ -934,25 +977,6 @@
                         self.__raise_install_error(exp, inc_list, proposed_dict,
                             set(), excludes)
 
-                self.__start_subphase(7)
-                # generate set of possible fmris
-                #
-                # ensure existing pkgs stay installed; explicitly add in
-                # installed fmris in case publisher change has occurred and
-                # some pkgs aren't part of new publisher
-                possible_set = set()
-                for f in self.__installed_fmris - self.__removal_fmris:
-                        possible_set |= self.__comb_newer_fmris(f)[0] | set([f])
-
-                # add the proposed fmris and populate self.__expl_install_dict.
-                self.__expl_install_dict = defaultdict(list)
-                for name, flist in proposed_dict.items():
-                        possible_set.update(flist)
-                        for fmri in flist:
-                                if self.__is_explicit_install(fmri):
-                                        self.__expl_install_dict[name].append(
-                                            fmri)
-
                 self.__start_subphase(8)
 
                 # Update the set of possible fmris with the transitive closure
@@ -1088,14 +1112,6 @@
                 self.__set_removed_and_required_packages(rejected=reject_set)
                 self.__progress()
 
-                # trim fmris we cannot install because they're older
-                for f in self.__installed_fmris:
-                        self.__progress()
-                        self.__trim_older(f)
-
-                # now trim any pkgs we cannot update due to freezes
-                self.__trim_frozen(existing_freezes)
-
                 self.__start_subphase(2)
                 # generate set of possible fmris
                 possible_set = set()
@@ -1106,6 +1122,20 @@
                                 matching = set([f]) # staying put is an option
                         possible_set |= matching
 
+                self.__allowed_downgrades = set()
+                for f in possible_set:
+                        self.__allowed_downgrades |= \
+                            self.__allow_incorp_downgrades(f, excludes=excludes)
+                possible_set |= self.__allowed_downgrades
+
+                # trim fmris we cannot install because they're older
+                for f in self.__installed_fmris:
+                        self.__progress()
+                        self.__trim_older(f)
+
+                # now trim any pkgs we cannot update due to freezes
+                self.__trim_frozen(existing_freezes)
+
                 self.__start_subphase(3)
                 # Update the set of possible FMRIs with the transitive closure
                 # of all dependencies.
@@ -2887,8 +2917,8 @@
         def __trim_older(self, fmri):
                 """Trim any fmris older than this one"""
                 reason = (N_("Newer version {0} is already installed"), (fmri,))
-                self.__trim(self.__comb_newer_fmris(fmri, dotrim=False)[1],
-                    _TRIM_INSTALLED_NEWER, reason)
+                self.__trim(self.__comb_newer_fmris(fmri, dotrim=False)[1] -
+                    self.__allowed_downgrades, _TRIM_INSTALLED_NEWER, reason)
 
         def __trim_nonmatching_variants(self, fmri):
                 """Trim packages that don't support image architecture or other
@@ -3088,6 +3118,98 @@
                 self.__trim(fmri, _TRIM_UNSUPPORTED,
                     N_("Package contains invalid or unsupported actions"))
 
+        def __get_older_incorp_pkgs(self, fmri, install_holds, excludes=EmptyI,
+            candidates=None, depth=0):
+                """Get all incorporated pkgs for the given 'fmri' whose versions
+                are older than what is currently installed in the image."""
+
+                if not candidates:
+                        candidates = set()
+
+                if fmri in self.__dg_incorp_cache:
+                        candidates |= self.__dg_incorp_cache[fmri]
+                        return candidates
+
+                if depth > 10:
+                        # Safeguard against circular dependencies.
+                        # If it happens, just end the recursion tree.
+                        return candidates
+
+                self.__dg_incorp_cache[fmri] = set()
+                self.__progress()
+
+                # Get all incorporated packages for this fmri.
+                matching_incorp_sets = [
+                    m[0]
+                    for m in
+                    self.__get_incorp_nonmatch_dict(fmri,excludes).values()
+                ]
+
+                for df in chain.from_iterable(matching_incorp_sets):
+                        if df.pkg_name not in self.__installed_dict:
+                                continue
+
+                        # Ignore pkgs which are incorporated at a higher or
+                        # the same version.
+                        inst_ver = self.__installed_dict[df.pkg_name].version
+                        if df.version.is_successor(inst_ver, None) or \
+                            df.version == inst_ver:
+                                continue
+
+                        # Do not allow implicit publisher switches
+                        if df.get_publisher() != fmri.get_publisher():
+                                continue
+
+                        # Do not allow pkgs which are marked for removal.
+                        if fmri in self.__removal_fmris:
+                                continue
+
+                        # Do not allow pkgs with install-holds but filter out
+                        # child holds
+                        install_hold = False
+                        for ha in [
+                            sa
+                            for sa in self.__get_actions(df, "set")
+                            if sa.attrs["name"] == "pkg.depend.install-hold"
+                        ]:
+                                install_hold = True
+                                for h in install_holds:
+                                        if ha.attrs["value"].startswith(h):
+                                                # This is a child hold of an
+                                                # incorporating pkg, ignore.
+                                                install_hold = False
+                                                break
+                                if not install_hold:
+                                        break
+                        if install_hold:
+                                continue
+
+                        self.__dg_incorp_cache[fmri].add(df)
+                        candidates.add(df)
+
+                        # Check if this pkgs has incorporate deps of its own.
+                        self.__get_older_incorp_pkgs(df, install_holds,
+                            excludes, candidates, depth+1)
+
+                return candidates
+
+        def __allow_incorp_downgrades(self, fmri, excludes=EmptyI):
+                """Find packages which have lower versions than installed but
+                are incorporated by a package in the proposed list."""
+
+                install_holds = set([
+                    sa.attrs["value"]
+                    for sa in self.__get_actions(fmri, "set")
+                    if sa.attrs["name"] == "pkg.depend.install-hold"
+                ])
+
+                # Get all pkgs which are incorporated by 'fmri',
+                # including nested incorps.
+                candidates = self.__get_older_incorp_pkgs(fmri, install_holds,
+                    excludes=excludes)
+
+                return candidates
+
         def __dotrim(self, fmri_list):
                 """Return fmri_list trimmed of any fmris in self.__trim_dict"""
 
--- a/src/tests/cli/t_pkg_install.py	Wed Aug 19 17:55:12 2015 -0700
+++ b/src/tests/cli/t_pkg_install.py	Thu Aug 20 13:26:55 2015 -0700
@@ -3022,7 +3022,7 @@
                 # attempt to force downgrade of package w/ older incorp
                 self.pkg("install [email protected]")
                 self.pkg("uninstall [email protected]")
-                self.pkg("install [email protected]", exit=1)
+                self.pkg("install [email protected]")
 
                 # upgrade pkg that loses incorp. deps. in new version
                 self.pkg("install [email protected]")
@@ -10888,5 +10888,170 @@
                 expected = self.reduceSpaces(expected)
                 self.assertEqualDiff(expected, output)
 
+
+class TestPkgUpdateDowngradeIncorp(pkg5unittest.ManyDepotTestCase):
+        persistent_setup = True
+
+        pkgs = (
+                """
+                    open [email protected],5.11-0
+                    close """,
+                """
+                    open [email protected],5.11-0
+                    close """,
+                """
+                    open [email protected],5.11-0
+                    add set pkg.depend.install-hold=B
+                    close """,
+                """
+                    open [email protected],5.11-0
+                    add set pkg.depend.install-hold=B
+                    close """,
+                """
+                    open [email protected],5.11-0
+                    add set pkg.depend.install-hold=parent.C
+                    close """,
+                """
+                    open [email protected],5.11-0
+                    add set pkg.depend.install-hold=parent.C
+                    close """,
+                """
+                    open [email protected],5.11-0
+                    add depend type=incorporate [email protected]
+                    close """,
+                """
+                    open [email protected],5.11-0
+                    add depend type=incorporate [email protected]
+                    close """,
+                """
+                    open [email protected],5.11-0
+                    add depend type=incorporate [email protected]
+                    close """,
+                """
+                    open [email protected],5.11-0
+                    add depend type=incorporate [email protected]
+                    close """,
+                """
+                    open [email protected],5.11-0
+                    add depend type=incorporate [email protected]
+                    close """,
+                """
+                    open [email protected],5.11-0
+                    add depend type=incorporate [email protected]
+                    close """,
+                """
+                    open [email protected],5.11-0
+                    add set pkg.depend.install-hold=parent
+                    add depend type=incorporate [email protected]
+                    add depend type=incorporate [email protected]
+                    close """,
+                """
+                    open [email protected],5.11-0
+                    add set pkg.depend.install-hold=parent
+                    add depend type=incorporate [email protected]
+                    add depend type=incorporate [email protected]
+                    close """,
+
+                """
+                    open [email protected],5.11-0
+                    add depend type=incorporate [email protected]
+                    close """,
+
+                """
+                    open [email protected],5.11-0
+                    add depend type=incorporate [email protected]
+                    close """,
+
+                """
+                    open [email protected],5.11-0
+                    close """,
+                )
+
+        pub2_pkgs = (
+                """
+                    open [email protected],5.11-0
+                    close """,
+                )
+
+        def setUp(self):
+                pkg5unittest.ManyDepotTestCase.setUp(self, ["test1", "test2"])
+                self.rurl = self.dcs[1].get_repo_url()
+                self.rurl2 = self.dcs[2].get_repo_url()
+
+                self.pkgsend_bulk(self.rurl, self.pkgs)
+                self.pkgsend_bulk(self.rurl2, self.pub2_pkgs)
+
+        def test_incorp_downgrade(self):
+                """ Test that downgrades are allowed if they are incorporated
+                by FMRIs which are requested by the user or are about to be
+                updated as part of an update-all operation."""
+
+                self.image_create(self.rurl, prefix="")
+                self.pkg("install A incorp@1")
+                self.pkg("list [email protected] [email protected]")
+
+                # When incorp moves forward, A should move backwards
+                self.pkg("update incorp@2")
+                self.pkg("list [email protected] [email protected]")
+
+                # start over and test if that also works if we do an update all
+                self.pkg("update incorp@1")
+                self.pkg("update -v")
+                self.pkg("list [email protected] [email protected]")
+
+                # prepare test for nested incorporations
+                self.pkg("uninstall incorp")
+                self.pkg("update A@2")
+                self.pkg("install parent_incorp@1 child_incorp@2")
+                self.pkg("list [email protected] child_incorp@2 parent_incorp@1")
+
+                # test update of nested incorporation supports downgrade
+                self.pkg("update -v parent_incorp@2")
+                self.pkg("list A@1 child_incorp@1 parent_incorp@2")
+
+                # prepare test for explicit downgrade of incorp
+                self.pkg("uninstall parent_incorp")
+                self.pkg("update child_incorp@2")
+                self.pkg("list A@2 child_incorp@2")
+
+                # test explicit downgrade of incorp downgrades incorp'ed pkgs
+                self.pkg("update -v child_incorp@1")
+                self.pkg("list A@1 child_incorp@1")
+
+
+        def test_incorp_downgrade_installhold(self):
+                """Test correct handling of install-holds when determining
+                which pkgs are ok to downgrade."""
+
+                # prepare test for install-hold
+                self.image_create(self.rurl, prefix="")
+                self.pkg("install ihold_incorp@2 B")
+                self.pkg("list ihold_incorp@2 B@2")
+                # test that install hold prevents downgrade
+                self.pkg("update -v ihold_incorp@1", exit=1)
+
+                # prep test for parent install-hold
+                self.pkg("install --reject B C@2")
+                self.pkg("list ihold_incorp@2 C@2")
+                # test that downgrade is allowed if install-hold is child of
+                # requested FMRI
+                self.pkg("update -v ihold_incorp@1")
+                self.pkg("list ihold_incorp@1 C@1")
+
+        def test_incorp_downgrade_pubswitch(self):
+                """Test that implicit publisher switches of incorporated pkgs
+                are not allowed."""
+
+                # prepare test for publisher switch
+                self.image_create(self.rurl, prefix="")
+                self.pkg("set-publisher --non-sticky test1")
+                self.pkg("set-publisher --non-sticky -p {0}".format(self.rurl2))
+                self.pkg("list -af")
+                self.pkg("install p_incorp@1 D@2")
+                self.pkg("list p_incorp@1 D@2")
+
+                self.pkg("update p_incorp@2", exit=1)
+
+
 if __name__ == "__main__":
         unittest.main()
--- a/src/tests/cli/t_pkg_linked.py	Wed Aug 19 17:55:12 2015 -0700
+++ b/src/tests/cli/t_pkg_linked.py	Thu Aug 20 13:26:55 2015 -0700
@@ -2501,6 +2501,66 @@
                 self._pkg([2], "list network", rv=EXIT_OOPS)
 
 
+class TestPkgLinkedIncorpDowngrade(TestPkgLinked):
+        """Test that incorporated pkgs can be downgraded if incorporation is
+        updated."""
+
+        pkgs = (
+                """
+                    open [email protected],5.11-0.1
+                    add depend type=incorporate fmri=A@2
+                    add depend type=parent fmri=%s
+                    close """ % pkg.actions.depend.DEPEND_SELF,
+                """
+                    open [email protected],5.11-0.1
+                    add depend type=incorporate fmri=A@1
+                    add depend type=parent fmri=%s
+                    close """ % pkg.actions.depend.DEPEND_SELF,
+                """
+                    open [email protected],5.11-0.1
+                    add depend type=require fmri=pkg:/incorp
+                    close """,
+                """
+                    open [email protected],5.11-0.1
+                    add depend type=require fmri=pkg:/incorp
+                    close """,
+        )
+
+        def setUp(self):
+                self.i_count = 3
+                pkg5unittest.ManyDepotTestCase.setUp(self, ["test"],
+                    image_count=self.i_count)
+
+                # get repo url
+                self.rurl1 = self.dcs[1].get_repo_url()
+
+                # setup image names and paths
+                self.i_name = []
+                self.i_path = []
+                self.i_api = []
+                self.i_api_reset = []
+                for i in range(self.i_count):
+                        name = "system:img%d" % i
+                        self.i_name.insert(i, name)
+                        self.i_path.insert(i, self.img_path(i))
+
+                self.pkgsend_bulk(self.rurl1, self.pkgs)
+
+
+        def test_incorp_downgrade(self):
+                """Test that incorporated pkgs can be downgraded if
+                incorporation is updated."""
+
+                # create parent (0), push child (1, 2)
+                self._imgs_create(3)
+                self._attach_child(0, [1, 2])
+
+                self._pkg([0, 1, 2], "install -v incorp@1 A")
+                self._pkg([0, 1, 2], "list A@2")
+                self._pkg([0], "update -v incorp@2")
+                self._pkg([0, 1, 2], "list A@1")
+
+
 class TestFacetInheritance(TestPkgLinked):
         """Class to test facet inheritance between images.