1212# See the License for the specific language governing permissions and
1313# limitations under the License.
1414
15- """Representations of the system's Snaps, and abstractions around managing them.
15+ """Legacy Charmhub-hosted snap library, deprecated in favour of ``charmlibs.snap``.
16+
17+ WARNING: This library is deprecated and will no longer receive feature updates or bugfixes.
18+ ``charmlibs.snap`` version 1.0 is a bug-for-bug compatible migration of this library.
19+ Add 'charmlibs-snap~=1.0' to your charm's dependencies, and remove this Charmhub-hosted library.
20+ Then replace `from charms.operator_libs_linux.v2 import snap` with `from charmlibs import snap`.
21+ Read more:
22+ - https://documentation.ubuntu.com/charmlibs
23+ - https://pypi.org/project/charmlibs-snap
24+
25+ ---
26+
27+ Representations of the system's Snaps, and abstractions around managing them.
1628
1729The `snap` module provides convenience methods for listing, installing, refreshing, and removing
1830Snap packages, in addition to setting and getting configuration options for them.
5466except snap.SnapError as e:
5567 logger.error("An exception occurred when installing snaps. Reason: %s" % e.message)
5668```
69+
70+ Dependencies:
71+ Note that this module requires `opentelemetry-api`, which is already included into
72+ your charm's virtual environment via `ops >= 2.21`.
5773"""
5874
5975from __future__ import annotations
85101 TypeVar ,
86102)
87103
104+ import opentelemetry .trace
105+
88106if typing .TYPE_CHECKING :
89107 # avoid typing_extensions import at runtime
90- from typing_extensions import NotRequired , ParamSpec , Required , TypeAlias , Unpack
108+ from typing_extensions import NotRequired , ParamSpec , Required , Self , TypeAlias , Unpack
91109
92110 _P = ParamSpec ("_P" )
93111 _T = TypeVar ("_T" )
94112
95113logger = logging .getLogger (__name__ )
114+ tracer = opentelemetry .trace .get_tracer (__name__ )
96115
97116# The unique Charmhub library identifier, never change it
98117LIBID = "05394e5893f94f2d90feb7cbe6b633cd"
102121
103122# Increment this PATCH version before using `charmcraft publish-lib` or reset
104123# to 0 if you are raising the major API version
105- LIBPATCH = 10
124+ LIBPATCH = 15
125+
126+ PYDEPS = ["opentelemetry-api" ]
106127
107128
108129# Regex to locate 7-bit C1 ANSI sequences
@@ -140,6 +161,7 @@ class _SnapDict(TypedDict, total=True):
140161 name : str
141162 channel : str
142163 revision : str
164+ version : str
143165 confinement : str
144166 apps : NotRequired [list [dict [str , JSONType ]] | None ]
145167
@@ -268,6 +290,24 @@ class SnapState(Enum):
268290class SnapError (Error ):
269291 """Raised when there's an error running snap control commands."""
270292
293+ @classmethod
294+ def _from_called_process_error (cls , msg : str , error : CalledProcessError ) -> Self :
295+ lines = [msg ]
296+ if error .stdout :
297+ lines .extend (['Stdout:' , error .stdout ])
298+ if error .stderr :
299+ lines .extend (['Stderr:' , error .stderr ])
300+ try :
301+ cmd = ['journalctl' , '--unit' , 'snapd' , '--lines' , '20' ]
302+ with tracer .start_as_current_span (cmd [0 ]) as span :
303+ span .set_attribute ("argv" , cmd )
304+ logs = subprocess .check_output (cmd , text = True )
305+ except Exception as e :
306+ lines .extend (['Error fetching logs:' , str (e )])
307+ else :
308+ lines .extend (['Latest logs:' , logs ])
309+ return cls ('\n ' .join (lines ))
310+
271311
272312class SnapNotFoundError (Error ):
273313 """Raised when a requested snap is not known to the system."""
@@ -282,6 +322,7 @@ class Snap:
282322 - channel: "stable", "candidate", "beta", and "edge" are common
283323 - revision: a string representing the snap's revision
284324 - confinement: "classic", "strict", or "devmode"
325+ - version: a string representing the snap's version, if set by the snap author
285326 """
286327
287328 def __init__ (
@@ -293,6 +334,8 @@ def __init__(
293334 confinement : str ,
294335 apps : list [dict [str , JSONType ]] | None = None ,
295336 cohort : str | None = None ,
337+ * ,
338+ version : str | None = None ,
296339 ) -> None :
297340 self ._name = name
298341 self ._state = state
@@ -301,6 +344,7 @@ def __init__(
301344 self ._confinement = confinement
302345 self ._cohort = cohort or ""
303346 self ._apps = apps or []
347+ self ._version = version
304348 self ._snap_client = SnapClient ()
305349
306350 def __eq__ (self , other : object ) -> bool :
@@ -340,11 +384,12 @@ def _snap(self, command: str, optargs: Iterable[str] | None = None) -> str:
340384 optargs = optargs or []
341385 args = ["snap" , command , self ._name , * optargs ]
342386 try :
343- return subprocess .check_output (args , text = True )
387+ with tracer .start_as_current_span (args [0 ]) as span :
388+ span .set_attribute ("argv" , args )
389+ return subprocess .check_output (args , text = True , stderr = subprocess .PIPE )
344390 except CalledProcessError as e :
345- raise SnapError (
346- f"Snap: { self ._name !r} ; command { args !r} failed with output = { e .output !r} "
347- ) from e
391+ msg = f'Snap: { self ._name !r} -- command { args !r} failed!'
392+ raise SnapError ._from_called_process_error (msg = msg , error = e ) from e
348393
349394 def _snap_daemons (
350395 self ,
@@ -369,9 +414,12 @@ def _snap_daemons(
369414 args = ["snap" , * command , * services ]
370415
371416 try :
372- return subprocess .run (args , text = True , check = True , capture_output = True )
417+ with tracer .start_as_current_span (args [0 ]) as span :
418+ span .set_attribute ("argv" , args )
419+ return subprocess .run (args , text = True , check = True , capture_output = True )
373420 except CalledProcessError as e :
374- raise SnapError (f"Could not { args } for snap [{ self ._name } ]: { e .stderr } " ) from e
421+ msg = f'Snap: { self ._name !r} -- command { args !r} failed!'
422+ raise SnapError ._from_called_process_error (msg = msg , error = e ) from e
375423
376424 @typing .overload
377425 def get (self , key : None | Literal ["" ], * , typed : Literal [False ] = False ) -> NoReturn : ...
@@ -475,9 +523,12 @@ def connect(self, plug: str, service: str | None = None, slot: str | None = None
475523
476524 args = ["snap" , * command ]
477525 try :
478- subprocess .run (args , text = True , check = True , capture_output = True )
526+ with tracer .start_as_current_span (args [0 ]) as span :
527+ span .set_attribute ("argv" , args )
528+ subprocess .run (args , text = True , check = True , capture_output = True )
479529 except CalledProcessError as e :
480- raise SnapError (f"Could not { args } for snap [{ self ._name } ]: { e .stderr } " ) from e
530+ msg = f'Snap: { self ._name !r} -- command { args !r} failed!'
531+ raise SnapError ._from_called_process_error (msg = msg , error = e ) from e
481532
482533 def hold (self , duration : timedelta | None = None ) -> None :
483534 """Add a refresh hold to a snap.
@@ -506,11 +557,12 @@ def alias(self, application: str, alias: str | None = None) -> None:
506557 alias = application
507558 args = ["snap" , "alias" , f"{ self .name } .{ application } " , alias ]
508559 try :
509- subprocess .check_output (args , text = True )
560+ with tracer .start_as_current_span (args [0 ]) as span :
561+ span .set_attribute ("argv" , args )
562+ subprocess .run (args , text = True , check = True , capture_output = True )
510563 except CalledProcessError as e :
511- raise SnapError (
512- f"Snap: { self ._name !r} ; command { args !r} failed with output = { e .output !r} "
513- ) from e
564+ msg = f'Snap: { self ._name !r} -- command { args !r} failed!'
565+ raise SnapError ._from_called_process_error (msg = msg , error = e ) from e
514566
515567 def restart (self , services : list [str ] | None = None , reload : bool = False ) -> None :
516568 """Restarts a snap's services.
@@ -577,6 +629,9 @@ def _refresh(
577629 if revision :
578630 args .append (f'--revision="{ revision } "' )
579631
632+ if self .confinement == 'classic' :
633+ args .append ('--classic' )
634+
580635 if devmode :
581636 args .append ("--devmode" )
582637
@@ -745,6 +800,11 @@ def held(self) -> bool:
745800 info = self ._snap ("info" )
746801 return "hold:" in info
747802
803+ @property
804+ def version (self ) -> str | None :
805+ """Returns the version for a snap."""
806+ return self ._version
807+
748808
749809class _UnixSocketConnection (http .client .HTTPConnection ):
750810 """Implementation of HTTPConnection that connects to a named Unix socket."""
@@ -913,15 +973,20 @@ def _request_raw(
913973
914974 def get_installed_snaps (self ) -> list [dict [str , JSONType ]]:
915975 """Get information about currently installed snaps."""
916- return self ._request ("GET" , "snaps" ) # type: ignore
976+ with tracer .start_as_current_span ("get_installed_snaps" ):
977+ return self ._request ("GET" , "snaps" ) # type: ignore
917978
918979 def get_snap_information (self , name : str ) -> dict [str , JSONType ]:
919980 """Query the snap server for information about single snap."""
920- return self ._request ("GET" , "find" , {"name" : name })[0 ] # type: ignore
981+ with tracer .start_as_current_span ("get_snap_information" ) as span :
982+ span .set_attribute ("name" , name )
983+ return self ._request ("GET" , "find" , {"name" : name })[0 ] # type: ignore
921984
922985 def get_installed_snap_apps (self , name : str ) -> list [dict [str , JSONType ]]:
923986 """Query the snap server for apps belonging to a named, currently installed snap."""
924- return self ._request ("GET" , "apps" , {"names" : name , "select" : "service" }) # type: ignore
987+ with tracer .start_as_current_span ("get_installed_snap_apps" ) as span :
988+ span .set_attribute ("name" , name )
989+ return self ._request ("GET" , "apps" , {"names" : name , "select" : "service" }) # type: ignore
925990
926991 def _put_snap_conf (self , name : str , conf : dict [str , JSONAble ]) -> None :
927992 """Set the configuration details for an installed snap."""
@@ -1005,6 +1070,7 @@ def _load_installed_snaps(self) -> None:
10051070 revision = i ["revision" ],
10061071 confinement = i ["confinement" ],
10071072 apps = i .get ("apps" ),
1073+ version = i .get ("version" ),
10081074 )
10091075 self ._snap_map [snap .name ] = snap
10101076
@@ -1024,6 +1090,7 @@ def _load_info(self, name: str) -> Snap:
10241090 revision = info ["revision" ],
10251091 confinement = info ["confinement" ],
10261092 apps = None ,
1093+ version = info .get ("version" ),
10271094 )
10281095
10291096
@@ -1261,7 +1328,13 @@ def install_local(
12611328 if dangerous :
12621329 args .append ("--dangerous" )
12631330 try :
1264- result = subprocess .check_output (args , text = True ).splitlines ()[- 1 ]
1331+ with tracer .start_as_current_span (args [0 ]) as span :
1332+ span .set_attribute ("argv" , args )
1333+ result = subprocess .check_output (
1334+ args ,
1335+ text = True ,
1336+ stderr = subprocess .PIPE ,
1337+ ).splitlines ()[- 1 ]
12651338 snap_name , _ = result .split (" " , 1 )
12661339 snap_name = ansi_filter .sub ("" , snap_name )
12671340
@@ -1277,7 +1350,8 @@ def install_local(
12771350 )
12781351 raise SnapError (f"Failed to find snap { snap_name } in Snap cache" ) from e
12791352 except CalledProcessError as e :
1280- raise SnapError (f"Could not install snap { filename } : { e .output } " ) from e
1353+ msg = f'Cound not install snap { filename } !'
1354+ raise SnapError ._from_called_process_error (msg = msg , error = e ) from e
12811355
12821356
12831357def _system_set (config_item : str , value : str ) -> None :
@@ -1289,9 +1363,12 @@ def _system_set(config_item: str, value: str) -> None:
12891363 """
12901364 args = ["snap" , "set" , "system" , f"{ config_item } ={ value } " ]
12911365 try :
1292- subprocess .check_call (args , text = True )
1366+ with tracer .start_as_current_span (args [0 ]) as span :
1367+ span .set_attribute ("argv" , args )
1368+ subprocess .run (args , text = True , check = True , capture_output = True )
12931369 except CalledProcessError as e :
1294- raise SnapError (f"Failed setting system config '{ config_item } ' to '{ value } '" ) from e
1370+ msg = f"Failed setting system config '{ config_item } ' to '{ value } '"
1371+ raise SnapError ._from_called_process_error (msg = msg , error = e ) from e
12951372
12961373
12971374def hold_refresh (days : int = 90 , forever : bool = False ) -> None :
0 commit comments