artefacts-cli 0.8.0__py3-none-any.whl → 0.9.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- artefacts/cli/__init__.py +67 -19
- artefacts/cli/app.py +196 -167
- artefacts/cli/app_containers.py +97 -25
- artefacts/cli/app_containers.pyi +3 -0
- artefacts/cli/bagparser.py +4 -0
- artefacts/cli/config.py +62 -0
- artefacts/cli/constants.py +7 -0
- artefacts/cli/containers/__init__.py +5 -5
- artefacts/cli/containers/docker_cm.py +175 -0
- artefacts/cli/containers/docker_utils.py +98 -0
- artefacts/cli/containers/utils.py +20 -8
- artefacts/cli/helpers.py +55 -0
- artefacts/cli/i18n.py +35 -0
- artefacts/cli/locales/art.pot +524 -0
- artefacts/cli/locales/base.pot +995 -0
- artefacts/cli/locales/click.pot +496 -0
- artefacts/cli/other.py +1 -0
- artefacts/cli/ros1.py +21 -6
- artefacts/cli/ros2.py +10 -3
- artefacts/cli/utils.py +8 -4
- artefacts/cli/utils_ros.py +35 -9
- artefacts/cli/version.py +2 -2
- artefacts/copava/__init__.py +1 -0
- {artefacts_cli-0.8.0.dist-info → artefacts_cli-0.9.2.dist-info}/METADATA +10 -3
- artefacts_cli-0.9.2.dist-info/RECORD +33 -0
- {artefacts_cli-0.8.0.dist-info → artefacts_cli-0.9.2.dist-info}/WHEEL +1 -1
- artefacts/cli/containers/docker.py +0 -119
- artefacts_cli-0.8.0.dist-info/RECORD +0 -24
- {artefacts_cli-0.8.0.dist-info → artefacts_cli-0.9.2.dist-info}/entry_points.txt +0 -0
- {artefacts_cli-0.8.0.dist-info → artefacts_cli-0.9.2.dist-info}/top_level.txt +0 -0
artefacts/cli/app_containers.py
CHANGED
@@ -7,6 +7,7 @@ import click
|
|
7
7
|
from c2d.core import Converter
|
8
8
|
|
9
9
|
from artefacts.cli.constants import DEFAULT_API_URL
|
10
|
+
from artefacts.cli.i18n import localise
|
10
11
|
from artefacts.cli.utils import config_validation, read_config
|
11
12
|
from artefacts.cli.containers.utils import ContainerMgr
|
12
13
|
|
@@ -23,35 +24,45 @@ def containers(ctx: click.Context, debug: bool):
|
|
23
24
|
@click.option(
|
24
25
|
"--path",
|
25
26
|
default=".",
|
26
|
-
help=
|
27
|
+
help=localise(
|
28
|
+
"[Deprecated since 0.8.0; please see --root] Path to the root of the project."
|
29
|
+
),
|
27
30
|
)
|
28
31
|
@click.option(
|
29
32
|
"--root",
|
30
33
|
default=".",
|
31
|
-
help="Path to the root of the project.",
|
34
|
+
help=localise("Path to the root of the project."),
|
32
35
|
)
|
33
36
|
@click.option(
|
34
37
|
"--dockerfile",
|
35
38
|
default="Dockerfile",
|
36
|
-
help=
|
39
|
+
help=localise(
|
40
|
+
"Path to a custom Dockerfile. Defaults to Dockerfile under `path` (see option of the same name)."
|
41
|
+
),
|
37
42
|
)
|
38
43
|
@click.option(
|
39
44
|
"--name",
|
40
45
|
required=False,
|
41
|
-
help=
|
46
|
+
help=localise(
|
47
|
+
"[Deprecated since 0.8.0; not used and will disappear after 0.8.0] Name for the generated image"
|
48
|
+
),
|
42
49
|
)
|
43
50
|
@click.option(
|
44
51
|
"--config",
|
45
52
|
callback=config_validation,
|
46
53
|
default="artefacts.yaml",
|
47
|
-
help=
|
54
|
+
help=localise(
|
55
|
+
"Path to the Artefacts configuration file. It defaults to `./artefacts.yaml`"
|
56
|
+
),
|
48
57
|
)
|
49
58
|
@click.option(
|
50
59
|
"--only",
|
51
60
|
required=False,
|
52
61
|
type=Optional[list],
|
53
62
|
default=None,
|
54
|
-
help=
|
63
|
+
help=localise(
|
64
|
+
"Optional list of job names to process. The default is to process all jobs."
|
65
|
+
),
|
55
66
|
)
|
56
67
|
@click.pass_context
|
57
68
|
def build(
|
@@ -67,7 +78,11 @@ def build(
|
|
67
78
|
artefacts_config = read_config(config)
|
68
79
|
except FileNotFoundError:
|
69
80
|
raise click.ClickException(
|
70
|
-
|
81
|
+
localise(
|
82
|
+
"Project config file not found: {config}. Please provide an Artefacts configuration file to proceed (running `artefacts init` allows to generate one).".format(
|
83
|
+
config=config
|
84
|
+
)
|
85
|
+
)
|
71
86
|
)
|
72
87
|
prefix = artefacts_config["project"].strip().lower()
|
73
88
|
dockerfiles = []
|
@@ -87,7 +102,11 @@ def build(
|
|
87
102
|
elif dockerfile != "Dockerfile" and not os.path.exists(dockerfile):
|
88
103
|
# The user asks explicitly for using a specific Dockerfile, so fast fail if we cannot find it
|
89
104
|
raise click.ClickException(
|
90
|
-
|
105
|
+
localise(
|
106
|
+
"Dockerfile `{dockerfile}` not found. Please ensure the file exits. Automatic Dockerfile generation may also work by dropping the --dockerfile option.".format(
|
107
|
+
dockerfile=dockerfile
|
108
|
+
)
|
109
|
+
)
|
91
110
|
)
|
92
111
|
else:
|
93
112
|
# The split on `prefix` is to ensure there is no slash (project names are org/project) confusing the path across supported OS.
|
@@ -100,7 +119,11 @@ def build(
|
|
100
119
|
)
|
101
120
|
if not dest_root.exists():
|
102
121
|
click.echo(
|
103
|
-
|
122
|
+
localise(
|
123
|
+
"No {dockerfile} found here. Let's generate one per scenario based on artefacts.yaml. They will be available under the `{dest_root}` folder and used from there.".format(
|
124
|
+
dockerfile=dockerfile, dest_root=dest_root
|
125
|
+
)
|
126
|
+
)
|
104
127
|
)
|
105
128
|
# No condition on generating the Dockerfiles as:
|
106
129
|
# - Fast
|
@@ -114,7 +137,14 @@ def build(
|
|
114
137
|
dest.mkdir(parents=True, exist_ok=True)
|
115
138
|
_dockerfile = os.path.join(dest, "Dockerfile")
|
116
139
|
df.dump(_dockerfile)
|
117
|
-
click.echo(
|
140
|
+
click.echo(
|
141
|
+
f"[{job_name}] "
|
142
|
+
+ localise(
|
143
|
+
"Using generated Dockerfile at: {dockerfile}".format(
|
144
|
+
dockerfile=_dockerfile
|
145
|
+
)
|
146
|
+
)
|
147
|
+
)
|
118
148
|
dockerfiles.append(
|
119
149
|
dict(
|
120
150
|
path=root,
|
@@ -128,43 +158,78 @@ def build(
|
|
128
158
|
# No condition on building the images, as relatively fast when already exists, and straightforward logic.
|
129
159
|
image, _ = handler.build(**specs)
|
130
160
|
else:
|
131
|
-
click.echo("No Dockerfile, nothing to do.")
|
161
|
+
click.echo(localise("No Dockerfile, nothing to do."))
|
132
162
|
|
133
163
|
|
134
164
|
@containers.command()
|
135
|
-
@click.argument("
|
165
|
+
@click.argument("image_name", metavar=localise("IMAGE_NAME"))
|
136
166
|
@click.pass_context
|
137
|
-
def check(ctx: click.Context,
|
138
|
-
if
|
139
|
-
|
167
|
+
def check(ctx: click.Context, image_name: str):
|
168
|
+
if image_name is None:
|
169
|
+
image_name = "artefacts"
|
140
170
|
handler = ContainerMgr()
|
141
|
-
result = handler.check(
|
171
|
+
result = handler.check(image_name)
|
142
172
|
if ctx.parent is None:
|
143
173
|
# Print only if the command is called directly.
|
144
|
-
print(
|
174
|
+
print(
|
175
|
+
localise("Container image {name} exists and ready to use.").format(
|
176
|
+
name=image_name
|
177
|
+
)
|
178
|
+
)
|
145
179
|
return result
|
146
180
|
|
147
181
|
|
148
|
-
@containers.command(
|
149
|
-
|
182
|
+
@containers.command(
|
183
|
+
context_settings=dict(
|
184
|
+
ignore_unknown_options=True,
|
185
|
+
)
|
186
|
+
)
|
187
|
+
@click.argument("jobname", metavar=localise("JOBNAME"))
|
150
188
|
@click.option(
|
151
189
|
"--config",
|
152
190
|
callback=config_validation,
|
153
191
|
default="artefacts.yaml",
|
154
|
-
help=
|
192
|
+
help=localise(
|
193
|
+
"Path to the Artefacts configuration file. It defaults to `./artefacts.yaml`"
|
194
|
+
),
|
155
195
|
)
|
156
196
|
@click.option(
|
157
197
|
"--with-gui",
|
158
198
|
"with_gui",
|
159
199
|
default=False,
|
160
|
-
help=
|
200
|
+
help=localise(
|
201
|
+
"Show any GUI if any is created by the test runs. By default, UI elements are run but hidden---only test logs are returned. Please note GUI often assume an X11 environment, typically with Qt, so this may not work without a appropriate environment."
|
202
|
+
),
|
161
203
|
)
|
204
|
+
# Not ready, placeholder for the introduction of Podman
|
205
|
+
# @click.option(
|
206
|
+
# "--backend",
|
207
|
+
# help="Container backend to use, mostly useful if you are using several at the same time. When this option is not specified, Artefacts will try to automatically detect available backends, with a preference for Docker if found. The `--engine-args` allows to pass engine arguments like `--gpu` for Docker, etc.",
|
208
|
+
# )
|
209
|
+
@click.argument("engine-args", nargs=-1, type=click.UNPROCESSED)
|
162
210
|
@click.pass_context
|
163
|
-
def run(
|
211
|
+
def run(
|
212
|
+
ctx: click.Context,
|
213
|
+
jobname: str,
|
214
|
+
config: str,
|
215
|
+
with_gui: bool,
|
216
|
+
# backend: str,
|
217
|
+
engine_args: tuple,
|
218
|
+
):
|
219
|
+
# Workaround for job names coming after engine arguments
|
220
|
+
# Idea: Job names do not start with hyphens.
|
221
|
+
if jobname.startswith("-") and engine_args is not None:
|
222
|
+
_fix = list(engine_args)
|
223
|
+
_fix.insert(0, jobname)
|
224
|
+
jobname = _fix.pop()
|
225
|
+
engine_args = tuple(_fix)
|
226
|
+
|
164
227
|
try:
|
165
228
|
artefacts_config = read_config(config)
|
166
229
|
except FileNotFoundError:
|
167
|
-
raise click.ClickException(
|
230
|
+
raise click.ClickException(
|
231
|
+
localise("Project config file not found: {config}".format(config=config))
|
232
|
+
)
|
168
233
|
project = artefacts_config["project"]
|
169
234
|
handler = ContainerMgr()
|
170
235
|
params = dict(
|
@@ -175,11 +240,18 @@ def run(ctx: click.Context, jobname: str, config: str, with_gui: bool):
|
|
175
240
|
# Hidden settings primarily useful to Artefacts developers
|
176
241
|
api_url=os.environ.get("ARTEFACTS_API_URL", DEFAULT_API_URL),
|
177
242
|
api_key=os.environ.get("ARTEFACTS_KEY", None),
|
243
|
+
engine_args=list(engine_args),
|
178
244
|
)
|
179
245
|
container, logs = handler.run(**params)
|
180
246
|
if container:
|
181
|
-
print(
|
247
|
+
print(
|
248
|
+
localise(
|
249
|
+
"Container run complete: Container Id for inspection: {container_id}".format(
|
250
|
+
container_id=container["Id"]
|
251
|
+
)
|
252
|
+
)
|
253
|
+
)
|
182
254
|
else:
|
183
|
-
print("Package run failed:")
|
255
|
+
print(localise("Package run failed:"))
|
184
256
|
for entry in logs:
|
185
257
|
print("\t- " + entry)
|
artefacts/cli/bagparser.py
CHANGED
artefacts/cli/config.py
ADDED
@@ -0,0 +1,62 @@
|
|
1
|
+
import os
|
2
|
+
import platform
|
3
|
+
from typing import Optional
|
4
|
+
|
5
|
+
import click
|
6
|
+
|
7
|
+
from artefacts.cli.i18n import localise
|
8
|
+
from artefacts.cli.helpers import (
|
9
|
+
get_conf_from_file,
|
10
|
+
get_artefacts_api_url,
|
11
|
+
)
|
12
|
+
|
13
|
+
|
14
|
+
class APIConf:
|
15
|
+
def __init__(
|
16
|
+
self, project_name: str, api_version: str, job_name: Optional[str] = None
|
17
|
+
) -> None:
|
18
|
+
config = get_conf_from_file()
|
19
|
+
if project_name in config:
|
20
|
+
profile = config[project_name]
|
21
|
+
else:
|
22
|
+
profile = {}
|
23
|
+
self.api_url = get_artefacts_api_url(profile)
|
24
|
+
self.api_key = os.environ.get("ARTEFACTS_KEY", profile.get("ApiKey", None))
|
25
|
+
if self.api_key is None:
|
26
|
+
batch_id = os.environ.get("AWS_BATCH_JOB_ID", None)
|
27
|
+
job_id = os.environ.get("ARTEFACTS_JOB_ID", None)
|
28
|
+
if batch_id is None or job_id is None:
|
29
|
+
raise click.ClickException(
|
30
|
+
localise(
|
31
|
+
"No API KEY set. Please run `artefacts config add {project_name}`".format(
|
32
|
+
project_name=project_name
|
33
|
+
)
|
34
|
+
)
|
35
|
+
)
|
36
|
+
auth_type = "Internal"
|
37
|
+
# Batch id for array jobs contains array index
|
38
|
+
batch_id = batch_id.split(":")[0]
|
39
|
+
self.headers = {"Authorization": f"{auth_type} {job_id}:{batch_id}"}
|
40
|
+
else:
|
41
|
+
auth_type = "ApiKey"
|
42
|
+
self.headers = {"Authorization": f"{auth_type} {self.api_key}"}
|
43
|
+
self.headers["User-Agent"] = (
|
44
|
+
f"ArtefactsClient/{api_version} ({platform.platform()}/{platform.python_version()})"
|
45
|
+
)
|
46
|
+
if job_name:
|
47
|
+
click.echo(
|
48
|
+
f"[{job_name}] "
|
49
|
+
+ localise(
|
50
|
+
"Connecting to {api_url} using {auth_type}".format(
|
51
|
+
api_url=self.api_url, auth_type=auth_type
|
52
|
+
)
|
53
|
+
)
|
54
|
+
)
|
55
|
+
else:
|
56
|
+
click.echo(
|
57
|
+
localise(
|
58
|
+
"Connecting to {api_url} using {auth_type}".format(
|
59
|
+
api_url=self.api_url, auth_type=auth_type
|
60
|
+
)
|
61
|
+
)
|
62
|
+
)
|
artefacts/cli/constants.py
CHANGED
@@ -1,3 +1,6 @@
|
|
1
|
+
import os
|
2
|
+
|
3
|
+
|
1
4
|
DEFAULT_API_URL = "https://app.artefacts.com/api"
|
2
5
|
|
3
6
|
SUPPORTED_FRAMEWORKS = [
|
@@ -16,3 +19,7 @@ DEPRECATED_FRAMEWORKS = {
|
|
16
19
|
"ros2:0": "ros2:galactic",
|
17
20
|
"ros1:0": "ros1:noetic",
|
18
21
|
}
|
22
|
+
|
23
|
+
HOME = os.path.expanduser("~")
|
24
|
+
CONFIG_DIR = f"{HOME}/.artefacts"
|
25
|
+
CONFIG_PATH = f"{CONFIG_DIR}/config"
|
@@ -1,4 +1,4 @@
|
|
1
|
-
from collections.abc import
|
1
|
+
from collections.abc import Iterator
|
2
2
|
import configparser
|
3
3
|
import os
|
4
4
|
from pathlib import Path
|
@@ -8,7 +8,7 @@ from artefacts.cli.constants import DEFAULT_API_URL
|
|
8
8
|
|
9
9
|
|
10
10
|
class CMgr:
|
11
|
-
def build(self, **kwargs) -> Tuple[str,
|
11
|
+
def build(self, **kwargs) -> Tuple[str, Iterator]:
|
12
12
|
"""
|
13
13
|
Returns the build image ID (e.g. sha256:abcdefghi)
|
14
14
|
and an iterator over the build log entries.
|
@@ -25,11 +25,11 @@ class CMgr:
|
|
25
25
|
self,
|
26
26
|
image: str,
|
27
27
|
project: str,
|
28
|
-
jobname: str = None,
|
29
|
-
artefacts_dir: str = Path("~/.artefacts").expanduser(),
|
28
|
+
jobname: Optional[str] = None,
|
29
|
+
artefacts_dir: Union[str, Path] = Path("~/.artefacts").expanduser(),
|
30
30
|
api_url: str = DEFAULT_API_URL,
|
31
31
|
with_gui: bool = False,
|
32
|
-
) -> Tuple[Any,
|
32
|
+
) -> Tuple[Any, Iterator]:
|
33
33
|
"""
|
34
34
|
Returns a container (Any type as depends on the framework)
|
35
35
|
and an iterator over the container log entries.
|
@@ -0,0 +1,175 @@
|
|
1
|
+
from collections.abc import Iterator
|
2
|
+
import json
|
3
|
+
import os
|
4
|
+
from pathlib import Path
|
5
|
+
import platform
|
6
|
+
from typing import Any, Optional, Tuple, Union
|
7
|
+
from uuid import uuid4
|
8
|
+
|
9
|
+
from artefacts.cli.constants import DEFAULT_API_URL
|
10
|
+
from artefacts.cli.containers import CMgr
|
11
|
+
from artefacts.cli.containers.docker_utils import cli2sdk
|
12
|
+
from artefacts.cli.i18n import localise
|
13
|
+
from artefacts.cli.utils import ensure_available
|
14
|
+
|
15
|
+
ensure_available("docker")
|
16
|
+
|
17
|
+
import docker # noqa: E402
|
18
|
+
from docker import APIClient # noqa: E402
|
19
|
+
|
20
|
+
|
21
|
+
class DockerManager(CMgr):
|
22
|
+
def __init__(self):
|
23
|
+
self.client = APIClient()
|
24
|
+
|
25
|
+
def build(self, **kwargs) -> Tuple[str, Iterator]:
|
26
|
+
kwargs["tag"] = kwargs.pop("name")
|
27
|
+
# Ensure `path` is a string, the Docker package does not support pathlib.
|
28
|
+
kwargs["path"] = str(kwargs.pop("path"))
|
29
|
+
# Remove intermediate containers
|
30
|
+
kwargs["rm"] = True
|
31
|
+
logs = []
|
32
|
+
img_id = None
|
33
|
+
for entry in self.client.build(**kwargs):
|
34
|
+
line_data = [
|
35
|
+
json.loads(v) for v in entry.decode("utf-8").split("\r\n") if len(v) > 0
|
36
|
+
]
|
37
|
+
for data in line_data:
|
38
|
+
if "stream" in data:
|
39
|
+
line = data["stream"].strip()
|
40
|
+
if not line.startswith("---") and len(line) > 0:
|
41
|
+
print(f"[{kwargs['tag'].split('/')[-1]}] {line}")
|
42
|
+
logs.append(line)
|
43
|
+
elif "aux" in data and "ID" in data["aux"]:
|
44
|
+
img_id = str(data["aux"]["ID"])
|
45
|
+
if img_id is None:
|
46
|
+
img_id = str(self.client.inspect_image(kwargs["tag"])["Id"])
|
47
|
+
return img_id, iter(logs)
|
48
|
+
|
49
|
+
def check(
|
50
|
+
self,
|
51
|
+
image: str,
|
52
|
+
) -> bool:
|
53
|
+
return len(self.client.images(name=image)) > 0
|
54
|
+
|
55
|
+
def run(
|
56
|
+
self,
|
57
|
+
image: str,
|
58
|
+
project: str,
|
59
|
+
jobname: Optional[str] = None,
|
60
|
+
artefacts_dir: Union[str, Path] = Path("~/.artefacts").expanduser(),
|
61
|
+
api_url: str = DEFAULT_API_URL,
|
62
|
+
api_key: Optional[str] = None,
|
63
|
+
with_gui: bool = False,
|
64
|
+
engine_args: Optional[list] = None,
|
65
|
+
) -> Tuple[Any, Iterator]:
|
66
|
+
"""
|
67
|
+
Run an application as an Artefacts-enabled container in a Docker engine
|
68
|
+
"""
|
69
|
+
env = {
|
70
|
+
"JOB_ID": str(uuid4()),
|
71
|
+
"ARTEFACTS_JOB_NAME": jobname,
|
72
|
+
"ARTEFACTS_API_URL": api_url,
|
73
|
+
}
|
74
|
+
|
75
|
+
env["ARTEFACTS_KEY"] = api_key or self._get_artefacts_api_key(
|
76
|
+
project, artefacts_dir
|
77
|
+
)
|
78
|
+
if env["ARTEFACTS_KEY"] is None:
|
79
|
+
return None, iter(
|
80
|
+
[
|
81
|
+
localise(
|
82
|
+
"Missing API key for the project. Does `{path}/config` exist and contain your key? Alternatively ARTEFACTS_KEY can be set with the key.".format(
|
83
|
+
path=artefacts_dir
|
84
|
+
)
|
85
|
+
)
|
86
|
+
]
|
87
|
+
)
|
88
|
+
try:
|
89
|
+
if platform.system() in ["Darwin", "Windows"]:
|
90
|
+
# Assume we run in Docker Desktop
|
91
|
+
env["DISPLAY"] = "host.docker.internal:0"
|
92
|
+
else:
|
93
|
+
env["DISPLAY"] = os.environ.get("DISPLAY", ":0")
|
94
|
+
|
95
|
+
if not with_gui:
|
96
|
+
env["QT_QPA_PLATFORM"] = "offscreen"
|
97
|
+
|
98
|
+
# Default configs
|
99
|
+
host_conf = dict(
|
100
|
+
network_mode="host",
|
101
|
+
)
|
102
|
+
|
103
|
+
container_conf = dict(
|
104
|
+
image=image,
|
105
|
+
environment=env,
|
106
|
+
detach=False,
|
107
|
+
)
|
108
|
+
|
109
|
+
# Apply user config and overrides, if any
|
110
|
+
if engine_args:
|
111
|
+
option = None
|
112
|
+
# Add a marker to detect end of args
|
113
|
+
engine_args.append("--end--")
|
114
|
+
for current in engine_args:
|
115
|
+
is_option = current.startswith("-")
|
116
|
+
if is_option and not option:
|
117
|
+
option = current.lstrip("-")
|
118
|
+
if "=" in option:
|
119
|
+
option, value = option.split("=")
|
120
|
+
cli2sdk(host_conf, container_conf, option, value)
|
121
|
+
option = None
|
122
|
+
elif not is_option and option:
|
123
|
+
cli2sdk(host_conf, container_conf, option, current)
|
124
|
+
option = None
|
125
|
+
elif is_option and option:
|
126
|
+
# Assuming detection of concatenated flags, all set.
|
127
|
+
for flag in str(option):
|
128
|
+
cli2sdk(host_conf, container_conf, flag, True)
|
129
|
+
if current != "--end--":
|
130
|
+
option = current
|
131
|
+
|
132
|
+
# Final container config
|
133
|
+
container_conf["host_config"] = self.client.create_host_config(**host_conf)
|
134
|
+
|
135
|
+
try:
|
136
|
+
container = self.client.create_container(**container_conf)
|
137
|
+
except Exception as e:
|
138
|
+
# SDK errors may have an explanation
|
139
|
+
# E.g. in 7.1.0 https://github.com/docker/docker-py/blob/7.1.0/docker/errors.py#L53
|
140
|
+
known = str(e)
|
141
|
+
if known.endswith(")"):
|
142
|
+
# Error message: info ("explanation")
|
143
|
+
# We return explanation
|
144
|
+
detail = known[known.index("(") + 2 : -2]
|
145
|
+
else:
|
146
|
+
# Error message: info
|
147
|
+
# We return info
|
148
|
+
detail = known[known.index(":") + 2 :]
|
149
|
+
raise Exception(f"Invalid container configuration: {detail}")
|
150
|
+
self.client.start(container=container.get("Id"))
|
151
|
+
|
152
|
+
for entry in self.client.logs(container=container.get("Id"), stream=True):
|
153
|
+
print(entry.decode("utf-8").strip())
|
154
|
+
|
155
|
+
return container, iter([])
|
156
|
+
except docker.errors.ImageNotFound:
|
157
|
+
return None, iter(
|
158
|
+
[
|
159
|
+
localise(
|
160
|
+
"Image {image} not found by Docker. Perhaps need to build first?".format(
|
161
|
+
image=image
|
162
|
+
)
|
163
|
+
)
|
164
|
+
]
|
165
|
+
)
|
166
|
+
except Exception as e:
|
167
|
+
return None, iter(
|
168
|
+
[
|
169
|
+
localise(
|
170
|
+
"Failed to run from {image}. All we know: {message}".format(
|
171
|
+
image=image, message=e
|
172
|
+
)
|
173
|
+
)
|
174
|
+
]
|
175
|
+
)
|
@@ -0,0 +1,98 @@
|
|
1
|
+
from typing import Any
|
2
|
+
|
3
|
+
import docker # noqa: E402
|
4
|
+
|
5
|
+
from artefacts.cli.i18n import localise
|
6
|
+
|
7
|
+
|
8
|
+
def _identity(v: Any) -> Any:
|
9
|
+
return v
|
10
|
+
|
11
|
+
|
12
|
+
def _make_gpu_device_request(gpus: str) -> list:
|
13
|
+
"""
|
14
|
+
Code based on Docker documented use, not on the source code.
|
15
|
+
|
16
|
+
https://docs.docker.com/reference/cli/docker/container/run/#gpus
|
17
|
+
"""
|
18
|
+
if '"device=' in gpus:
|
19
|
+
try:
|
20
|
+
# Docs: `--gpus '"device=1,2"'`
|
21
|
+
_g = gpus.strip('"')
|
22
|
+
# There must be 2 double quotes.
|
23
|
+
assert len(_g) == len(gpus) - 2
|
24
|
+
ids = sorted(_g[_g.index("=") + 1 :].split(","))
|
25
|
+
except Exception as e:
|
26
|
+
raise Exception(
|
27
|
+
localise(
|
28
|
+
'Invalid GPU device for Docker: {g} ({e}). Accepted device formats are all, device=1 and "device=1,2" (with a list of devices, quotes must be included'.format(
|
29
|
+
g=gpus, e=e
|
30
|
+
)
|
31
|
+
)
|
32
|
+
)
|
33
|
+
elif "device=" in gpus:
|
34
|
+
# Docs: `--gpus device=1` or `--gpus device=GPU-3faa8-219`
|
35
|
+
if "," in gpus:
|
36
|
+
raise Exception(
|
37
|
+
localise(
|
38
|
+
'Invalid GPU device for Docker: {g}. List of devices must be double-quoted. Accepted device formats are all, device=1 and "device=1,2" (with a list of devices, quotes must be included'.format(
|
39
|
+
g=gpus,
|
40
|
+
)
|
41
|
+
)
|
42
|
+
)
|
43
|
+
ids = [gpus.split("=")[-1]]
|
44
|
+
elif "all" == gpus:
|
45
|
+
ids = [gpus]
|
46
|
+
else:
|
47
|
+
raise Exception(
|
48
|
+
localise(
|
49
|
+
'Invalid GPU device for Docker: {g}. Accepted device formats are all, device=1 and "device=1,2" (with a list of devices, quotes must be included'.format(
|
50
|
+
g=gpus
|
51
|
+
)
|
52
|
+
)
|
53
|
+
)
|
54
|
+
return [docker.types.DeviceRequest(device_ids=ids, capabilities=[["gpu"]])]
|
55
|
+
|
56
|
+
|
57
|
+
_cli_sdk_option_map = {
|
58
|
+
"net": {
|
59
|
+
"t": "host",
|
60
|
+
"o": "network_mode",
|
61
|
+
},
|
62
|
+
"gpus": {
|
63
|
+
"t": "host",
|
64
|
+
"o": "device_requests",
|
65
|
+
"f": _make_gpu_device_request,
|
66
|
+
},
|
67
|
+
"t": {
|
68
|
+
"t": "container",
|
69
|
+
"o": "tty",
|
70
|
+
},
|
71
|
+
"i": {
|
72
|
+
"t": "container",
|
73
|
+
"o": "stdin_open",
|
74
|
+
},
|
75
|
+
"tty": {
|
76
|
+
"t": "container",
|
77
|
+
},
|
78
|
+
"interactive": {
|
79
|
+
"t": "container",
|
80
|
+
"o": "stdin_open",
|
81
|
+
},
|
82
|
+
}
|
83
|
+
|
84
|
+
|
85
|
+
def cli2sdk(host: dict, container: dict, option: str, value: Any) -> None:
|
86
|
+
"""
|
87
|
+
`host` and `container` are IO map arguments.
|
88
|
+
"""
|
89
|
+
method = _cli_sdk_option_map.get(option)
|
90
|
+
if method:
|
91
|
+
if method["t"] == "host":
|
92
|
+
host[method.get("o") or option] = (method.get("f") or _identity)(value)
|
93
|
+
elif method["t"] == "container":
|
94
|
+
container[method.get("o") or option] = (method.get("f") or _identity)(value)
|
95
|
+
else:
|
96
|
+
# Based on current knowledge of the SDK, it seems
|
97
|
+
# we can opt for the host config as default.
|
98
|
+
host[option] = value
|