6666and their properties (available groups, baseuri. gpg key). This class can add, disable, or
6767manipulate repositories. Items can be retrieved as `DebianRepository` objects.
6868
69- In order add a new repository with explicit details for fields, a new `DebianRepository` can
70- be added to `RepositoryMapping`
69+ In order to add a new repository with explicit details for fields, a new `DebianRepository`
70+ can be added to `RepositoryMapping`
7171
7272`RepositoryMapping` provides an abstraction around the existing repositories on the system,
7373and can be accessed and iterated over like any `Mapping` object, to retrieve values by key,
9898 repo = DebianRepository.from_repo_line(line)
9999 repositories.add(repo)
100100```
101+
102+ Dependencies:
103+ Note that this module requires `opentelemetry-api`, which is already included into
104+ your charm's virtual environment via `ops >= 2.21`.
101105"""
102106
103107from __future__ import annotations
114118from typing import Any , Iterable , Iterator , Literal , Mapping
115119from urllib .parse import urlparse
116120
121+ import opentelemetry .trace
122+
117123logger = logging .getLogger (__name__ )
124+ tracer = opentelemetry .trace .get_tracer (__name__ )
118125
119126# The unique Charmhub library identifier, never change it
120127LIBID = "7c3dbc9c2ad44a47bd6fcb25caa270e5"
124131
125132# Increment this PATCH version before using `charmcraft publish-lib` or reset
126133# to 0 if you are raising the major API version
127- LIBPATCH = 17
134+ LIBPATCH = 18
135+
136+ PYDEPS = ["opentelemetry-api" ]
128137
129138
130139VALID_SOURCE_TYPES = ("deb" , "deb-src" )
@@ -249,7 +258,9 @@ def _apt(
249258 try :
250259 env = os .environ .copy ()
251260 env ["DEBIAN_FRONTEND" ] = "noninteractive"
252- subprocess .run (_cmd , capture_output = True , check = True , text = True , env = env )
261+ with tracer .start_as_current_span (_cmd [0 ]) as span :
262+ span .set_attribute ("argv" , _cmd )
263+ subprocess .run (_cmd , capture_output = True , check = True , text = True , env = env )
253264 except CalledProcessError as e :
254265 raise PackageError (
255266 f"Could not { command } package(s) { package_names } : { e .stderr } "
@@ -464,18 +475,20 @@ def from_apt_cache(
464475 arch: an optional architecture, defaulting to `dpkg --print-architecture`.
465476 If an architecture is not specified, this will be used for selection.
466477 """
467- system_arch = check_output (
468- ["dpkg" , "--print-architecture" ], universal_newlines = True
469- ).strip ()
478+ cmd = ["dpkg" , "--print-architecture" ]
479+ with tracer .start_as_current_span (cmd [0 ]) as span :
480+ span .set_attribute ("argv" , cmd )
481+ system_arch = check_output (cmd , universal_newlines = True ).strip ()
470482 arch = arch if arch else system_arch
471483
472484 # Regexps are a really terrible way to do this. Thanks dpkg
473485 keys = ("Package" , "Architecture" , "Version" )
474486
487+ cmd = ["apt-cache" , "show" , package ]
475488 try :
476- output = check_output (
477- [ "apt-cache " , "show" , package ], stderr = PIPE , universal_newlines = True
478- )
489+ with tracer . start_as_current_span ( cmd [ 0 ]) as span :
490+ span . set_attribute ( "argv " , cmd )
491+ output = check_output ( cmd , stderr = PIPE , universal_newlines = True )
479492 except CalledProcessError as e :
480493 raise PackageError (f"Could not list packages in apt-cache: { e .stderr } " ) from None
481494
@@ -880,7 +893,9 @@ def update() -> None:
880893 """Update the apt cache via `apt-get update`."""
881894 cmd = ["apt-get" , "update" , "--error-on=any" ]
882895 try :
883- subprocess .run (cmd , capture_output = True , check = True )
896+ with tracer .start_as_current_span (cmd [0 ]) as span :
897+ span .set_attribute ("argv" , cmd )
898+ subprocess .run (cmd , capture_output = True , check = True )
884899 except CalledProcessError as e :
885900 logger .error (
886901 "%s:\n stdout:\n %s\n stderr:\n %s" ,
@@ -1107,12 +1122,14 @@ def disable(self) -> None:
11071122 " Please raise an issue if you require this feature."
11081123 )
11091124 searcher = f"{ self .repotype } { self .make_options_string ()} { self .uri } { self .release } "
1110- with fileinput .input (self ._filename , inplace = True ) as lines :
1111- for line in lines :
1112- if re .match (rf"^{ re .escape (searcher )} \s" , line ):
1113- print (f"# { line } " , end = "" )
1114- else :
1115- print (line , end = "" )
1125+ with tracer .start_as_current_span ("disable source" ) as span :
1126+ span .set_attribute ("filename" , self ._filename )
1127+ with fileinput .input (self ._filename , inplace = True ) as lines :
1128+ for line in lines :
1129+ if re .match (rf"^{ re .escape (searcher )} \s" , line ):
1130+ print (f"# { line } " , end = "" )
1131+ else :
1132+ print (line , end = "" )
11161133
11171134 def import_key (self , key : str ) -> None :
11181135 """Import an ASCII Armor key.
@@ -1145,8 +1162,10 @@ def _get_keyid_by_gpg_key(key_material: bytes) -> str:
11451162 """
11461163 # Use the same gpg command for both Xenial and Bionic
11471164 cmd = ["gpg" , "--with-colons" , "--with-fingerprint" ]
1148- ps = subprocess .run (cmd , capture_output = True , input = key_material )
1149- out , err = ps .stdout .decode (), ps .stderr .decode ()
1165+ with tracer .start_as_current_span (cmd [0 ]) as span :
1166+ span .set_attribute ("argv" , cmd )
1167+ ps = subprocess .run (cmd , capture_output = True , input = key_material )
1168+ out , err = ps .stdout .decode (), ps .stderr .decode ()
11501169 if "gpg: no valid OpenPGP data found." in err :
11511170 raise GPGKeyError ("Invalid GPG key material provided" )
11521171 # from gnupg2 docs: fpr :: Fingerprint (fingerprint is in field 10)
@@ -1191,8 +1210,10 @@ def _get_key_by_keyid(keyid: str) -> str:
11911210 "https://keyserver.ubuntu.com" "/pks/lookup?op=get&options=mr&exact=on&search=0x{}"
11921211 )
11931212 curl_cmd = ["curl" , keyserver_url .format (keyid )]
1194- # use proxy server settings in order to retrieve the key
1195- return check_output (curl_cmd ).decode ()
1213+ with tracer .start_as_current_span (curl_cmd [0 ]) as span :
1214+ span .set_attribute ("argv" , curl_cmd )
1215+ # use proxy server settings in order to retrieve the key
1216+ return check_output (curl_cmd ).decode ()
11961217
11971218 @staticmethod
11981219 def _dearmor_gpg_key (key_asc : bytes ) -> bytes :
@@ -1207,8 +1228,11 @@ def _dearmor_gpg_key(key_asc: bytes) -> bytes:
12071228 Raises:
12081229 GPGKeyError
12091230 """
1210- ps = subprocess .run (["gpg" , "--dearmor" ], capture_output = True , input = key_asc )
1211- out , err = ps .stdout , ps .stderr .decode ()
1231+ cmd = ["gpg" , "--dearmor" ]
1232+ with tracer .start_as_current_span (cmd [0 ]) as span :
1233+ span .set_attribute ("argv" , cmd )
1234+ ps = subprocess .run (cmd , capture_output = True , input = key_asc )
1235+ out , err = ps .stdout , ps .stderr .decode ()
12121236 if "gpg: no valid OpenPGP data found." in err :
12131237 raise GPGKeyError (
12141238 "Invalid GPG key material. Check your network setup"
@@ -1289,11 +1313,12 @@ def __init__(self):
12891313 if not os .path .isfile (default_sources ):
12901314 raise
12911315
1292- # read sources.list.d
1293- for file in glob .iglob (os .path .join (sources_dir , "*.list" )):
1294- self .load (file )
1295- for file in glob .iglob (os .path .join (sources_dir , "*.sources" )):
1296- self .load_deb822 (file )
1316+ with tracer .start_as_current_span ("load sources" ):
1317+ # read sources.list.d
1318+ for file in glob .iglob (os .path .join (sources_dir , "*.list" )):
1319+ self .load (file )
1320+ for file in glob .iglob (os .path .join (sources_dir , "*.sources" )):
1321+ self .load_deb822 (file )
12971322
12981323 def __contains__ (self , key : Any ) -> bool :
12991324 """Magic method for checking presence of repo in mapping.
@@ -1533,7 +1558,9 @@ def _add_repository(
15331558 cmd .append ("--no-update" )
15341559 logger .info ("%s" , cmd )
15351560 try :
1536- subprocess .run (cmd , check = True , capture_output = True )
1561+ with tracer .start_as_current_span (cmd [0 ]) as span :
1562+ span .set_attribute ("argv" , cmd )
1563+ subprocess .run (cmd , check = True , capture_output = True )
15371564 except CalledProcessError as e :
15381565 logger .error (
15391566 "subprocess.run(%s):\n stdout:\n %s\n stderr:\n %s" ,
0 commit comments