Skip to content
This repository was archived by the owner on Mar 27, 2026. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 56 additions & 29 deletions lib/charms/operator_libs_linux/v0/apt.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,8 +66,8 @@
and their properties (available groups, baseuri. gpg key). This class can add, disable, or
manipulate repositories. Items can be retrieved as `DebianRepository` objects.

In order add a new repository with explicit details for fields, a new `DebianRepository` can
be added to `RepositoryMapping`
In order to add a new repository with explicit details for fields, a new `DebianRepository`
can be added to `RepositoryMapping`

`RepositoryMapping` provides an abstraction around the existing repositories on the system,
and can be accessed and iterated over like any `Mapping` object, to retrieve values by key,
Expand Down Expand Up @@ -98,6 +98,10 @@
repo = DebianRepository.from_repo_line(line)
repositories.add(repo)
```

Dependencies:
Note that this module requires `opentelemetry-api`, which is already included into
your charm's virtual environment via `ops >= 2.21`.
"""

from __future__ import annotations
Expand All @@ -114,7 +118,10 @@
from typing import Any, Iterable, Iterator, Literal, Mapping
from urllib.parse import urlparse

import opentelemetry.trace

logger = logging.getLogger(__name__)
tracer = opentelemetry.trace.get_tracer(__name__)

# The unique Charmhub library identifier, never change it
LIBID = "7c3dbc9c2ad44a47bd6fcb25caa270e5"
Expand All @@ -124,7 +131,9 @@

# Increment this PATCH version before using `charmcraft publish-lib` or reset
# to 0 if you are raising the major API version
LIBPATCH = 17
LIBPATCH = 18

PYDEPS = ["opentelemetry-api"]
Comment thread
dimaqq marked this conversation as resolved.


VALID_SOURCE_TYPES = ("deb", "deb-src")
Expand Down Expand Up @@ -249,7 +258,9 @@ def _apt(
try:
env = os.environ.copy()
env["DEBIAN_FRONTEND"] = "noninteractive"
subprocess.run(_cmd, capture_output=True, check=True, text=True, env=env)
with tracer.start_as_current_span(_cmd[0]) as span:
span.set_attribute("argv", _cmd)
subprocess.run(_cmd, capture_output=True, check=True, text=True, env=env)
except CalledProcessError as e:
raise PackageError(
f"Could not {command} package(s) {package_names}: {e.stderr}"
Expand Down Expand Up @@ -464,18 +475,20 @@ def from_apt_cache(
arch: an optional architecture, defaulting to `dpkg --print-architecture`.
If an architecture is not specified, this will be used for selection.
"""
system_arch = check_output(
["dpkg", "--print-architecture"], universal_newlines=True
).strip()
cmd = ["dpkg", "--print-architecture"]
with tracer.start_as_current_span(cmd[0]) as span:
span.set_attribute("argv", cmd)
system_arch = check_output(cmd, universal_newlines=True).strip()
arch = arch if arch else system_arch

# Regexps are a really terrible way to do this. Thanks dpkg
keys = ("Package", "Architecture", "Version")

cmd = ["apt-cache", "show", package]
try:
output = check_output(
["apt-cache", "show", package], stderr=PIPE, universal_newlines=True
)
with tracer.start_as_current_span(cmd[0]) as span:
span.set_attribute("argv", cmd)
output = check_output(cmd, stderr=PIPE, universal_newlines=True)
except CalledProcessError as e:
raise PackageError(f"Could not list packages in apt-cache: {e.stderr}") from None

Expand Down Expand Up @@ -880,7 +893,9 @@ def update() -> None:
"""Update the apt cache via `apt-get update`."""
cmd = ["apt-get", "update", "--error-on=any"]
try:
subprocess.run(cmd, capture_output=True, check=True)
with tracer.start_as_current_span(cmd[0]) as span:
span.set_attribute("argv", cmd)
subprocess.run(cmd, capture_output=True, check=True)
except CalledProcessError as e:
logger.error(
"%s:\nstdout:\n%s\nstderr:\n%s",
Expand Down Expand Up @@ -1107,12 +1122,14 @@ def disable(self) -> None:
" Please raise an issue if you require this feature."
)
searcher = f"{self.repotype} {self.make_options_string()}{self.uri} {self.release}"
with fileinput.input(self._filename, inplace=True) as lines:
for line in lines:
if re.match(rf"^{re.escape(searcher)}\s", line):
print(f"# {line}", end="")
else:
print(line, end="")
with tracer.start_as_current_span("disable source") as span:
Comment thread
tonyandrewmeyer marked this conversation as resolved.
span.set_attribute("filename", self._filename)
with fileinput.input(self._filename, inplace=True) as lines:
for line in lines:
if re.match(rf"^{re.escape(searcher)}\s", line):
print(f"# {line}", end="")
else:
print(line, end="")

def import_key(self, key: str) -> None:
"""Import an ASCII Armor key.
Expand Down Expand Up @@ -1145,8 +1162,10 @@ def _get_keyid_by_gpg_key(key_material: bytes) -> str:
"""
# Use the same gpg command for both Xenial and Bionic
cmd = ["gpg", "--with-colons", "--with-fingerprint"]
ps = subprocess.run(cmd, capture_output=True, input=key_material)
out, err = ps.stdout.decode(), ps.stderr.decode()
with tracer.start_as_current_span(cmd[0]) as span:
span.set_attribute("argv", cmd)
ps = subprocess.run(cmd, capture_output=True, input=key_material)
out, err = ps.stdout.decode(), ps.stderr.decode()
if "gpg: no valid OpenPGP data found." in err:
raise GPGKeyError("Invalid GPG key material provided")
# from gnupg2 docs: fpr :: Fingerprint (fingerprint is in field 10)
Expand Down Expand Up @@ -1191,8 +1210,10 @@ def _get_key_by_keyid(keyid: str) -> str:
"https://keyserver.ubuntu.com" "/pks/lookup?op=get&options=mr&exact=on&search=0x{}"
)
curl_cmd = ["curl", keyserver_url.format(keyid)]
# use proxy server settings in order to retrieve the key
return check_output(curl_cmd).decode()
with tracer.start_as_current_span(curl_cmd[0]) as span:
span.set_attribute("argv", curl_cmd)
Comment thread
tonyandrewmeyer marked this conversation as resolved.
# use proxy server settings in order to retrieve the key
return check_output(curl_cmd).decode()

@staticmethod
def _dearmor_gpg_key(key_asc: bytes) -> bytes:
Expand All @@ -1207,8 +1228,11 @@ def _dearmor_gpg_key(key_asc: bytes) -> bytes:
Raises:
GPGKeyError
"""
ps = subprocess.run(["gpg", "--dearmor"], capture_output=True, input=key_asc)
out, err = ps.stdout, ps.stderr.decode()
cmd = ["gpg", "--dearmor"]
with tracer.start_as_current_span(cmd[0]) as span:
span.set_attribute("argv", cmd)
ps = subprocess.run(cmd, capture_output=True, input=key_asc)
out, err = ps.stdout, ps.stderr.decode()
if "gpg: no valid OpenPGP data found." in err:
raise GPGKeyError(
"Invalid GPG key material. Check your network setup"
Expand Down Expand Up @@ -1289,11 +1313,12 @@ def __init__(self):
if not os.path.isfile(default_sources):
raise

# read sources.list.d
for file in glob.iglob(os.path.join(sources_dir, "*.list")):
self.load(file)
for file in glob.iglob(os.path.join(sources_dir, "*.sources")):
self.load_deb822(file)
with tracer.start_as_current_span("load sources"):
Comment thread
tonyandrewmeyer marked this conversation as resolved.
# read sources.list.d
for file in glob.iglob(os.path.join(sources_dir, "*.list")):
self.load(file)
for file in glob.iglob(os.path.join(sources_dir, "*.sources")):
self.load_deb822(file)

def __contains__(self, key: Any) -> bool:
"""Magic method for checking presence of repo in mapping.
Expand Down Expand Up @@ -1533,7 +1558,9 @@ def _add_repository(
cmd.append("--no-update")
logger.info("%s", cmd)
try:
subprocess.run(cmd, check=True, capture_output=True)
with tracer.start_as_current_span(cmd[0]) as span:
span.set_attribute("argv", cmd)
subprocess.run(cmd, check=True, capture_output=True)
except CalledProcessError as e:
logger.error(
"subprocess.run(%s):\nstdout:\n%s\nstderr:\n%s",
Expand Down
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
# Copyright 2021 Canonical Ltd.
# See LICENSE file for licensing details.
ops
opentelemetry-api
3 changes: 3 additions & 0 deletions tox.ini
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ description = Run static type checker
allowlist_externals = echo
deps =
pyright==1.1.385
-r requirements.txt
commands =
#pyright {[vars]lib_dir} {posargs} # FIXME: enable static analysis of all libs
#pyright {[vars]test_dir} {posargs} # FIXME: enable static analysis of tests
Expand Down Expand Up @@ -120,6 +121,7 @@ commands_post =
description = Run integration tests for Ubuntu instance.
deps =
pytest
-r requirements.txt
commands =
pytest --ignore={[vars]tst_dir}unit \
--ignore={[vars]tst_dir}integration/test_dnf.py \
Expand All @@ -134,6 +136,7 @@ commands =
description = Run integration tests for CentOS instance.
deps =
pytest
-r requirements.txt
commands =
pytest --log-cli-level=INFO \
--tb native \
Expand Down