diff --git a/lib/charms/operator_libs_linux/v0/apt.py b/lib/charms/operator_libs_linux/v0/apt.py index 27ab9390..e2c91398 100644 --- a/lib/charms/operator_libs_linux/v0/apt.py +++ b/lib/charms/operator_libs_linux/v0/apt.py @@ -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, @@ -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 @@ -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" @@ -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"] VALID_SOURCE_TYPES = ("deb", "deb-src") @@ -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}" @@ -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 @@ -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", @@ -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: + 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. @@ -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) @@ -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) + # 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: @@ -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" @@ -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"): + # 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. @@ -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", diff --git a/requirements.txt b/requirements.txt index adf59d00..d8fa6e86 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,4 @@ # Copyright 2021 Canonical Ltd. # See LICENSE file for licensing details. ops +opentelemetry-api diff --git a/tox.ini b/tox.ini index a4cde9c3..a4c516cd 100644 --- a/tox.ini +++ b/tox.ini @@ -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 @@ -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 \ @@ -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 \