--- 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.