holoscan-cli 2.9.0__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.
- holoscan_cli/__init__.py +35 -0
- holoscan_cli/__main__.py +164 -0
- holoscan_cli/common/argparse_types.py +156 -0
- holoscan_cli/common/artifact_sources.py +160 -0
- holoscan_cli/common/constants.py +119 -0
- holoscan_cli/common/dockerutils.py +521 -0
- holoscan_cli/common/enum_types.py +49 -0
- holoscan_cli/common/exceptions.py +126 -0
- holoscan_cli/common/sdk_utils.py +195 -0
- holoscan_cli/common/utils.py +137 -0
- holoscan_cli/logging.json +37 -0
- holoscan_cli/nics/__init__.py +15 -0
- holoscan_cli/nics/nics.py +33 -0
- holoscan_cli/package-source.json +32 -0
- holoscan_cli/packager/__init__.py +15 -0
- holoscan_cli/packager/arguments.py +148 -0
- holoscan_cli/packager/config_reader.py +180 -0
- holoscan_cli/packager/container_builder.py +426 -0
- holoscan_cli/packager/manifest_files.py +217 -0
- holoscan_cli/packager/models.py +90 -0
- holoscan_cli/packager/package_command.py +197 -0
- holoscan_cli/packager/packager.py +124 -0
- holoscan_cli/packager/parameters.py +603 -0
- holoscan_cli/packager/platforms.py +426 -0
- holoscan_cli/packager/templates/Dockerfile.jinja2 +479 -0
- holoscan_cli/packager/templates/dockerignore +92 -0
- holoscan_cli/packager/templates/tools.sh +414 -0
- holoscan_cli/py.typed +14 -0
- holoscan_cli/runner/__init__.py +15 -0
- holoscan_cli/runner/resources.py +185 -0
- holoscan_cli/runner/run_command.py +207 -0
- holoscan_cli/runner/runner.py +340 -0
- holoscan_cli/version/__init__.py +15 -0
- holoscan_cli/version/version.py +53 -0
- holoscan_cli-2.9.0.dist-info/LICENSE +201 -0
- holoscan_cli-2.9.0.dist-info/METADATA +102 -0
- holoscan_cli-2.9.0.dist-info/RECORD +39 -0
- holoscan_cli-2.9.0.dist-info/WHEEL +4 -0
- holoscan_cli-2.9.0.dist-info/entry_points.txt +4 -0
|
@@ -0,0 +1,521 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: Copyright (c) 2023-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
#
|
|
4
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
# you may not use this file except in compliance with the License.
|
|
6
|
+
# You may obtain a copy of the License at
|
|
7
|
+
#
|
|
8
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
#
|
|
10
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
# See the License for the specific language governing permissions and
|
|
14
|
+
# limitations under the License.
|
|
15
|
+
import json
|
|
16
|
+
import logging
|
|
17
|
+
import os
|
|
18
|
+
import posixpath
|
|
19
|
+
import re
|
|
20
|
+
import subprocess
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
from typing import Optional
|
|
23
|
+
|
|
24
|
+
from python_on_whales import docker
|
|
25
|
+
|
|
26
|
+
from ..common.utils import run_cmd_output
|
|
27
|
+
from .constants import DefaultValues, EnvironmentVariables
|
|
28
|
+
from .enum_types import PlatformConfiguration, SdkType
|
|
29
|
+
from .exceptions import GpuResourceError, InvalidManifestError, RunContainerError
|
|
30
|
+
from .utils import get_gpu_count, get_requested_gpus
|
|
31
|
+
|
|
32
|
+
logger = logging.getLogger("common")
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def parse_docker_image_name_and_tag(
|
|
36
|
+
image_name: str,
|
|
37
|
+
) -> tuple[Optional[str], Optional[str]]:
|
|
38
|
+
"""Parse a given Docker image name and tag.
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
image_name (str): Docker image name and optionally a tag
|
|
42
|
+
|
|
43
|
+
Returns:
|
|
44
|
+
Tuple[Optional[str], Optional[str]]: a tuple with first item as the name of the image
|
|
45
|
+
and tag as the second item
|
|
46
|
+
"""
|
|
47
|
+
match = re.search(
|
|
48
|
+
r"^(?P<name>([\w.\-_]+((:\d+|)(?=/[a-z0-9._-]+/[a-z0-9._-]+))|)(/?)([a-z0-9.\-_/]+(/[a-z0-9.\-_]+|)))(:(?P<tag>[\w.\-_]{1,127})|)$",
|
|
49
|
+
image_name,
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
if match is None or match.group("name") is None:
|
|
53
|
+
return None, None
|
|
54
|
+
|
|
55
|
+
name = match.group("name")
|
|
56
|
+
tag = match.group("tag") if match.group("tag") else None
|
|
57
|
+
|
|
58
|
+
return (name, tag)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def create_or_use_network(network: Optional[str], image_name: Optional[str]) -> str:
|
|
62
|
+
"""Create a Docker network by the given name if not already exists.
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
network (Optional[str]): name of the network to create
|
|
66
|
+
image_name (Optional[str]): name of the image used to generate a network name from
|
|
67
|
+
|
|
68
|
+
Raises:
|
|
69
|
+
RunContainerError: when unable to retrieve the specified network or failed to create one.
|
|
70
|
+
|
|
71
|
+
Returns:
|
|
72
|
+
str: network name
|
|
73
|
+
"""
|
|
74
|
+
if network is None and image_name is not None:
|
|
75
|
+
network = image_name.split(":")[0]
|
|
76
|
+
network += "-network"
|
|
77
|
+
|
|
78
|
+
assert network is not None
|
|
79
|
+
|
|
80
|
+
try:
|
|
81
|
+
networks = docker.network.list(filters={"name": f"^{network}$"})
|
|
82
|
+
if len(networks) > 0:
|
|
83
|
+
return networks[0].name
|
|
84
|
+
except Exception as ex:
|
|
85
|
+
raise RunContainerError(f"error retrieving network information: {ex}") from ex
|
|
86
|
+
|
|
87
|
+
try:
|
|
88
|
+
return docker.network.create(network, driver="bridge").name
|
|
89
|
+
except Exception as ex:
|
|
90
|
+
raise RunContainerError(f"error creating Docker network: {ex}") from ex
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def image_exists(image_name: str) -> bool:
|
|
94
|
+
"""Checks if the Docker image exists.
|
|
95
|
+
|
|
96
|
+
Args:
|
|
97
|
+
image_name (str): name of the Docker image
|
|
98
|
+
|
|
99
|
+
Returns:
|
|
100
|
+
bool: whether the image exists or not.
|
|
101
|
+
"""
|
|
102
|
+
if image_name is None:
|
|
103
|
+
return False
|
|
104
|
+
try:
|
|
105
|
+
if not docker.image.exists(image_name):
|
|
106
|
+
logger.info(f"Attempting to pull image {image_name}..")
|
|
107
|
+
docker.image.pull(image_name)
|
|
108
|
+
return docker.image.exists(image_name)
|
|
109
|
+
except Exception as e:
|
|
110
|
+
logger.error(str(e))
|
|
111
|
+
return False
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def docker_export_tarball(file: str, tag: str):
|
|
115
|
+
"""Exports the docker image to a file
|
|
116
|
+
|
|
117
|
+
Args:
|
|
118
|
+
file (str): name of the exported file
|
|
119
|
+
tag (str): source Docker image tag
|
|
120
|
+
"""
|
|
121
|
+
docker.image.save(tag, file)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def create_and_get_builder(builder_name: str):
|
|
125
|
+
"""Creates a Docker BuildX builder
|
|
126
|
+
|
|
127
|
+
Args:
|
|
128
|
+
builder_name (str): name of the builder to create
|
|
129
|
+
|
|
130
|
+
Returns:
|
|
131
|
+
_type_: name of the builder created
|
|
132
|
+
"""
|
|
133
|
+
builders = docker.buildx.list()
|
|
134
|
+
for builder in builders:
|
|
135
|
+
if builder.name == builder_name:
|
|
136
|
+
logger.info(f"Using existing Docker BuildKit builder `{builder_name}`")
|
|
137
|
+
return builder_name
|
|
138
|
+
|
|
139
|
+
logger.info(
|
|
140
|
+
f"Creating Docker BuildKit builder `{builder_name}` using `docker-container`"
|
|
141
|
+
)
|
|
142
|
+
builder = docker.buildx.create(
|
|
143
|
+
name=builder_name, driver="docker-container", driver_options={"network": "host"}
|
|
144
|
+
)
|
|
145
|
+
return builder.name
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def build_docker_image(**kwargs):
|
|
149
|
+
"""Builds a Docker image"""
|
|
150
|
+
_ = docker.buildx.build(**kwargs)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def docker_run(
|
|
154
|
+
name: str,
|
|
155
|
+
image_name: str,
|
|
156
|
+
input_path: Optional[Path],
|
|
157
|
+
output_path: Optional[Path],
|
|
158
|
+
app_info: dict,
|
|
159
|
+
pkg_info: dict,
|
|
160
|
+
quiet: bool,
|
|
161
|
+
commands: list[str],
|
|
162
|
+
health_check: bool,
|
|
163
|
+
network: str,
|
|
164
|
+
network_interface: Optional[str],
|
|
165
|
+
use_all_nics: bool,
|
|
166
|
+
gpu_enum: Optional[str],
|
|
167
|
+
config: Optional[Path],
|
|
168
|
+
render: bool,
|
|
169
|
+
user: str,
|
|
170
|
+
terminal: bool,
|
|
171
|
+
devices: list[str],
|
|
172
|
+
platform_config: str,
|
|
173
|
+
shared_memory_size: str = "1GB",
|
|
174
|
+
is_root: bool = False,
|
|
175
|
+
):
|
|
176
|
+
"""Creates and runs a Docker container
|
|
177
|
+
|
|
178
|
+
`HOLOSCAN_HOSTING_SERVICE` environment variable is used for hiding the help message
|
|
179
|
+
inside the tools.sh when the users run the container using holoscan run.
|
|
180
|
+
|
|
181
|
+
Args:
|
|
182
|
+
image_name (str): Docker image name
|
|
183
|
+
input_path (Optional[Path]): input data path
|
|
184
|
+
output_path (Optional[Path]): output data path
|
|
185
|
+
app_info (dict): app manifest
|
|
186
|
+
pkg_info (dict): package manifest
|
|
187
|
+
quiet (bool): prints only stderr when True, otherwise, prints all logs
|
|
188
|
+
commands (List[str]): list of arguments to provide to the container
|
|
189
|
+
health_check (bool): whether or not to enable the gRPC health check service
|
|
190
|
+
network (str): Docker network to associate the container with
|
|
191
|
+
network_interface (Optional[str]): Name of the network interface for setting
|
|
192
|
+
UCX_NET_DEVICES
|
|
193
|
+
use_all_nics (bool): Sets UCX_CM_USE_ALL_DEVICES to 'y' if True
|
|
194
|
+
config (Optional[Path]): optional configuration file for overriding the embedded one
|
|
195
|
+
render (bool): whether or not to enable graphic rendering
|
|
196
|
+
user (str): UID and GID to associate with the container
|
|
197
|
+
terminal (bool): whether or not to enter bash terminal
|
|
198
|
+
devices (List[str]): list of devices to be mapped into the container
|
|
199
|
+
platformConfig (str): platform configuration value used when packaging the application,
|
|
200
|
+
shared_memory_size (str): size of /dev/shm,
|
|
201
|
+
is_root (bool): whether the user is root (UID = 0) or not
|
|
202
|
+
"""
|
|
203
|
+
|
|
204
|
+
volumes = []
|
|
205
|
+
environment_variables = {
|
|
206
|
+
"NVIDIA_DRIVER_CAPABILITIES": "all",
|
|
207
|
+
"HOLOSCAN_HOSTING_SERVICE": "HOLOSCAN_RUN",
|
|
208
|
+
"UCX_CM_USE_ALL_DEVICES": "y" if use_all_nics else "n",
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
if network_interface is not None:
|
|
212
|
+
environment_variables["UCX_NET_DEVICES"] = network_interface
|
|
213
|
+
|
|
214
|
+
if health_check:
|
|
215
|
+
environment_variables["HOLOSCAN_ENABLE_HEALTH_CHECK"] = "true"
|
|
216
|
+
|
|
217
|
+
if logger.root.level == logging.DEBUG:
|
|
218
|
+
environment_variables["UCX_LOG_LEVEL"] = "DEBUG"
|
|
219
|
+
environment_variables["VK_LOADER_DEBUG"] = "all"
|
|
220
|
+
|
|
221
|
+
if render:
|
|
222
|
+
volumes.append(("/tmp/.X11-unix", "/tmp/.X11-unix"))
|
|
223
|
+
display = os.environ.get("DISPLAY", None)
|
|
224
|
+
if display is not None:
|
|
225
|
+
environment_variables["DISPLAY"] = display
|
|
226
|
+
xdg_session_type = os.environ.get("XDG_SESSION_TYPE", None)
|
|
227
|
+
if xdg_session_type is not None:
|
|
228
|
+
environment_variables["XDG_SESSION_TYPE"] = xdg_session_type
|
|
229
|
+
xdg_runtime_dir = os.environ.get("XDG_RUNTIME_DIR", None)
|
|
230
|
+
if xdg_runtime_dir is not None:
|
|
231
|
+
volumes.append((xdg_runtime_dir, xdg_runtime_dir))
|
|
232
|
+
environment_variables["XDG_RUNTIME_DIR"] = xdg_runtime_dir
|
|
233
|
+
wayland_display = os.environ.get("WAYLAND_DISPLAY", None)
|
|
234
|
+
if wayland_display is not None:
|
|
235
|
+
environment_variables["WAYLAND_DISPLAY"] = wayland_display
|
|
236
|
+
|
|
237
|
+
# Use user-specified --gpu values
|
|
238
|
+
if gpu_enum is not None:
|
|
239
|
+
environment_variables["NVIDIA_VISIBLE_DEVICES"] = gpu_enum
|
|
240
|
+
# If the image was built for iGPU but the system is configured for dGPU, attempt
|
|
241
|
+
# targeting the system's iGPU using the CDI spec
|
|
242
|
+
elif (
|
|
243
|
+
platform_config == PlatformConfiguration.iGPU.value
|
|
244
|
+
and not _host_is_native_igpu()
|
|
245
|
+
):
|
|
246
|
+
environment_variables["NVIDIA_VISIBLE_DEVICES"] = "nvidia.com/igpu=0"
|
|
247
|
+
logger.info(
|
|
248
|
+
"Attempting to run an image for iGPU (integrated GPU) on a system configured "
|
|
249
|
+
"with a dGPU (discrete GPU). If this is correct (ex: IGX Orin developer kit), "
|
|
250
|
+
"make sure to enable iGPU on dGPU support as described in your developer kit "
|
|
251
|
+
"user guide. If not, either rebuild the image for dGPU or run this image on a "
|
|
252
|
+
"system configured for iGPU only (ex: Jetson AGX, Nano...)."
|
|
253
|
+
)
|
|
254
|
+
# Otherwise, read specs from package manifest
|
|
255
|
+
else:
|
|
256
|
+
requested_gpus = get_requested_gpus(pkg_info)
|
|
257
|
+
available_gpus = get_gpu_count()
|
|
258
|
+
|
|
259
|
+
if available_gpus < requested_gpus:
|
|
260
|
+
raise GpuResourceError(
|
|
261
|
+
f"Available GPUs ({available_gpus}) are less than required ({requested_gpus}). "
|
|
262
|
+
)
|
|
263
|
+
|
|
264
|
+
if requested_gpus == 0:
|
|
265
|
+
environment_variables["NVIDIA_VISIBLE_DEVICES"] = "all"
|
|
266
|
+
else:
|
|
267
|
+
environment_variables["NVIDIA_VISIBLE_DEVICES"] = ",".join(
|
|
268
|
+
map(str, range(0, requested_gpus))
|
|
269
|
+
)
|
|
270
|
+
|
|
271
|
+
if "path" in app_info["input"]:
|
|
272
|
+
mapped_input = Path(app_info["input"]["path"]).as_posix()
|
|
273
|
+
else:
|
|
274
|
+
mapped_input = DefaultValues.INPUT_DIR
|
|
275
|
+
|
|
276
|
+
if not posixpath.isabs(mapped_input):
|
|
277
|
+
mapped_input = posixpath.join(app_info["workingDirectory"], mapped_input)
|
|
278
|
+
if input_path is not None:
|
|
279
|
+
volumes.append((str(input_path), mapped_input))
|
|
280
|
+
|
|
281
|
+
if "path" in app_info["output"]:
|
|
282
|
+
mapped_output = Path(app_info["output"]["path"]).as_posix()
|
|
283
|
+
else:
|
|
284
|
+
mapped_output = DefaultValues.INPUT_DIR
|
|
285
|
+
|
|
286
|
+
if not posixpath.isabs(mapped_output):
|
|
287
|
+
mapped_output = posixpath.join(app_info["workingDirectory"], mapped_output)
|
|
288
|
+
if output_path is not None:
|
|
289
|
+
volumes.append((str(output_path), mapped_output))
|
|
290
|
+
|
|
291
|
+
for env in app_info["environment"]:
|
|
292
|
+
if env == EnvironmentVariables.HOLOSCAN_INPUT_PATH:
|
|
293
|
+
environment_variables[env] = mapped_input
|
|
294
|
+
elif env == EnvironmentVariables.HOLOSCAN_OUTPUT_PATH:
|
|
295
|
+
environment_variables[env] = mapped_output
|
|
296
|
+
else:
|
|
297
|
+
environment_variables[env] = app_info["environment"][env]
|
|
298
|
+
|
|
299
|
+
# always pass path to config file for Holoscan apps
|
|
300
|
+
if (
|
|
301
|
+
"sdk" in app_info
|
|
302
|
+
and app_info["sdk"] == SdkType.Holoscan.value
|
|
303
|
+
and env == EnvironmentVariables.HOLOSCAN_CONFIG_PATH
|
|
304
|
+
):
|
|
305
|
+
commands.append("--config")
|
|
306
|
+
commands.append(environment_variables[env])
|
|
307
|
+
|
|
308
|
+
if config is not None:
|
|
309
|
+
if EnvironmentVariables.HOLOSCAN_CONFIG_PATH not in app_info["environment"]:
|
|
310
|
+
raise InvalidManifestError(
|
|
311
|
+
"The application manifest does not contain a required "
|
|
312
|
+
f"environment variable: '{EnvironmentVariables.HOLOSCAN_CONFIG_PATH}'"
|
|
313
|
+
)
|
|
314
|
+
volumes.append(
|
|
315
|
+
(
|
|
316
|
+
str(config),
|
|
317
|
+
app_info["environment"][EnvironmentVariables.HOLOSCAN_CONFIG_PATH],
|
|
318
|
+
)
|
|
319
|
+
)
|
|
320
|
+
logger.info(f"Using user provided configuration file: {config}")
|
|
321
|
+
|
|
322
|
+
logger.debug(
|
|
323
|
+
f"Environment variables: {json.dumps(environment_variables, indent=4, sort_keys=True)}"
|
|
324
|
+
)
|
|
325
|
+
logger.debug(f"Volumes: {json.dumps(volumes, indent=4, sort_keys=True)}")
|
|
326
|
+
logger.debug(f"Shared memory size: {shared_memory_size}")
|
|
327
|
+
|
|
328
|
+
ipc_mode = "host" if shared_memory_size is None else None
|
|
329
|
+
ulimits = [
|
|
330
|
+
"memlock=-1",
|
|
331
|
+
"stack=67108864",
|
|
332
|
+
]
|
|
333
|
+
additional_devices, group_adds = _additional_devices_to_mount(is_root)
|
|
334
|
+
devices.extend(additional_devices)
|
|
335
|
+
|
|
336
|
+
video_group = run_cmd_output(["/usr/bin/cat", "/etc/group"], "video").split(":")[2]
|
|
337
|
+
if not is_root and video_group not in group_adds:
|
|
338
|
+
group_adds.append(video_group)
|
|
339
|
+
|
|
340
|
+
if terminal:
|
|
341
|
+
_enter_terminal(
|
|
342
|
+
name,
|
|
343
|
+
image_name,
|
|
344
|
+
app_info,
|
|
345
|
+
network,
|
|
346
|
+
user,
|
|
347
|
+
volumes,
|
|
348
|
+
environment_variables,
|
|
349
|
+
shared_memory_size,
|
|
350
|
+
ipc_mode,
|
|
351
|
+
ulimits,
|
|
352
|
+
devices,
|
|
353
|
+
group_adds,
|
|
354
|
+
)
|
|
355
|
+
else:
|
|
356
|
+
_start_container(
|
|
357
|
+
name,
|
|
358
|
+
image_name,
|
|
359
|
+
app_info,
|
|
360
|
+
quiet,
|
|
361
|
+
commands,
|
|
362
|
+
network,
|
|
363
|
+
user,
|
|
364
|
+
volumes,
|
|
365
|
+
environment_variables,
|
|
366
|
+
shared_memory_size,
|
|
367
|
+
ipc_mode,
|
|
368
|
+
ulimits,
|
|
369
|
+
devices,
|
|
370
|
+
group_adds,
|
|
371
|
+
)
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
def _start_container(
|
|
375
|
+
name,
|
|
376
|
+
image_name,
|
|
377
|
+
app_info,
|
|
378
|
+
quiet,
|
|
379
|
+
commands,
|
|
380
|
+
network,
|
|
381
|
+
user,
|
|
382
|
+
volumes,
|
|
383
|
+
environment_variables,
|
|
384
|
+
shared_memory_size,
|
|
385
|
+
ipc_mode,
|
|
386
|
+
ulimits,
|
|
387
|
+
devices,
|
|
388
|
+
group_adds,
|
|
389
|
+
):
|
|
390
|
+
container = docker.container.create(
|
|
391
|
+
image_name,
|
|
392
|
+
command=commands,
|
|
393
|
+
envs=environment_variables,
|
|
394
|
+
hostname=name,
|
|
395
|
+
name=name,
|
|
396
|
+
networks=[network],
|
|
397
|
+
remove=True,
|
|
398
|
+
shm_size=shared_memory_size,
|
|
399
|
+
user=user,
|
|
400
|
+
volumes=volumes,
|
|
401
|
+
workdir=app_info["workingDirectory"],
|
|
402
|
+
ipc=ipc_mode,
|
|
403
|
+
cap_add=["CAP_SYS_PTRACE"],
|
|
404
|
+
ulimit=ulimits,
|
|
405
|
+
devices=devices,
|
|
406
|
+
groups_add=group_adds,
|
|
407
|
+
runtime="nvidia",
|
|
408
|
+
)
|
|
409
|
+
container_name = container.name
|
|
410
|
+
container_id = container.id[:12]
|
|
411
|
+
|
|
412
|
+
ulimit_str = ", ".join(
|
|
413
|
+
f"{ulimit.name}={ulimit.soft}:{ulimit.hard}"
|
|
414
|
+
for ulimit in container.host_config.ulimits
|
|
415
|
+
)
|
|
416
|
+
logger.info(
|
|
417
|
+
f"Launching container ({container_id}) using image '{image_name}'..."
|
|
418
|
+
f"\n container name: {container_name}"
|
|
419
|
+
f"\n host name: {container.config.hostname}"
|
|
420
|
+
f"\n network: {network}"
|
|
421
|
+
f"\n user: {container.config.user}"
|
|
422
|
+
f"\n ulimits: {ulimit_str}"
|
|
423
|
+
f"\n cap_add: {', '.join(container.host_config.cap_add)}"
|
|
424
|
+
f"\n ipc mode: {container.host_config.ipc_mode}"
|
|
425
|
+
f"\n shared memory size: {container.host_config.shm_size}"
|
|
426
|
+
f"\n devices: {', '.join(devices)}"
|
|
427
|
+
f"\n group_add: {', '.join(group_adds)}"
|
|
428
|
+
)
|
|
429
|
+
logs = container.start(
|
|
430
|
+
attach=True,
|
|
431
|
+
stream=True,
|
|
432
|
+
)
|
|
433
|
+
|
|
434
|
+
for log in logs:
|
|
435
|
+
if log[0] == "stdout":
|
|
436
|
+
if not quiet:
|
|
437
|
+
print(log[1].decode("utf-8"))
|
|
438
|
+
elif log[0] == "stderr":
|
|
439
|
+
try:
|
|
440
|
+
print(str(log[1].decode("utf-8")))
|
|
441
|
+
except Exception:
|
|
442
|
+
print(str(log[1]))
|
|
443
|
+
|
|
444
|
+
logger.info(f"Container '{container_name}'({container_id}) exited.")
|
|
445
|
+
|
|
446
|
+
|
|
447
|
+
def _enter_terminal(
|
|
448
|
+
name,
|
|
449
|
+
image_name,
|
|
450
|
+
app_info,
|
|
451
|
+
network,
|
|
452
|
+
user,
|
|
453
|
+
volumes,
|
|
454
|
+
environment_variables,
|
|
455
|
+
shared_memory_size,
|
|
456
|
+
ipc_mode,
|
|
457
|
+
ulimits,
|
|
458
|
+
devices,
|
|
459
|
+
group_adds,
|
|
460
|
+
):
|
|
461
|
+
print("\n\nEntering terminal...")
|
|
462
|
+
print(
|
|
463
|
+
"\n".join(
|
|
464
|
+
f"\t{k:25s}\t{v}"
|
|
465
|
+
for k, v in sorted(environment_variables.items(), key=lambda t: str(t[0]))
|
|
466
|
+
)
|
|
467
|
+
)
|
|
468
|
+
print("\n\n")
|
|
469
|
+
docker.container.run(
|
|
470
|
+
image_name,
|
|
471
|
+
detach=False,
|
|
472
|
+
entrypoint="/bin/bash",
|
|
473
|
+
envs=environment_variables,
|
|
474
|
+
hostname=name,
|
|
475
|
+
interactive=True,
|
|
476
|
+
name=name,
|
|
477
|
+
networks=[network],
|
|
478
|
+
remove=True,
|
|
479
|
+
shm_size=shared_memory_size,
|
|
480
|
+
tty=True,
|
|
481
|
+
user=user,
|
|
482
|
+
volumes=volumes,
|
|
483
|
+
workdir=app_info["workingDirectory"],
|
|
484
|
+
ipc=ipc_mode,
|
|
485
|
+
cap_add=["CAP_SYS_PTRACE"],
|
|
486
|
+
ulimit=ulimits,
|
|
487
|
+
devices=devices,
|
|
488
|
+
groups_add=group_adds,
|
|
489
|
+
runtime="nvidia",
|
|
490
|
+
)
|
|
491
|
+
logger.info("Container exited.")
|
|
492
|
+
|
|
493
|
+
|
|
494
|
+
def _additional_devices_to_mount(is_root: bool):
|
|
495
|
+
"""Mounts additional devices"""
|
|
496
|
+
devices = []
|
|
497
|
+
group_adds = []
|
|
498
|
+
|
|
499
|
+
# On iGPU, the /dev/dri/* devices (mounted by the NV container runtime) permissions require root
|
|
500
|
+
# privilege or to be part of the `video` and `render` groups. The ID for these group names might
|
|
501
|
+
# differ on the host system and in the container, so we need to pass the group ID instead of the
|
|
502
|
+
# group name when running docker.
|
|
503
|
+
if (
|
|
504
|
+
os.path.exists("/sys/devices/platform/gpu.0/load")
|
|
505
|
+
and os.path.exists("/usr/bin/tegrastats")
|
|
506
|
+
and not is_root
|
|
507
|
+
):
|
|
508
|
+
group = run_cmd_output(["/usr/bin/cat", "/etc/group"], "video").split(":")[2]
|
|
509
|
+
group_adds.append(group)
|
|
510
|
+
group = run_cmd_output(["/usr/bin/cat", "/etc/group"], "render").split(":")[2]
|
|
511
|
+
group_adds.append(group)
|
|
512
|
+
return (devices, group_adds)
|
|
513
|
+
|
|
514
|
+
|
|
515
|
+
def _host_is_native_igpu() -> bool:
|
|
516
|
+
proc = subprocess.run(
|
|
517
|
+
["nvidia-smi", "--query-gpu", "name", "--format=csv,noheader"],
|
|
518
|
+
shell=False,
|
|
519
|
+
capture_output=True,
|
|
520
|
+
)
|
|
521
|
+
return "nvgpu" in str(proc.stdout)
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: Copyright (c) 2023-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
#
|
|
4
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
# you may not use this file except in compliance with the License.
|
|
6
|
+
# You may obtain a copy of the License at
|
|
7
|
+
#
|
|
8
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
#
|
|
10
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
# See the License for the specific language governing permissions and
|
|
14
|
+
# limitations under the License.
|
|
15
|
+
from enum import Enum
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class ApplicationType(Enum):
|
|
19
|
+
PythonModule = "Python Module"
|
|
20
|
+
PythonFile = "Python File"
|
|
21
|
+
CppCMake = "C++"
|
|
22
|
+
Binary = "Binary"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class SdkType(Enum):
|
|
26
|
+
"""
|
|
27
|
+
Note the values assigned are used as entry_points in setup.py for detecting the SDK to use.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
Holoscan = "holoscan"
|
|
31
|
+
MonaiDeploy = "monai-deploy"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class Arch(Enum):
|
|
35
|
+
amd64 = "linux/amd64"
|
|
36
|
+
arm64 = "linux/arm64"
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class Platform(Enum):
|
|
40
|
+
IGXOrinDevIt = "igx-orin-devkit"
|
|
41
|
+
JetsonAgxOrinDevKit = "jetson-agx-orin-devkit"
|
|
42
|
+
X64Workstation = "x64-workstation"
|
|
43
|
+
SBSA = "sbsa"
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class PlatformConfiguration(Enum):
|
|
47
|
+
iGPU = "igpu" # noqa: N815
|
|
48
|
+
dGPU = "dgpu" # noqa: N815
|
|
49
|
+
CPU = "cpu"
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: Copyright (c) 2023-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
#
|
|
4
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
# you may not use this file except in compliance with the License.
|
|
6
|
+
# You may obtain a copy of the License at
|
|
7
|
+
#
|
|
8
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
#
|
|
10
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
# See the License for the specific language governing permissions and
|
|
14
|
+
# limitations under the License.
|
|
15
|
+
class HoloscanSdkError(Exception):
|
|
16
|
+
"""Base class for exceptions in this module."""
|
|
17
|
+
|
|
18
|
+
pass
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class WrongApplicationPathError(HoloscanSdkError):
|
|
22
|
+
"""Raise when wrong application path is specified."""
|
|
23
|
+
|
|
24
|
+
pass
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class UnknownApplicationTypeError(HoloscanSdkError):
|
|
28
|
+
"""Raise when wrong application path is specified."""
|
|
29
|
+
|
|
30
|
+
pass
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class InvalidSdkError(HoloscanSdkError):
|
|
34
|
+
"""Raise when the SDK version or SDK file is not supported."""
|
|
35
|
+
|
|
36
|
+
pass
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class FailedToDetectSDKVersionError(HoloscanSdkError):
|
|
40
|
+
"""Raise when unable to detect the SDK version."""
|
|
41
|
+
|
|
42
|
+
pass
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class InvalidApplicationConfigurationError(HoloscanSdkError):
|
|
46
|
+
"""
|
|
47
|
+
Raise when required configuration value cannot be found
|
|
48
|
+
in the application configuration files."""
|
|
49
|
+
|
|
50
|
+
pass
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class IncompatiblePlatformConfigurationError(HoloscanSdkError):
|
|
54
|
+
"""
|
|
55
|
+
Raise when the platforms given by the user are incompatible."""
|
|
56
|
+
|
|
57
|
+
pass
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class RunContainerError(HoloscanSdkError):
|
|
61
|
+
"""
|
|
62
|
+
Raise when an error is encountered while running the container image."""
|
|
63
|
+
|
|
64
|
+
pass
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class InvalidManifestError(HoloscanSdkError):
|
|
68
|
+
"""
|
|
69
|
+
Raise when the manifest is invalid."""
|
|
70
|
+
|
|
71
|
+
pass
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class ManifestReadError(HoloscanSdkError):
|
|
75
|
+
"""
|
|
76
|
+
Raise when the manifest is invalid."""
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class ExternalAssetDownloadError(HoloscanSdkError):
|
|
80
|
+
"""
|
|
81
|
+
Raise when the manifest is invalid."""
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class InvalidSourceFileError(HoloscanSdkError):
|
|
85
|
+
"""
|
|
86
|
+
Raise when the provided artifact source file is invalid."""
|
|
87
|
+
|
|
88
|
+
pass
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
class InvalidTagValueError(HoloscanSdkError):
|
|
92
|
+
"""
|
|
93
|
+
Raise when the Docker tag is invalid."""
|
|
94
|
+
|
|
95
|
+
pass
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
class InvalidSharedMemoryValueError(HoloscanSdkError):
|
|
99
|
+
"""
|
|
100
|
+
Raise when the shared memory value is invalid."""
|
|
101
|
+
|
|
102
|
+
pass
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
class ManifestDownloadError(HoloscanSdkError):
|
|
106
|
+
"""
|
|
107
|
+
Raise when the failed to download manifest file."""
|
|
108
|
+
|
|
109
|
+
pass
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
class UnmatchedDeviceError(HoloscanSdkError):
|
|
113
|
+
"""
|
|
114
|
+
Raise when the shared memory value is invalid."""
|
|
115
|
+
|
|
116
|
+
def __init__(self, unmatched_devices: list[str], *args: object) -> None:
|
|
117
|
+
super().__init__(
|
|
118
|
+
f"The following devices cannot be found in /dev/: {str.join(',', unmatched_devices)}"
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
class GpuResourceError(HoloscanSdkError):
|
|
123
|
+
"""
|
|
124
|
+
Raise when the available GPUs are less than requetsed."""
|
|
125
|
+
|
|
126
|
+
pass
|