Skip to content

Commit 0383081

Browse files
committed
Factor out container utilities to separate module
1 parent 25fba42 commit 0383081

File tree

6 files changed

+211
-238
lines changed

6 files changed

+211
-238
lines changed

dangerzone/container_utils.py

Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
import gzip
2+
import logging
3+
import platform
4+
import shutil
5+
import subprocess
6+
from typing import List, Tuple
7+
8+
from . import errors
9+
from .util import get_resource_path, get_subprocess_startupinfo
10+
11+
CONTAINER_NAME = "dangerzone.rocks/dangerzone"
12+
13+
log = logging.getLogger(__name__)
14+
15+
16+
def get_runtime_name() -> str:
17+
if platform.system() == "Linux":
18+
runtime_name = "podman"
19+
else:
20+
# Windows, Darwin, and unknown use docker for now, dangerzone-vm eventually
21+
runtime_name = "docker"
22+
return runtime_name
23+
24+
25+
def get_runtime_version() -> Tuple[int, int]:
26+
"""Get the major/minor parts of the Docker/Podman version.
27+
28+
Some of the operations we perform in this module rely on some Podman features
29+
that are not available across all of our platforms. In order to have a proper
30+
fallback, we need to know the Podman version. More specifically, we're fine with
31+
just knowing the major and minor version, since writing/installing a full-blown
32+
semver parser is an overkill.
33+
"""
34+
# Get the Docker/Podman version, using a Go template.
35+
runtime = get_runtime_name()
36+
if runtime == "podman":
37+
query = "{{.Client.Version}}"
38+
else:
39+
query = "{{.Server.Version}}"
40+
41+
cmd = [runtime, "version", "-f", query]
42+
try:
43+
version = subprocess.run(
44+
cmd,
45+
startupinfo=get_subprocess_startupinfo(),
46+
capture_output=True,
47+
check=True,
48+
).stdout.decode()
49+
except Exception as e:
50+
msg = f"Could not get the version of the {runtime.capitalize()} tool: {e}"
51+
raise RuntimeError(msg) from e
52+
53+
# Parse this version and return the major/minor parts, since we don't need the
54+
# rest.
55+
try:
56+
major, minor, _ = version.split(".", 3)
57+
return (int(major), int(minor))
58+
except Exception as e:
59+
msg = (
60+
f"Could not parse the version of the {runtime.capitalize()} tool"
61+
f" (found: '{version}') due to the following error: {e}"
62+
)
63+
raise RuntimeError(msg)
64+
65+
66+
def get_runtime() -> str:
67+
container_tech = get_runtime_name()
68+
runtime = shutil.which(container_tech)
69+
if runtime is None:
70+
raise errors.NoContainerTechException(container_tech)
71+
return runtime
72+
73+
74+
def list_image_tags() -> List[str]:
75+
"""Get the tags of all loaded Dangerzone images.
76+
77+
This method returns a mapping of image tags to image IDs, for all Dangerzone
78+
images. This can be useful when we want to find which are the local image tags,
79+
and which image ID does the "latest" tag point to.
80+
"""
81+
return (
82+
subprocess.check_output(
83+
[
84+
get_runtime(),
85+
"image",
86+
"list",
87+
"--format",
88+
"{{ .Tag }}",
89+
CONTAINER_NAME,
90+
],
91+
text=True,
92+
startupinfo=get_subprocess_startupinfo(),
93+
)
94+
.strip()
95+
.split()
96+
)
97+
98+
99+
def delete_image_tag(tag: str) -> None:
100+
"""Delete a Dangerzone image tag."""
101+
name = CONTAINER_NAME + ":" + tag
102+
log.warning(f"Deleting old container image: {name}")
103+
try:
104+
subprocess.check_output(
105+
[get_runtime(), "rmi", "--force", name],
106+
startupinfo=get_subprocess_startupinfo(),
107+
)
108+
except Exception as e:
109+
log.warning(
110+
f"Couldn't delete old container image '{name}', so leaving it there."
111+
f" Original error: {e}"
112+
)
113+
114+
115+
def get_expected_tag() -> str:
116+
"""Get the tag of the Dangerzone image tarball from the image-id.txt file."""
117+
with open(get_resource_path("image-id.txt")) as f:
118+
return f.read().strip()
119+
120+
121+
def load_image_tarball() -> None:
122+
log.info("Installing Dangerzone container image...")
123+
p = subprocess.Popen(
124+
[get_runtime(), "load"],
125+
stdin=subprocess.PIPE,
126+
startupinfo=get_subprocess_startupinfo(),
127+
)
128+
129+
chunk_size = 4 << 20
130+
compressed_container_path = get_resource_path("container.tar.gz")
131+
with gzip.open(compressed_container_path) as f:
132+
while True:
133+
chunk = f.read(chunk_size)
134+
if len(chunk) > 0:
135+
if p.stdin:
136+
p.stdin.write(chunk)
137+
else:
138+
break
139+
_, err = p.communicate()
140+
if p.returncode < 0:
141+
if err:
142+
error = err.decode()
143+
else:
144+
error = "No output"
145+
raise errors.ImageInstallationException(
146+
f"Could not install container image: {error}"
147+
)
148+
149+
log.info("Successfully installed container image from")

dangerzone/errors.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,3 +117,26 @@ def wrapper(*args, **kwargs): # type: ignore
117117
sys.exit(1)
118118

119119
return cast(F, wrapper)
120+
121+
122+
#### Container-related errors
123+
124+
125+
class ImageNotPresentException(Exception):
126+
pass
127+
128+
129+
class ImageInstallationException(Exception):
130+
pass
131+
132+
133+
class NoContainerTechException(Exception):
134+
def __init__(self, container_tech: str) -> None:
135+
super().__init__(f"{container_tech} is not installed")
136+
137+
138+
class NotAvailableContainerTechException(Exception):
139+
def __init__(self, container_tech: str, error: str) -> None:
140+
self.error = error
141+
self.container_tech = container_tech
142+
super().__init__(f"{container_tech} is not available")

dangerzone/gui/main_window.py

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,6 @@
2525

2626
from .. import errors
2727
from ..document import SAFE_EXTENSION, Document
28-
from ..isolation_provider.container import (
29-
NoContainerTechException,
30-
NotAvailableContainerTechException,
31-
)
3228
from ..isolation_provider.qubes import is_qubes_native_conversion
3329
from ..util import format_exception, get_resource_path, get_version
3430
from .logic import Alert, CollapsibleBox, DangerzoneGui, UpdateDialog
@@ -496,10 +492,10 @@ def check_state(self) -> None:
496492

497493
try:
498494
self.dangerzone.isolation_provider.is_available()
499-
except NoContainerTechException as e:
495+
except errors.NoContainerTechException as e:
500496
log.error(str(e))
501497
state = "not_installed"
502-
except NotAvailableContainerTechException as e:
498+
except errors.NotAvailableContainerTechException as e:
503499
log.error(str(e))
504500
state = "not_running"
505501
error = e.error

0 commit comments

Comments
 (0)