diff --git a/apt_ostree/cmd/__init__.py b/apt_ostree/cmd/__init__.py index 5d44657..c5e99dd 100644 --- a/apt_ostree/cmd/__init__.py +++ b/apt_ostree/cmd/__init__.py @@ -11,6 +11,8 @@ class State: def __init__(self): self.debug = False self.workspace = None + self.repo = None + self.branch = None # pass state between command and apt-ostree sub-commands diff --git a/apt_ostree/cmd/shell.py b/apt_ostree/cmd/shell.py index e993f61..50903ad 100644 --- a/apt_ostree/cmd/shell.py +++ b/apt_ostree/cmd/shell.py @@ -14,6 +14,7 @@ from apt_ostree.cmd.options import debug_option from apt_ostree.cmd.options import workspace_option from apt_ostree.cmd import pass_state_context from apt_ostree.cmd.repo import repo +from apt_ostree.cmd.status import status from apt_ostree.cmd.version import version from apt_ostree.log import setup_log @@ -41,4 +42,5 @@ def main(): cli.add_command(compose) cli.add_command(repo) +cli.add_command(status) cli.add_command(version) diff --git a/apt_ostree/cmd/status.py b/apt_ostree/cmd/status.py new file mode 100644 index 0000000..e1e10ad --- /dev/null +++ b/apt_ostree/cmd/status.py @@ -0,0 +1,29 @@ +""" +Copyright (c) 2023 Wind River Systems, Inc. + +SPDX-License-Identifier: Apache-2.0 + +""" + +import errno +import sys + +import click + +from apt_ostree.cmd import pass_state_context +from apt_ostree.status import Status + + +@click.command(help="Get the status of the booted system.") +@pass_state_context +def status(state): + try: + Status(state).get_deployment() + except KeyboardInterrupt: + click.secho("\n" + ("Exiting at your request.")) + sys.exit(130) + except BrokenPipeError: + sys.exit() + except OSError as error: + if error.errno == errno.ENOSPC: + sys.exit("errror - No space left on device.") diff --git a/apt_ostree/ostree.py b/apt_ostree/ostree.py index 5c04ce7..a252302 100644 --- a/apt_ostree/ostree.py +++ b/apt_ostree/ostree.py @@ -6,9 +6,17 @@ SPDX-License-Identifier: Apache-2.0 """ import subprocess +import sys + +import click from apt_ostree.utils import run_command +# pylint: disable=wrong-import-position +import gi +gi.require_version("OSTree", "1.0") +from gi.repository import Gio, GLib, OSTree # noqa: H301 + class Ostree: def __init__(self, state): @@ -36,3 +44,48 @@ class Ostree: cmd += [str(root)] r = run_command(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) return r + + def get_sysroot(self): + """Load the /ostree directory (sysroot).""" + sysroot = OSTree.Sysroot() + if not sysroot.load(): + click.secho("Unable to load /sysroot", fg="red") + sys.exit(1) + return sysroot + + def open_ostree(self): + """"Open the ostree repository.""" + if self.state.repo: + repo = OSTree.Repo.new(Gio.File.new_for_path(str(self.state.repo))) + if not repo.open(None): + click.secho( + "Opening the archive OSTree repository failed.", fg="red") + sys.exit(1) + else: + sysroot = self.get_sysroot() + _, repo = sysroot.get_repo() + if not repo.open(): + click.secho( + "Opening the archive OSTree repository failed.", fg="red") + sys.exit(1) + return repo + + def get_branch(self): + """Get a branch in a current deployment.""" + if self.state.branch: + return self.state.branch + else: + sysroot = self.get_sysroot() + deployment = sysroot.get_booted_deployment() + origin = deployment.get_origin() + try: + refspec = origin.get_string("origin", "refspec") + except GLib.Error as e: + # If not a "key not found" error then raise the exception + if not e.matches(GLib.KeyFile.error_quark(), + GLib.KeyFileError.KEY_NOT_FOUND): + raise (e) + # Fallback to `baserefspec` + refspec = origin.get_string('origin', 'baserefspec') + + return refspec diff --git a/apt_ostree/status.py b/apt_ostree/status.py new file mode 100644 index 0000000..0b104ed --- /dev/null +++ b/apt_ostree/status.py @@ -0,0 +1,72 @@ +""" +Copyright (c) 2023 Wind River Systems, Inc. + +SPDX-License-Identifier: Apache-2.0 + +""" +import pathlib +import shlex + +from rich.console import Console +from rich.table import Table + +from apt_ostree.ostree import Ostree + + +class Status: + def __init__(self, state): + self.state = state + self.ostree = Ostree(state) + self.console = Console() + self.sysroot = None + + def get_deployment(self): + """Get information about the current deployment""" + self.sysroot = self.ostree.get_sysroot() + deployment = self.sysroot.get_booted_deployment() + + table = Table(box=None) + table.add_row("Current Deployment:") + table.add_row() + table.add_row("Branch:", f"[green]{self.ostree.get_branch()}[/green]") + table.add_row("Commit:", f"{deployment.get_csum()}") + + root = self._get_deployment_path(deployment) + os_release = self._get_os_release(root) + if deployment.get_osname() == "debian": + release = os_release.get("PRETTY_NAME").replace('"', '') + table.add_row("Debian Release:", release) + + table.add_row() + + if table.columns: + self.console.print(table) + + def _get_deployment_path(self, target_deployment): + """Get the path for the /sysroot""" + return pathlib.Path("/" + self.sysroot.get_deployment_dirpath( + target_deployment)) + + def _get_os_release(self, rootfs): + """Parse the /etc/os-release file.""" + try: + file = open(rootfs.joinpath("/etc/os-release"), encoding="utf-8") + except FileNotFoundError: + try: + file = open(rootfs.joinpath( + "/usr/lib/os-release"), encoding="utf-8") + except FileNotFoundError: + return {} + + os_release = {} + for line in file.readlines(): + line = line.strip() + if not line or line.startswith("#"): + continue + try: + k, v = line.split("=") + (v_parsed, ) = shlex.split(v) # expect only one token + except ValueError: + continue + os_release[k] = v + return os_release diff --git a/bindep.txt b/bindep.txt index b143b30..c8fdb7b 100644 --- a/bindep.txt +++ b/bindep.txt @@ -1 +1,7 @@ python3-apt [platform:dpkg] +ostree [platform:dpkg] +libostree-dev [platform:dpkg] +gir1.2-glib-2.0 [platform:dpkg] +gir1.2-ostree-1.0 [platform:dpkg] +libcairo2-dev [platform:dpkg] +libgirepository1.0-dev [platform:dpkg] diff --git a/tox.ini b/tox.ini index 52b2681..8623810 100644 --- a/tox.ini +++ b/tox.ini @@ -7,8 +7,8 @@ ignore_basepython_conflict = true [testenv] basepython = python3 -usedevelop = false -sitepacages = True +usedevelop = true +sitepacages = False setenv = PYTHONWARNINGS=default::DeprecationWarning OS_STDOUT_CAPTURE=1 @@ -39,8 +39,6 @@ commands = coverage html -d cover coverage xml -o cover/coverage.xml - - [testenv:docs] deps = -r{toxinidir}/doc/requirements.txt commands = sphinx-build -W -b html doc/source doc/build/html @@ -66,3 +64,8 @@ commands = skip_install = True deps = bindep commands = bindep test + +[stestr] +test_path=./apt_ostree/tests +top_dir=./ +group_regex=([^\.]*\.)*