Skip to content

Commit 4483a24

Browse files
Bump operator-libs-linux to version 2.15
1 parent 1ffb94a commit 4483a24

File tree

3 files changed

+102
-23
lines changed

3 files changed

+102
-23
lines changed

lib/charms/operator_libs_linux/v2/snap.py

Lines changed: 99 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,19 @@
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
1729
The `snap` module provides convenience methods for listing, installing, refreshing, and removing
1830
Snap packages, in addition to setting and getting configuration options for them.
@@ -54,6 +66,10 @@
5466
except 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

5975
from __future__ import annotations
@@ -85,14 +101,17 @@
85101
TypeVar,
86102
)
87103

104+
import opentelemetry.trace
105+
88106
if 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

95113
logger = logging.getLogger(__name__)
114+
tracer = opentelemetry.trace.get_tracer(__name__)
96115

97116
# The unique Charmhub library identifier, never change it
98117
LIBID = "05394e5893f94f2d90feb7cbe6b633cd"
@@ -102,7 +121,9 @@
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):
268290
class 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

272312
class 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

749809
class _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

12831357
def _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

12971374
def hold_refresh(days: int = 90, forever: bool = False) -> None:

poetry.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyproject.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ poetry-core = "*"
2828
pydantic = "^1.10"
2929
# grafana_agent/v0/cos_agent.py
3030
cosl = ">=0.0.50"
31+
# operator_libs_linux/v2/snap.py
32+
opentelemetry-api = "*"
3133
# tls_certificates_interface/v2/tls_certificates.py
3234
cryptography = ">=42.0.5"
3335
jsonschema = "*"

0 commit comments

Comments
 (0)