viettelcloud-aiplatform 0.3.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.
- viettelcloud/__init__.py +1 -0
- viettelcloud/aiplatform/__init__.py +15 -0
- viettelcloud/aiplatform/common/__init__.py +0 -0
- viettelcloud/aiplatform/common/constants.py +22 -0
- viettelcloud/aiplatform/common/types.py +28 -0
- viettelcloud/aiplatform/common/utils.py +40 -0
- viettelcloud/aiplatform/hub/OWNERS +14 -0
- viettelcloud/aiplatform/hub/__init__.py +25 -0
- viettelcloud/aiplatform/hub/api/__init__.py +13 -0
- viettelcloud/aiplatform/hub/api/_proxy_client.py +355 -0
- viettelcloud/aiplatform/hub/api/model_registry_client.py +561 -0
- viettelcloud/aiplatform/hub/api/model_registry_client_test.py +462 -0
- viettelcloud/aiplatform/optimizer/__init__.py +45 -0
- viettelcloud/aiplatform/optimizer/api/__init__.py +0 -0
- viettelcloud/aiplatform/optimizer/api/optimizer_client.py +248 -0
- viettelcloud/aiplatform/optimizer/backends/__init__.py +13 -0
- viettelcloud/aiplatform/optimizer/backends/base.py +77 -0
- viettelcloud/aiplatform/optimizer/backends/kubernetes/__init__.py +13 -0
- viettelcloud/aiplatform/optimizer/backends/kubernetes/backend.py +563 -0
- viettelcloud/aiplatform/optimizer/backends/kubernetes/utils.py +112 -0
- viettelcloud/aiplatform/optimizer/constants/__init__.py +13 -0
- viettelcloud/aiplatform/optimizer/constants/constants.py +59 -0
- viettelcloud/aiplatform/optimizer/types/__init__.py +13 -0
- viettelcloud/aiplatform/optimizer/types/algorithm_types.py +87 -0
- viettelcloud/aiplatform/optimizer/types/optimization_types.py +135 -0
- viettelcloud/aiplatform/optimizer/types/search_types.py +95 -0
- viettelcloud/aiplatform/py.typed +0 -0
- viettelcloud/aiplatform/trainer/__init__.py +82 -0
- viettelcloud/aiplatform/trainer/api/__init__.py +3 -0
- viettelcloud/aiplatform/trainer/api/trainer_client.py +277 -0
- viettelcloud/aiplatform/trainer/api/trainer_client_test.py +72 -0
- viettelcloud/aiplatform/trainer/backends/__init__.py +0 -0
- viettelcloud/aiplatform/trainer/backends/base.py +94 -0
- viettelcloud/aiplatform/trainer/backends/container/adapters/base.py +195 -0
- viettelcloud/aiplatform/trainer/backends/container/adapters/docker.py +231 -0
- viettelcloud/aiplatform/trainer/backends/container/adapters/podman.py +258 -0
- viettelcloud/aiplatform/trainer/backends/container/backend.py +668 -0
- viettelcloud/aiplatform/trainer/backends/container/backend_test.py +867 -0
- viettelcloud/aiplatform/trainer/backends/container/runtime_loader.py +631 -0
- viettelcloud/aiplatform/trainer/backends/container/runtime_loader_test.py +637 -0
- viettelcloud/aiplatform/trainer/backends/container/types.py +67 -0
- viettelcloud/aiplatform/trainer/backends/container/utils.py +213 -0
- viettelcloud/aiplatform/trainer/backends/kubernetes/__init__.py +0 -0
- viettelcloud/aiplatform/trainer/backends/kubernetes/backend.py +710 -0
- viettelcloud/aiplatform/trainer/backends/kubernetes/backend_test.py +1344 -0
- viettelcloud/aiplatform/trainer/backends/kubernetes/constants.py +15 -0
- viettelcloud/aiplatform/trainer/backends/kubernetes/utils.py +636 -0
- viettelcloud/aiplatform/trainer/backends/kubernetes/utils_test.py +582 -0
- viettelcloud/aiplatform/trainer/backends/localprocess/__init__.py +0 -0
- viettelcloud/aiplatform/trainer/backends/localprocess/backend.py +306 -0
- viettelcloud/aiplatform/trainer/backends/localprocess/backend_test.py +501 -0
- viettelcloud/aiplatform/trainer/backends/localprocess/constants.py +90 -0
- viettelcloud/aiplatform/trainer/backends/localprocess/job.py +184 -0
- viettelcloud/aiplatform/trainer/backends/localprocess/types.py +52 -0
- viettelcloud/aiplatform/trainer/backends/localprocess/utils.py +302 -0
- viettelcloud/aiplatform/trainer/constants/__init__.py +0 -0
- viettelcloud/aiplatform/trainer/constants/constants.py +179 -0
- viettelcloud/aiplatform/trainer/options/__init__.py +52 -0
- viettelcloud/aiplatform/trainer/options/common.py +55 -0
- viettelcloud/aiplatform/trainer/options/kubernetes.py +502 -0
- viettelcloud/aiplatform/trainer/options/kubernetes_test.py +259 -0
- viettelcloud/aiplatform/trainer/options/localprocess.py +20 -0
- viettelcloud/aiplatform/trainer/test/common.py +22 -0
- viettelcloud/aiplatform/trainer/types/__init__.py +0 -0
- viettelcloud/aiplatform/trainer/types/types.py +517 -0
- viettelcloud/aiplatform/trainer/types/types_test.py +115 -0
- viettelcloud_aiplatform-0.3.0.dist-info/METADATA +226 -0
- viettelcloud_aiplatform-0.3.0.dist-info/RECORD +71 -0
- viettelcloud_aiplatform-0.3.0.dist-info/WHEEL +4 -0
- viettelcloud_aiplatform-0.3.0.dist-info/licenses/LICENSE +201 -0
- viettelcloud_aiplatform-0.3.0.dist-info/licenses/NOTICE +36 -0
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
# Copyright 2025 The Kubeflow Authors.
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
"""
|
|
16
|
+
Docker client adapter implementation.
|
|
17
|
+
|
|
18
|
+
This module provides the DockerClientAdapter class that implements the
|
|
19
|
+
BaseContainerClientAdapter interface for Docker runtime.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from collections.abc import Iterator
|
|
23
|
+
from typing import Optional
|
|
24
|
+
|
|
25
|
+
from viettelcloud.aiplatform.trainer.backends.container.adapters.base import (
|
|
26
|
+
BaseContainerClientAdapter,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class DockerClientAdapter(BaseContainerClientAdapter):
|
|
31
|
+
"""Adapter for Docker client."""
|
|
32
|
+
|
|
33
|
+
def __init__(self, host: Optional[str] = None):
|
|
34
|
+
"""
|
|
35
|
+
Initialize Docker client.
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
host: Docker host URL, or None to use environment defaults
|
|
39
|
+
"""
|
|
40
|
+
try:
|
|
41
|
+
import docker # type: ignore
|
|
42
|
+
except ImportError as e:
|
|
43
|
+
raise ImportError(
|
|
44
|
+
"The 'docker' Python package is not installed. Install with extras: "
|
|
45
|
+
"pip install viettelcloud-aiplatform[docker]"
|
|
46
|
+
) from e
|
|
47
|
+
|
|
48
|
+
if host:
|
|
49
|
+
self.client = docker.DockerClient(base_url=host)
|
|
50
|
+
else:
|
|
51
|
+
self.client = docker.from_env()
|
|
52
|
+
|
|
53
|
+
self._runtime_type = "docker"
|
|
54
|
+
|
|
55
|
+
def ping(self):
|
|
56
|
+
"""Test connection to Docker daemon."""
|
|
57
|
+
self.client.ping()
|
|
58
|
+
|
|
59
|
+
def create_network(self, name: str, labels: dict[str, str]) -> str:
|
|
60
|
+
"""Create a Docker network."""
|
|
61
|
+
try:
|
|
62
|
+
self.client.networks.get(name)
|
|
63
|
+
return name
|
|
64
|
+
except Exception:
|
|
65
|
+
pass
|
|
66
|
+
|
|
67
|
+
self.client.networks.create(
|
|
68
|
+
name=name,
|
|
69
|
+
check_duplicate=True,
|
|
70
|
+
labels=labels,
|
|
71
|
+
)
|
|
72
|
+
return name
|
|
73
|
+
|
|
74
|
+
def delete_network(self, network_id: str):
|
|
75
|
+
"""Delete Docker network."""
|
|
76
|
+
try:
|
|
77
|
+
net = self.client.networks.get(network_id)
|
|
78
|
+
net.remove()
|
|
79
|
+
except Exception:
|
|
80
|
+
pass
|
|
81
|
+
|
|
82
|
+
def create_and_start_container(
|
|
83
|
+
self,
|
|
84
|
+
image: str,
|
|
85
|
+
command: list[str],
|
|
86
|
+
name: str,
|
|
87
|
+
network_id: str,
|
|
88
|
+
environment: dict[str, str],
|
|
89
|
+
labels: dict[str, str],
|
|
90
|
+
volumes: dict[str, dict[str, str]],
|
|
91
|
+
working_dir: str,
|
|
92
|
+
) -> str:
|
|
93
|
+
"""Create and start a Docker container."""
|
|
94
|
+
container = self.client.containers.run(
|
|
95
|
+
image=image,
|
|
96
|
+
command=tuple(command),
|
|
97
|
+
name=name,
|
|
98
|
+
detach=True,
|
|
99
|
+
working_dir=working_dir,
|
|
100
|
+
network=network_id,
|
|
101
|
+
environment=environment,
|
|
102
|
+
labels=labels,
|
|
103
|
+
volumes=volumes,
|
|
104
|
+
auto_remove=False,
|
|
105
|
+
)
|
|
106
|
+
return container.id
|
|
107
|
+
|
|
108
|
+
def get_container(self, container_id: str):
|
|
109
|
+
"""Get Docker container by ID."""
|
|
110
|
+
return self.client.containers.get(container_id)
|
|
111
|
+
|
|
112
|
+
def container_logs(self, container_id: str, follow: bool) -> Iterator[str]:
|
|
113
|
+
"""Stream logs from Docker container."""
|
|
114
|
+
container = self.get_container(container_id)
|
|
115
|
+
logs = container.logs(stream=bool(follow), follow=bool(follow))
|
|
116
|
+
if follow:
|
|
117
|
+
for chunk in logs:
|
|
118
|
+
if isinstance(chunk, bytes):
|
|
119
|
+
yield chunk.decode("utf-8", errors="ignore")
|
|
120
|
+
else:
|
|
121
|
+
yield str(chunk)
|
|
122
|
+
else:
|
|
123
|
+
if isinstance(logs, bytes):
|
|
124
|
+
yield logs.decode("utf-8", errors="ignore")
|
|
125
|
+
else:
|
|
126
|
+
yield str(logs)
|
|
127
|
+
|
|
128
|
+
def stop_container(self, container_id: str, timeout: int = 10):
|
|
129
|
+
"""Stop Docker container."""
|
|
130
|
+
container = self.get_container(container_id)
|
|
131
|
+
container.stop(timeout=timeout)
|
|
132
|
+
|
|
133
|
+
def remove_container(self, container_id: str, force: bool = True):
|
|
134
|
+
"""Remove Docker container."""
|
|
135
|
+
container = self.get_container(container_id)
|
|
136
|
+
container.remove(force=force)
|
|
137
|
+
|
|
138
|
+
def pull_image(self, image: str):
|
|
139
|
+
"""Pull Docker image."""
|
|
140
|
+
self.client.images.pull(image)
|
|
141
|
+
|
|
142
|
+
def image_exists(self, image: str) -> bool:
|
|
143
|
+
"""Check if Docker image exists locally."""
|
|
144
|
+
try:
|
|
145
|
+
self.client.images.get(image)
|
|
146
|
+
return True
|
|
147
|
+
except Exception:
|
|
148
|
+
return False
|
|
149
|
+
|
|
150
|
+
def run_oneoff_container(self, image: str, command: list[str]) -> str:
|
|
151
|
+
"""Run a short-lived Docker container and return output."""
|
|
152
|
+
try:
|
|
153
|
+
output = self.client.containers.run(
|
|
154
|
+
image=image,
|
|
155
|
+
command=tuple(command),
|
|
156
|
+
detach=False,
|
|
157
|
+
remove=True,
|
|
158
|
+
)
|
|
159
|
+
if isinstance(output, (bytes, bytearray)):
|
|
160
|
+
return output.decode("utf-8", errors="ignore")
|
|
161
|
+
return str(output)
|
|
162
|
+
except Exception as e:
|
|
163
|
+
raise RuntimeError(f"One-off container failed to run: {e}") from e
|
|
164
|
+
|
|
165
|
+
def container_status(self, container_id: str) -> tuple[str, Optional[int]]:
|
|
166
|
+
"""Get Docker container status."""
|
|
167
|
+
try:
|
|
168
|
+
container = self.get_container(container_id)
|
|
169
|
+
status = container.status
|
|
170
|
+
# Get exit code if container has exited
|
|
171
|
+
exit_code = None
|
|
172
|
+
if status == "exited":
|
|
173
|
+
inspect = container.attrs if hasattr(container, "attrs") else container.inspect()
|
|
174
|
+
exit_code = inspect.get("State", {}).get("ExitCode")
|
|
175
|
+
return (status, exit_code)
|
|
176
|
+
except Exception:
|
|
177
|
+
return ("unknown", None)
|
|
178
|
+
|
|
179
|
+
def get_container_ip(self, container_id: str, network_id: str) -> Optional[str]:
|
|
180
|
+
"""Get container's IP address on a specific network."""
|
|
181
|
+
try:
|
|
182
|
+
container = self.get_container(container_id)
|
|
183
|
+
# Refresh container info
|
|
184
|
+
container.reload()
|
|
185
|
+
# Get network settings
|
|
186
|
+
networks = container.attrs.get("NetworkSettings", {}).get("Networks", {})
|
|
187
|
+
|
|
188
|
+
# Try to find the network by exact name or ID
|
|
189
|
+
if network_id in networks:
|
|
190
|
+
return networks[network_id].get("IPAddress")
|
|
191
|
+
|
|
192
|
+
# Fallback: return first available IP
|
|
193
|
+
for _net_name, net_info in networks.items():
|
|
194
|
+
ip = net_info.get("IPAddress")
|
|
195
|
+
if ip:
|
|
196
|
+
return ip
|
|
197
|
+
|
|
198
|
+
return None
|
|
199
|
+
except Exception:
|
|
200
|
+
return None
|
|
201
|
+
|
|
202
|
+
def list_containers(self, filters: Optional[dict[str, list[str]]] = None) -> list[dict]:
|
|
203
|
+
"""List Docker containers with optional filters."""
|
|
204
|
+
try:
|
|
205
|
+
containers = self.client.containers.list(all=True, filters=filters)
|
|
206
|
+
result = []
|
|
207
|
+
for c in containers:
|
|
208
|
+
result.append(
|
|
209
|
+
{
|
|
210
|
+
"id": c.id,
|
|
211
|
+
"name": c.name,
|
|
212
|
+
"labels": c.labels,
|
|
213
|
+
"status": c.status,
|
|
214
|
+
"created": c.attrs.get("Created", ""),
|
|
215
|
+
}
|
|
216
|
+
)
|
|
217
|
+
return result
|
|
218
|
+
except Exception:
|
|
219
|
+
return []
|
|
220
|
+
|
|
221
|
+
def get_network(self, network_id: str) -> Optional[dict]:
|
|
222
|
+
"""Get Docker network information."""
|
|
223
|
+
try:
|
|
224
|
+
network = self.client.networks.get(network_id)
|
|
225
|
+
return {
|
|
226
|
+
"id": network.id,
|
|
227
|
+
"name": network.name,
|
|
228
|
+
"labels": network.attrs.get("Labels", {}),
|
|
229
|
+
}
|
|
230
|
+
except Exception:
|
|
231
|
+
return None
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
# Copyright 2025 The Kubeflow Authors.
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
"""
|
|
16
|
+
Podman client adapter implementation.
|
|
17
|
+
|
|
18
|
+
This module provides the PodmanClientAdapter class that implements the
|
|
19
|
+
BaseContainerClientAdapter interface for Podman runtime.
|
|
20
|
+
|
|
21
|
+
Key differences from Docker:
|
|
22
|
+
- Uses DNS-enabled bridge networks for better container name resolution
|
|
23
|
+
- GPU support via CDI (Container Device Interface) instead of NVIDIA Container Toolkit
|
|
24
|
+
- Slightly different API for some operations (e.g., container.create + start pattern)
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
from collections.abc import Iterator
|
|
28
|
+
from typing import Optional
|
|
29
|
+
|
|
30
|
+
from viettelcloud.aiplatform.trainer.backends.container.adapters.base import (
|
|
31
|
+
BaseContainerClientAdapter,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class PodmanClientAdapter(BaseContainerClientAdapter):
|
|
36
|
+
"""Adapter for Podman client."""
|
|
37
|
+
|
|
38
|
+
def __init__(self, host: Optional[str] = None):
|
|
39
|
+
"""
|
|
40
|
+
Initialize Podman client.
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
host: Podman host URL, or None to use environment defaults
|
|
44
|
+
"""
|
|
45
|
+
try:
|
|
46
|
+
import podman # type: ignore
|
|
47
|
+
except ImportError as e:
|
|
48
|
+
raise ImportError(
|
|
49
|
+
"The 'podman' Python package is not installed. Install with extras: "
|
|
50
|
+
"pip install viettelcloud-aiplatform[podman]"
|
|
51
|
+
) from e
|
|
52
|
+
|
|
53
|
+
if host:
|
|
54
|
+
self.client = podman.PodmanClient(base_url=host)
|
|
55
|
+
else:
|
|
56
|
+
self.client = podman.PodmanClient()
|
|
57
|
+
|
|
58
|
+
self._runtime_type = "podman"
|
|
59
|
+
|
|
60
|
+
def ping(self):
|
|
61
|
+
"""Test connection to Podman."""
|
|
62
|
+
self.client.ping()
|
|
63
|
+
|
|
64
|
+
def create_network(self, name: str, labels: dict[str, str]) -> str:
|
|
65
|
+
"""Create a Podman network with DNS enabled."""
|
|
66
|
+
try:
|
|
67
|
+
self.client.networks.get(name)
|
|
68
|
+
return name
|
|
69
|
+
except Exception:
|
|
70
|
+
pass
|
|
71
|
+
|
|
72
|
+
self.client.networks.create(
|
|
73
|
+
name=name,
|
|
74
|
+
driver="bridge",
|
|
75
|
+
dns_enabled=True,
|
|
76
|
+
labels=labels,
|
|
77
|
+
)
|
|
78
|
+
return name
|
|
79
|
+
|
|
80
|
+
def delete_network(self, network_id: str):
|
|
81
|
+
"""Delete Podman network."""
|
|
82
|
+
try:
|
|
83
|
+
net = self.client.networks.get(network_id)
|
|
84
|
+
net.remove()
|
|
85
|
+
except Exception:
|
|
86
|
+
pass
|
|
87
|
+
|
|
88
|
+
def create_and_start_container(
|
|
89
|
+
self,
|
|
90
|
+
image: str,
|
|
91
|
+
command: list[str],
|
|
92
|
+
name: str,
|
|
93
|
+
network_id: str,
|
|
94
|
+
environment: dict[str, str],
|
|
95
|
+
labels: dict[str, str],
|
|
96
|
+
volumes: dict[str, dict[str, str]],
|
|
97
|
+
working_dir: str,
|
|
98
|
+
) -> str:
|
|
99
|
+
"""Create and start a Podman container."""
|
|
100
|
+
container = self.client.containers.run(
|
|
101
|
+
image=image,
|
|
102
|
+
command=command,
|
|
103
|
+
name=name,
|
|
104
|
+
network=network_id,
|
|
105
|
+
working_dir=working_dir,
|
|
106
|
+
environment=environment,
|
|
107
|
+
labels=labels,
|
|
108
|
+
volumes=volumes,
|
|
109
|
+
detach=True,
|
|
110
|
+
remove=False,
|
|
111
|
+
)
|
|
112
|
+
return container.id
|
|
113
|
+
|
|
114
|
+
def get_container(self, container_id: str):
|
|
115
|
+
"""Get Podman container by ID."""
|
|
116
|
+
return self.client.containers.get(container_id)
|
|
117
|
+
|
|
118
|
+
def container_logs(self, container_id: str, follow: bool) -> Iterator[str]:
|
|
119
|
+
"""Stream logs from Podman container."""
|
|
120
|
+
container = self.get_container(container_id)
|
|
121
|
+
logs = container.logs(stream=bool(follow), follow=bool(follow))
|
|
122
|
+
if follow:
|
|
123
|
+
for chunk in logs:
|
|
124
|
+
if isinstance(chunk, bytes):
|
|
125
|
+
yield chunk.decode("utf-8", errors="ignore")
|
|
126
|
+
else:
|
|
127
|
+
yield str(chunk)
|
|
128
|
+
else:
|
|
129
|
+
if isinstance(logs, bytes):
|
|
130
|
+
yield logs.decode("utf-8", errors="ignore")
|
|
131
|
+
else:
|
|
132
|
+
yield str(logs)
|
|
133
|
+
|
|
134
|
+
def stop_container(self, container_id: str, timeout: int = 10):
|
|
135
|
+
"""Stop Podman container."""
|
|
136
|
+
container = self.get_container(container_id)
|
|
137
|
+
container.stop(timeout=timeout)
|
|
138
|
+
|
|
139
|
+
def remove_container(self, container_id: str, force: bool = True):
|
|
140
|
+
"""Remove Podman container."""
|
|
141
|
+
container = self.get_container(container_id)
|
|
142
|
+
container.remove(force=force)
|
|
143
|
+
|
|
144
|
+
def pull_image(self, image: str):
|
|
145
|
+
"""Pull Podman image."""
|
|
146
|
+
self.client.images.pull(image)
|
|
147
|
+
|
|
148
|
+
def image_exists(self, image: str) -> bool:
|
|
149
|
+
"""Check if Podman image exists locally."""
|
|
150
|
+
try:
|
|
151
|
+
self.client.images.get(image)
|
|
152
|
+
return True
|
|
153
|
+
except Exception:
|
|
154
|
+
return False
|
|
155
|
+
|
|
156
|
+
def run_oneoff_container(self, image: str, command: list[str]) -> str:
|
|
157
|
+
"""Run a short-lived Podman container and return output."""
|
|
158
|
+
try:
|
|
159
|
+
container = self.client.containers.create(
|
|
160
|
+
image=image,
|
|
161
|
+
command=command,
|
|
162
|
+
detach=False,
|
|
163
|
+
remove=True,
|
|
164
|
+
)
|
|
165
|
+
container.start()
|
|
166
|
+
container.wait()
|
|
167
|
+
logs = container.logs()
|
|
168
|
+
|
|
169
|
+
if isinstance(logs, (bytes, bytearray)):
|
|
170
|
+
return logs.decode("utf-8", errors="ignore")
|
|
171
|
+
return str(logs)
|
|
172
|
+
except Exception as e:
|
|
173
|
+
raise RuntimeError(f"One-off container failed to run: {e}") from e
|
|
174
|
+
|
|
175
|
+
def container_status(self, container_id: str) -> tuple[str, Optional[int]]:
|
|
176
|
+
"""Get Podman container status."""
|
|
177
|
+
try:
|
|
178
|
+
container = self.get_container(container_id)
|
|
179
|
+
status = container.status
|
|
180
|
+
# Get exit code if container has exited
|
|
181
|
+
exit_code = None
|
|
182
|
+
if status == "exited":
|
|
183
|
+
inspect = container.attrs if hasattr(container, "attrs") else container.inspect()
|
|
184
|
+
exit_code = inspect.get("State", {}).get("ExitCode")
|
|
185
|
+
return (status, exit_code)
|
|
186
|
+
except Exception:
|
|
187
|
+
return ("unknown", None)
|
|
188
|
+
|
|
189
|
+
def get_container_ip(self, container_id: str, network_id: str) -> Optional[str]:
|
|
190
|
+
"""Get container's IP address on a specific network."""
|
|
191
|
+
try:
|
|
192
|
+
container = self.get_container(container_id)
|
|
193
|
+
# Get container inspect data
|
|
194
|
+
inspect = container.attrs if hasattr(container, "attrs") else container.inspect()
|
|
195
|
+
|
|
196
|
+
# Get network settings - Podman structure is similar to Docker
|
|
197
|
+
networks = inspect.get("NetworkSettings", {}).get("Networks", {})
|
|
198
|
+
|
|
199
|
+
# Try to find the network by exact name or ID
|
|
200
|
+
if network_id in networks:
|
|
201
|
+
return networks[network_id].get("IPAddress")
|
|
202
|
+
|
|
203
|
+
# Fallback: return first available IP
|
|
204
|
+
for _net_name, net_info in networks.items():
|
|
205
|
+
ip = net_info.get("IPAddress")
|
|
206
|
+
if ip:
|
|
207
|
+
return ip
|
|
208
|
+
|
|
209
|
+
return None
|
|
210
|
+
except Exception:
|
|
211
|
+
return None
|
|
212
|
+
|
|
213
|
+
def list_containers(self, filters: Optional[dict[str, list[str]]] = None) -> list[dict]:
|
|
214
|
+
"""List Podman containers with optional filters."""
|
|
215
|
+
# Work-around for https://github.com/containers/podman-py/issues/542
|
|
216
|
+
for k, v in filters.items():
|
|
217
|
+
if len(v) == 1:
|
|
218
|
+
filters[k] = v[0]
|
|
219
|
+
try:
|
|
220
|
+
containers = self.client.containers.list(all=True, filters=filters)
|
|
221
|
+
result = []
|
|
222
|
+
for c in containers:
|
|
223
|
+
# The container status needs to be reloaded when the container
|
|
224
|
+
# is retrieved from a list.
|
|
225
|
+
# See: https://github.com/containers/podman-py/issues/446
|
|
226
|
+
c.reload()
|
|
227
|
+
inspect = c.attrs if hasattr(c, "attrs") else c.inspect()
|
|
228
|
+
labels = (
|
|
229
|
+
c.labels
|
|
230
|
+
if hasattr(c, "labels")
|
|
231
|
+
else inspect.get("Config", {}).get("Labels", {})
|
|
232
|
+
)
|
|
233
|
+
result.append(
|
|
234
|
+
{
|
|
235
|
+
"id": c.id,
|
|
236
|
+
"name": c.name,
|
|
237
|
+
"labels": labels,
|
|
238
|
+
"status": c.status,
|
|
239
|
+
"created": inspect.get("Created", ""),
|
|
240
|
+
}
|
|
241
|
+
)
|
|
242
|
+
return result
|
|
243
|
+
except Exception:
|
|
244
|
+
return []
|
|
245
|
+
|
|
246
|
+
def get_network(self, network_id: str) -> Optional[dict]:
|
|
247
|
+
"""Get Podman network information."""
|
|
248
|
+
try:
|
|
249
|
+
network = self.client.networks.get(network_id)
|
|
250
|
+
inspect = network.attrs if hasattr(network, "attrs") else network.inspect()
|
|
251
|
+
return {
|
|
252
|
+
"id": inspect.get("ID", network_id),
|
|
253
|
+
"name": inspect.get("Name", network_id),
|
|
254
|
+
# The case is important for labels
|
|
255
|
+
"labels": inspect.get("labels", {}),
|
|
256
|
+
}
|
|
257
|
+
except Exception:
|
|
258
|
+
return None
|