Skip to content

Commit

Permalink
Factor out container utilities to separate module
Browse files Browse the repository at this point in the history
  • Loading branch information
apyrgio committed Dec 4, 2024
1 parent 9b244b8 commit e7cd6e3
Show file tree
Hide file tree
Showing 6 changed files with 242 additions and 238 deletions.
176 changes: 176 additions & 0 deletions dangerzone/container_utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
import gzip
import json
import logging
import platform
import shutil
import subprocess
from typing import Dict, Tuple

from .util import get_resource_path, get_subprocess_startupinfo
from . import errors

CONTAINER_NAME = "dangerzone.rocks/dangerzone"

log = logging.getLogger(__name__)


def get_runtime_name() -> str:
if platform.system() == "Linux":
runtime_name = "podman"
else:
# Windows, Darwin, and unknown use docker for now, dangerzone-vm eventually
runtime_name = "docker"
return runtime_name


def get_runtime_version() -> Tuple[int, int]:
"""Get the major/minor parts of the Docker/Podman version.
Some of the operations we perform in this module rely on some Podman features
that are not available across all of our platforms. In order to have a proper
fallback, we need to know the Podman version. More specifically, we're fine with
just knowing the major and minor version, since writing/installing a full-blown
semver parser is an overkill.
"""
# Get the Docker/Podman version, using a Go template.
runtime = get_runtime_name()
if runtime == "podman":
query = "{{.Client.Version}}"
else:
query = "{{.Server.Version}}"

cmd = [runtime, "version", "-f", query]
try:
version = subprocess.run(
cmd,
startupinfo=get_subprocess_startupinfo(),
capture_output=True,
check=True,
).stdout.decode()
except Exception as e:
msg = f"Could not get the version of the {runtime.capitalize()} tool: {e}"
raise RuntimeError(msg) from e

# Parse this version and return the major/minor parts, since we don't need the
# rest.
try:
major, minor, _ = version.split(".", 3)
return (int(major), int(minor))
except Exception as e:
msg = (
f"Could not parse the version of the {runtime.capitalize()} tool"
f" (found: '{version}') due to the following error: {e}"
)
raise RuntimeError(msg)


def get_runtime() -> str:
container_tech = get_runtime_name()
runtime = shutil.which(container_tech)
if runtime is None:
raise errors.NoContainerTechException(container_tech)
return runtime


def list_image_tags() -> Dict[str, str]:
"""Get the tags of all loaded Dangerzone images.
This method returns a mapping of image tags to image IDs, for all Dangerzone
images. This can be useful when we want to find which are the local image tags,
and which image ID does the "latest" tag point to.
"""
images = json.loads(
subprocess.check_output(
[
get_runtime(),
"image",
"list",
"--format",
"json",
CONTAINER_NAME,
],
text=True,
startupinfo=get_subprocess_startupinfo(),
)
)

# Grab every image name and associate it with an image ID.
tags = {}
for image in images:
for name in image["Names"]:
tag = name.split(":")[1]
tags[tag] = image["Id"]

return tags


def delete_image_tag(tag: str) -> None:
"""Delete a Dangerzone image tag."""
name = CONTAINER_NAME + ":" + tag
log.warning(f"Deleting old container image: {name}")
try:
subprocess.check_output(
[get_runtime(), "rmi", "--force", name],
startupinfo=get_subprocess_startupinfo(),
)
except Exception as e:
log.warning(
f"Couldn't delete old container image '{name}', so leaving it there."
f" Original error: {e}"
)


def add_image_tag(cur_tag: str, new_tag: str) -> None:
"""Add a tag to an existing Dangerzone image."""
cur_image_name = CONTAINER_NAME + ":" + cur_tag
new_image_name = CONTAINER_NAME + ":" + new_tag
subprocess.check_output(
[
get_runtime(),
"tag",
cur_image_name,
new_image_name,
],
startupinfo=get_subprocess_startupinfo(),
)

log.info(
f"Successfully tagged container image '{cur_image_name}' as '{new_image_name}'"
)


def get_expected_tag() -> str:
"""Get the tag of the Dangerzone image tarball from the image-id.txt file."""
with open(get_resource_path("image-id.txt")) as f:
return f.read().strip()


def load_image_tarball() -> None:
log.info("Installing Dangerzone container image...")
p = subprocess.Popen(
[get_runtime(), "load"],
stdin=subprocess.PIPE,
startupinfo=get_subprocess_startupinfo(),
)

chunk_size = 4 << 20
compressed_container_path = get_resource_path("container.tar.gz")
with gzip.open(compressed_container_path) as f:
while True:
chunk = f.read(chunk_size)
if len(chunk) > 0:
if p.stdin:
p.stdin.write(chunk)
else:
break
_, err = p.communicate()
if p.returncode < 0:
if err:
error = err.decode()
else:
error = "No output"
raise errors.ImageInstallationException(
f"Could not install container image: {error}"
)

log.info("Successfully installed container image from")
23 changes: 23 additions & 0 deletions dangerzone/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -117,3 +117,26 @@ def wrapper(*args, **kwargs): # type: ignore
sys.exit(1)

return cast(F, wrapper)


#### Container-related errors


class ImageNotPresentException(Exception):
pass


class ImageInstallationException(Exception):
pass


class NoContainerTechException(Exception):
def __init__(self, container_tech: str) -> None:
super().__init__(f"{container_tech} is not installed")


class NotAvailableContainerTechException(Exception):
def __init__(self, container_tech: str, error: str) -> None:
self.error = error
self.container_tech = container_tech
super().__init__(f"{container_tech} is not available")
8 changes: 2 additions & 6 deletions dangerzone/gui/main_window.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,6 @@

from .. import errors
from ..document import SAFE_EXTENSION, Document
from ..isolation_provider.container import (
NoContainerTechException,
NotAvailableContainerTechException,
)
from ..isolation_provider.qubes import is_qubes_native_conversion
from ..util import format_exception, get_resource_path, get_version
from .logic import Alert, CollapsibleBox, DangerzoneGui, UpdateDialog
Expand Down Expand Up @@ -496,10 +492,10 @@ def check_state(self) -> None:

try:
self.dangerzone.isolation_provider.is_available()
except NoContainerTechException as e:
except errors.NoContainerTechException as e:
log.error(str(e))
state = "not_installed"
except NotAvailableContainerTechException as e:
except errors.NotAvailableContainerTechException as e:
log.error(str(e))
state = "not_running"
error = e.error
Expand Down
Loading

0 comments on commit e7cd6e3

Please sign in to comment.