Skip to content

Commit

Permalink
feat(apt): add candidate_version argument to add_package
Browse files Browse the repository at this point in the history
Force to install package with candidate version which get from apt-cache policy output
  • Loading branch information
jneo8 committed Dec 18, 2023
1 parent 405948a commit c5a8fea
Show file tree
Hide file tree
Showing 2 changed files with 204 additions and 0 deletions.
31 changes: 31 additions & 0 deletions lib/charms/operator_libs_linux/v0/apt.py
Original file line number Diff line number Diff line change
Expand Up @@ -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<version>(.*))")
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.
Expand All @@ -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
Expand All @@ -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)
Expand Down
173 changes: 173 additions & 0 deletions tests/unit/test_apt.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 <[email protected]>
Original-Maintainer: Fabio Fantoni <[email protected]>
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 <[email protected]>
Original-Maintainer: Bernd Zeimetz <[email protected]>
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")
Expand Down Expand Up @@ -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("<charms.operator_libs_linux.v0.apt.PackageError>", 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(
"<charms.operator_libs_linux.v0.apt.PackageNotFoundError>", 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)

0 comments on commit c5a8fea

Please sign in to comment.