From c5a8fea4f87cccfa562ff1aaa133025293ad9b0c Mon Sep 17 00:00:00 2001 From: jneo8 Date: Mon, 18 Dec 2023 15:38:03 +0800 Subject: [PATCH] feat(apt): add candidate_version argument to add_package Force to install package with candidate version which get from apt-cache policy output --- lib/charms/operator_libs_linux/v0/apt.py | 31 ++++ tests/unit/test_apt.py | 173 +++++++++++++++++++++++ 2 files changed, 204 insertions(+) diff --git a/lib/charms/operator_libs_linux/v0/apt.py b/lib/charms/operator_libs_linux/v0/apt.py index c3a2329e..f1a3749f 100644 --- a/lib/charms/operator_libs_linux/v0/apt.py +++ b/lib/charms/operator_libs_linux/v0/apt.py @@ -721,11 +721,39 @@ def __ne__(self, other) -> bool: return not self.__eq__(other) +def get_candidate_version(package: str) -> Optional[str]: + """Get candiate version of package from apt-cache. + + Args: + package: package name + Returns: + A version string + Raises: + PackageError if fail to use apt-cache policy command + PackageNotFoundError if fail to get Candidate version + """ + try: + output = check_output( + ["apt-cache", "policy", package], stderr=PIPE, universal_newlines=True + ) + except CalledProcessError as e: + raise PackageError(f"Could not list packages in apt-cache: {e.output}") from None + + lines = [line.strip() for line in output.strip().split("\n")] + for line in lines: + candidate_matcher = re.compile(r"^Candidate:\s(?P(.*))") + matches = candidate_matcher.search(line) + if matches: + return matches.groupdict().get("version") + raise PackageNotFoundError(f"Could not find candidate version package in apt-cache: {output}") + + def add_package( package_names: Union[str, List[str]], version: Optional[str] = "", arch: Optional[str] = "", update_cache: Optional[bool] = False, + candidate_version: bool = False, ) -> Union[DebianPackage, List[DebianPackage]]: """Add a package or list of packages to the system. @@ -735,6 +763,7 @@ def add_package( version: an (Optional) version as a string. Defaults to the latest known arch: an optional architecture for the package update_cache: whether or not to run `apt-get update` prior to operating + candidate_version: whether or not to use `apt-cache policy` to get and install candidate version Raises: TypeError if no package name is given, or explicit version is set for multiple packages @@ -758,6 +787,8 @@ def add_package( ) for p in package_names: + if candidate_version: + version = get_candidate_version(p) pkg, success = _add(p, version, arch) if success: packages["success"].append(pkg) diff --git a/tests/unit/test_apt.py b/tests/unit/test_apt.py index 2cf3e9ca..b98e5cc9 100644 --- a/tests/unit/test_apt.py +++ b/tests/unit/test_apt.py @@ -172,6 +172,113 @@ Description-md5: e7f99df3aa92cf870d335784e155ec33 """ +apt_cache_freeipmi_tools = """ +Package: freeipmi-tools +Architecture: amd64 +Version: 1.6.9-2~bpo20.04.1 +Priority: extra +Section: admin +Source: freeipmi +Origin: Ubuntu +Maintainer: Ubuntu Developers +Original-Maintainer: Fabio Fantoni +Bugs: https://bugs.launchpad.net/ubuntu/+filebug +Installed-Size: 3102 +Depends: freeipmi-common (= 1.6.9-2~bpo20.04.1), libc6 (>= 2.15), libfreeipmi17 (>= 1.6.2), libipmiconsole2 (>= 1.4.4), libipmidetect0 (>= 1.1.5) +Suggests: freeipmi-bmc-watchdog, freeipmi-ipmidetect +Filename: pool/main/f/freeipmi/freeipmi-tools_1.6.9-2~bpo20.04.1_amd64.deb +Size: 637216 +MD5sum: fa4105fb6b0fb48969d56f005c7d32e8 +SHA1: 4625f8601a3af2e787389a30b7c5b8027c908cad +SHA256: 247667a2835c5e775a9f68ec12e27f4a01c30dfd6a6306c29f10a3b52a255947 +SHA512: b553c00327ec3304a0249ba238bbbe226fd293af6f3fc5aeb52b7ee3f90a8216d316e34ea4e8c69b9781beb9a746fc210fee34186d96202251439c33302b24db +Homepage: https://www.gnu.org/software/freeipmi/ +Description-en: GNU implementation of the IPMI protocol - tools + FreeIPMI is a collection of Intelligent Platform Management IPMI + system software. It provides in-band and out-of-band software and a + development library conforming to the Intelligent Platform Management + Interface (IPMI v1.5 and v2.0) standards. + . + This package contains assorted IPMI-related tools: + * bmc-config - configure BMC values + * bmc-info - display BMC information + * ipmi-chassis - IPMI chassis management utility + * ipmi-fru - display FRU information + * ipmi-locate - IPMI probing utility + * ipmi-oem - IPMI OEM utility + * ipmi-pet - decode Platform Event Traps + * ipmi-raw - IPMI raw communication utility + * ipmi-sel - display SEL entries + * ipmi-sensors - display IPMI sensor information + * ipmi-sensors-config - configure sensors + * ipmiconsole - IPMI console utility + * ipmiping - send IPMI Get Authentication Capability request + * ipmipower - IPMI power control utility + * pef-config - configure PEF values + * rmcpping - send RMCP Ping to network hosts +Description-md5: 6752c6921b38f7d4192531a8ab33783c + + +Package: freeipmi-tools +Architecture: amd64 +Version: 1.6.4-3ubuntu1.1 +Priority: extra +Section: admin +Source: freeipmi +Origin: Ubuntu +Maintainer: Ubuntu Developers +Original-Maintainer: Bernd Zeimetz +Bugs: https://bugs.launchpad.net/ubuntu/+filebug +Installed-Size: 3099 +Depends: libc6 (>= 2.15), libfreeipmi17 (>= 1.6.2), libipmiconsole2 (>= 1.4.4), libipmidetect0 (>= 1.1.5), freeipmi-common (= 1.6.4-3ubuntu1.1) +Suggests: freeipmi-ipmidetect, freeipmi-bmc-watchdog +Filename: pool/main/f/freeipmi/freeipmi-tools_1.6.4-3ubuntu1.1_amd64.deb +Size: 636384 +MD5sum: bc7c1ec3484d07d3627ba92bb0300693 +SHA1: b5851b2160d5139d141e3c1b29b946f6fd895871 +SHA256: 6d0a643fcb62404b17d7574baf854b9b39443a2f0067c2076f5f663437d39968 +SHA512: 8f89796c86a8a410c71996d8fb229293492c949a664476e0fa63fd3fe7b1523a3174997172fe82f6eed6fb59c0a4fde221273d6149b45d4e18da7e99faed02d6 +Homepage: http://www.gnu.org/software/freeipmi/ +Description-en: GNU implementation of the IPMI protocol - tools + FreeIPMI is a collection of Intelligent Platform Management IPMI + system software. It provides in-band and out-of-band software and a + development library conforming to the Intelligent Platform Management + Interface (IPMI v1.5 and v2.0) standards. + . + This package contains assorted IPMI-related tools: + * bmc-config - configure BMC values + * bmc-info - display BMC information + * ipmi-chassis - IPMI chassis management utility + * ipmi-fru - display FRU information + * ipmi-locate - IPMI probing utility + * ipmi-oem - IPMI OEM utility + * ipmi-pet - decode Platform Event Traps + * ipmi-raw - IPMI raw communication utility + * ipmi-sel - display SEL entries + * ipmi-sensors - display IPMI sensor information + * ipmi-sensors-config - configure sensors + * ipmiconsole - IPMI console utility + * ipmiping - send IPMI Get Authentication Capability request + * ipmipower - IPMI power control utility + * pef-config - configure PEF values + * rmcpping - send RMCP Ping to network hosts +Description-md5: 6752c6921b38f7d4192531a8ab33783c +""" + + +apt_cache_policy_freeipmi_tools_focal = """ +freeipmi-tools: + Installed: (none) + Candidate: 1.6.4-3ubuntu1.1 + Version table: + 1.6.9-2~bpo20.04.1 100 + 100 http://archive.ubuntu.com/ubuntu focal-backports/main amd64 Packages + 1.6.4-3ubuntu1.1 500 + 500 http://archive.ubuntu.com/ubuntu focal-updates/main amd64 Packages + 1.6.4-3ubuntu1 500 + 500 http://archive.ubuntu.com/ubuntu focal/main amd64 Packages +""" + class TestApt(unittest.TestCase): @patch("charms.operator_libs_linux.v0.apt.check_output") @@ -510,3 +617,69 @@ def test_remove_package_not_installed(self, mock_subprocess, mock_subprocess_out packages = apt.remove_package("ubuntu-advantage-tools") mock_subprocess.assert_not_called() self.assertEqual(packages, []) + + @patch("charms.operator_libs_linux.v0.apt.check_output") + def test_get_candidate_version(self, mock_subprocess_output): + mock_subprocess_output.return_value = apt_cache_policy_freeipmi_tools_focal + + version = apt.get_candidate_version("freeipmi_tools") + self.assertEqual(version, "1.6.4-3ubuntu1.1") + + @patch("charms.operator_libs_linux.v0.apt.check_output") + def test_get_candidate_version_package_not_found_error(self, mock_subprocess_output): + mock_subprocess_output.side_effect = subprocess.CalledProcessError( + returncode=-1, cmd=["apt-cache", "policy", "fake_package_name"] + ) + + with self.assertRaises(apt.PackageError) as ctx: + apt.get_candidate_version("fake_package_name") + + self.assertEqual("", ctx.exception.name) + self.assertIn("Could not list packages in apt-cache:", ctx.exception.message) + + @patch("charms.operator_libs_linux.v0.apt.check_output") + def test_get_candidate_version_can_not_found_candidate(self, mock_subprocess_output): + output = apt_cache_policy_freeipmi_tools_focal.replace("Candidate", "candidate") + mock_subprocess_output.return_value = output + with self.assertRaises(apt.PackageNotFoundError) as ctx: + apt.get_candidate_version("freeipmi_tools") + + self.assertEqual( + "", ctx.exception.name + ) + self.assertIn( + f"Could not find candidate version package in apt-cache: {output}", + ctx.exception.message, + ) + + @patch("charms.operator_libs_linux.v0.apt.check_output") + @patch("charms.operator_libs_linux.v0.apt.subprocess.run") + @patch("os.environ.copy") + def test_can_run_bare_changes_on_single_package_with_candidate_version( + self, mock_environ, mock_subprocess, mock_subprocess_output + ): + mock_subprocess.return_value = 0 + mock_subprocess_output.side_effect = [ + apt_cache_policy_freeipmi_tools_focal, + "amd64", + subprocess.CalledProcessError(returncode=100, cmd=["dpkg", "-l", "freeipmi-tools"]), + "amd64", + apt_cache_freeipmi_tools, + ] + mock_environ.return_value = {} + + # foo = apt.add_package("freeipmi_tools", candidate_version=True) + foo = apt.add_package("freeipmi_tools", candidate_version=True) + mock_subprocess.assert_called_with( + [ + "apt-get", + "-y", + "--option=Dpkg::Options::=--force-confold", + "install", + "freeipmi-tools=1.6.4-3ubuntu1.1", + ], + capture_output=True, + check=True, + env={"DEBIAN_FRONTEND": "noninteractive"}, + ) + self.assertEqual(foo.present, True)