container-manager-mcp 0.0.4__py3-none-any.whl → 1.2.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.
- container_manager_mcp/__init__.py +23 -15
- container_manager_mcp/__main__.py +6 -0
- container_manager_mcp/container_manager.py +1145 -309
- container_manager_mcp/container_manager_a2a.py +339 -0
- container_manager_mcp/container_manager_mcp.py +2061 -1110
- container_manager_mcp/mcp_config.json +7 -0
- container_manager_mcp/skills/container-manager-compose/SKILL.md +25 -0
- container_manager_mcp/skills/container-manager-containers/SKILL.md +28 -0
- container_manager_mcp/skills/container-manager-containers/troubleshoot.md +5 -0
- container_manager_mcp/skills/container-manager-images/SKILL.md +25 -0
- container_manager_mcp/skills/container-manager-info/SKILL.md +23 -0
- container_manager_mcp/skills/container-manager-logs/SKILL.md +22 -0
- container_manager_mcp/skills/container-manager-networks/SKILL.md +22 -0
- container_manager_mcp/skills/container-manager-swarm/SKILL.md +28 -0
- container_manager_mcp/skills/container-manager-swarm/orchestrate.md +4 -0
- container_manager_mcp/skills/container-manager-system/SKILL.md +19 -0
- container_manager_mcp/skills/container-manager-volumes/SKILL.md +23 -0
- container_manager_mcp/utils.py +31 -0
- container_manager_mcp-1.2.0.dist-info/METADATA +371 -0
- container_manager_mcp-1.2.0.dist-info/RECORD +26 -0
- container_manager_mcp-1.2.0.dist-info/entry_points.txt +4 -0
- {container_manager_mcp-0.0.4.dist-info → container_manager_mcp-1.2.0.dist-info}/top_level.txt +1 -0
- scripts/validate_a2a_agent.py +150 -0
- scripts/validate_agent.py +67 -0
- container_manager_mcp-0.0.4.dist-info/METADATA +0 -243
- container_manager_mcp-0.0.4.dist-info/RECORD +0 -9
- container_manager_mcp-0.0.4.dist-info/entry_points.txt +0 -3
- {container_manager_mcp-0.0.4.dist-info → container_manager_mcp-1.2.0.dist-info}/WHEEL +0 -0
- {container_manager_mcp-0.0.4.dist-info → container_manager_mcp-1.2.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -6,9 +6,13 @@ import logging
|
|
|
6
6
|
import os
|
|
7
7
|
from abc import ABC, abstractmethod
|
|
8
8
|
from typing import List, Dict, Optional, Any
|
|
9
|
-
import
|
|
9
|
+
import argparse
|
|
10
10
|
import json
|
|
11
|
+
import shutil
|
|
11
12
|
import subprocess
|
|
13
|
+
from datetime import datetime
|
|
14
|
+
import platform
|
|
15
|
+
import traceback
|
|
12
16
|
|
|
13
17
|
try:
|
|
14
18
|
import docker
|
|
@@ -26,6 +30,7 @@ except ImportError:
|
|
|
26
30
|
|
|
27
31
|
|
|
28
32
|
class ContainerManagerBase(ABC):
|
|
33
|
+
|
|
29
34
|
def __init__(self, silent: bool = False, log_file: str = None):
|
|
30
35
|
self.silent = silent
|
|
31
36
|
self.setup_logging(log_file)
|
|
@@ -36,7 +41,7 @@ class ContainerManagerBase(ABC):
|
|
|
36
41
|
log_file = os.path.join(script_dir, "container_manager.log")
|
|
37
42
|
logging.basicConfig(
|
|
38
43
|
filename=log_file,
|
|
39
|
-
level=logging.
|
|
44
|
+
level=logging.DEBUG, # Changed to DEBUG for more detailed logging
|
|
40
45
|
format="%(asctime)s - %(levelname)s - %(message)s",
|
|
41
46
|
)
|
|
42
47
|
self.logger = logging.getLogger(__name__)
|
|
@@ -53,7 +58,55 @@ class ContainerManagerBase(ABC):
|
|
|
53
58
|
if result:
|
|
54
59
|
self.logger.info(f"Result: {result}")
|
|
55
60
|
if error:
|
|
56
|
-
self.logger.error(
|
|
61
|
+
self.logger.error(
|
|
62
|
+
f"Error in {action}: {type(error).__name__}: {str(error)}"
|
|
63
|
+
)
|
|
64
|
+
self.logger.error(f"Traceback: {traceback.format_exc()}")
|
|
65
|
+
|
|
66
|
+
def _format_size(self, size_bytes: int) -> str:
|
|
67
|
+
"""Helper to format bytes to human-readable (e.g., 1.23GB)."""
|
|
68
|
+
for unit in ["B", "KB", "MB", "GB", "TB"]:
|
|
69
|
+
if size_bytes < 1024.0:
|
|
70
|
+
return (
|
|
71
|
+
f"{size_bytes:.2f}{unit}" if unit != "B" else f"{size_bytes}{unit}"
|
|
72
|
+
)
|
|
73
|
+
size_bytes /= 1024.0
|
|
74
|
+
return f"{size_bytes:.2f}PB"
|
|
75
|
+
|
|
76
|
+
def _parse_timestamp(self, timestamp: Any) -> str:
|
|
77
|
+
"""Parse timestamp (integer, float, or string) to ISO 8601 string."""
|
|
78
|
+
if not timestamp:
|
|
79
|
+
return "unknown"
|
|
80
|
+
|
|
81
|
+
# Handle numeric timestamps (Unix timestamps in seconds)
|
|
82
|
+
if isinstance(timestamp, (int, float)):
|
|
83
|
+
try:
|
|
84
|
+
return datetime.fromtimestamp(timestamp).strftime("%Y-%m-%dT%H:%M:%S")
|
|
85
|
+
except (ValueError, OSError):
|
|
86
|
+
return "unknown"
|
|
87
|
+
|
|
88
|
+
# Handle string timestamps
|
|
89
|
+
if isinstance(timestamp, str):
|
|
90
|
+
# Common ISO 8601-like formats to try
|
|
91
|
+
formats = [
|
|
92
|
+
"%Y-%m-%dT%H:%M:%S", # 2023-10-05T14:30:00
|
|
93
|
+
"%Y-%m-%d %H:%M:%S", # 2023-10-05 14:30:00
|
|
94
|
+
"%Y-%m-%dT%H:%M:%S%z", # 2023-10-05T14:30:00+0000
|
|
95
|
+
"%Y-%m-%d %H:%M:%S%z", # 2023-10-05 14:30:00+0000
|
|
96
|
+
"%Y-%m-%dT%H:%M:%S.%f", # 2023-10-05T14:30:00.123456
|
|
97
|
+
"%Y-%m-%d %H:%M:%S.%f", # 2023-10-05 14:30:00.123456
|
|
98
|
+
"%Y-%m-%dT%H:%M:%S.%f%z", # 2023-10-05T14:30:00.123456+0000
|
|
99
|
+
"%Y-%m-%d", # 2023-10-05
|
|
100
|
+
]
|
|
101
|
+
for fmt in formats:
|
|
102
|
+
try:
|
|
103
|
+
parsed = datetime.strptime(timestamp, fmt)
|
|
104
|
+
return parsed.strftime("%Y-%m-%dT%H:%M:%S")
|
|
105
|
+
except ValueError:
|
|
106
|
+
continue
|
|
107
|
+
return "unknown"
|
|
108
|
+
|
|
109
|
+
return "unknown"
|
|
57
110
|
|
|
58
111
|
@abstractmethod
|
|
59
112
|
def get_version(self) -> Dict:
|
|
@@ -77,6 +130,10 @@ class ContainerManagerBase(ABC):
|
|
|
77
130
|
def remove_image(self, image: str, force: bool = False) -> Dict:
|
|
78
131
|
pass
|
|
79
132
|
|
|
133
|
+
@abstractmethod
|
|
134
|
+
def prune_images(self, force: bool = False, all: bool = False) -> Dict:
|
|
135
|
+
pass
|
|
136
|
+
|
|
80
137
|
@abstractmethod
|
|
81
138
|
def list_containers(self, all: bool = False) -> List[Dict]:
|
|
82
139
|
pass
|
|
@@ -102,6 +159,10 @@ class ContainerManagerBase(ABC):
|
|
|
102
159
|
def remove_container(self, container_id: str, force: bool = False) -> Dict:
|
|
103
160
|
pass
|
|
104
161
|
|
|
162
|
+
@abstractmethod
|
|
163
|
+
def prune_containers(self) -> Dict:
|
|
164
|
+
pass
|
|
165
|
+
|
|
105
166
|
@abstractmethod
|
|
106
167
|
def get_container_logs(self, container_id: str, tail: str = "all") -> str:
|
|
107
168
|
pass
|
|
@@ -124,6 +185,10 @@ class ContainerManagerBase(ABC):
|
|
|
124
185
|
def remove_volume(self, name: str, force: bool = False) -> Dict:
|
|
125
186
|
pass
|
|
126
187
|
|
|
188
|
+
@abstractmethod
|
|
189
|
+
def prune_volumes(self, force: bool = False, all: bool = False) -> Dict:
|
|
190
|
+
pass
|
|
191
|
+
|
|
127
192
|
@abstractmethod
|
|
128
193
|
def list_networks(self) -> List[Dict]:
|
|
129
194
|
pass
|
|
@@ -136,7 +201,14 @@ class ContainerManagerBase(ABC):
|
|
|
136
201
|
def remove_network(self, network_id: str) -> Dict:
|
|
137
202
|
pass
|
|
138
203
|
|
|
139
|
-
|
|
204
|
+
@abstractmethod
|
|
205
|
+
def prune_networks(self) -> Dict:
|
|
206
|
+
pass
|
|
207
|
+
|
|
208
|
+
@abstractmethod
|
|
209
|
+
def prune_system(self, force: bool = False, all: bool = False) -> Dict:
|
|
210
|
+
pass
|
|
211
|
+
|
|
140
212
|
@abstractmethod
|
|
141
213
|
def compose_up(
|
|
142
214
|
self, compose_file: str, detach: bool = True, build: bool = False
|
|
@@ -155,19 +227,23 @@ class ContainerManagerBase(ABC):
|
|
|
155
227
|
def compose_logs(self, compose_file: str, service: Optional[str] = None) -> str:
|
|
156
228
|
pass
|
|
157
229
|
|
|
158
|
-
|
|
230
|
+
@abstractmethod
|
|
159
231
|
def init_swarm(self, advertise_addr: Optional[str] = None) -> Dict:
|
|
160
|
-
|
|
232
|
+
pass
|
|
161
233
|
|
|
234
|
+
@abstractmethod
|
|
162
235
|
def leave_swarm(self, force: bool = False) -> Dict:
|
|
163
|
-
|
|
236
|
+
pass
|
|
164
237
|
|
|
238
|
+
@abstractmethod
|
|
165
239
|
def list_nodes(self) -> List[Dict]:
|
|
166
|
-
|
|
240
|
+
pass
|
|
167
241
|
|
|
242
|
+
@abstractmethod
|
|
168
243
|
def list_services(self) -> List[Dict]:
|
|
169
|
-
|
|
244
|
+
pass
|
|
170
245
|
|
|
246
|
+
@abstractmethod
|
|
171
247
|
def create_service(
|
|
172
248
|
self,
|
|
173
249
|
name: str,
|
|
@@ -176,10 +252,11 @@ class ContainerManagerBase(ABC):
|
|
|
176
252
|
ports: Optional[Dict[str, str]] = None,
|
|
177
253
|
mounts: Optional[List[str]] = None,
|
|
178
254
|
) -> Dict:
|
|
179
|
-
|
|
255
|
+
pass
|
|
180
256
|
|
|
257
|
+
@abstractmethod
|
|
181
258
|
def remove_service(self, service_id: str) -> Dict:
|
|
182
|
-
|
|
259
|
+
pass
|
|
183
260
|
|
|
184
261
|
|
|
185
262
|
class DockerManager(ContainerManagerBase):
|
|
@@ -193,10 +270,53 @@ class DockerManager(ContainerManagerBase):
|
|
|
193
270
|
self.logger.error(f"Failed to connect to Docker daemon: {str(e)}")
|
|
194
271
|
raise RuntimeError(f"Failed to connect to Docker: {str(e)}")
|
|
195
272
|
|
|
273
|
+
def prune_system(self, force: bool = False, all: bool = False) -> Dict:
|
|
274
|
+
params = {"force": force, "all": all}
|
|
275
|
+
try:
|
|
276
|
+
filters = {"until": None} if all else {}
|
|
277
|
+
result = self.client.system.prune(filters=filters, volumes=all)
|
|
278
|
+
if result is None:
|
|
279
|
+
result = {
|
|
280
|
+
"SpaceReclaimed": 0,
|
|
281
|
+
"ImagesDeleted": [],
|
|
282
|
+
"ContainersDeleted": [],
|
|
283
|
+
"VolumesDeleted": [],
|
|
284
|
+
"NetworksDeleted": [],
|
|
285
|
+
}
|
|
286
|
+
self.logger.debug(f"Raw prune_system result: {result}")
|
|
287
|
+
pruned = {
|
|
288
|
+
"space_reclaimed": self._format_size(result.get("SpaceReclaimed", 0)),
|
|
289
|
+
"images_removed": (
|
|
290
|
+
[img["Id"][7:19] for img in result.get("ImagesDeleted", [])]
|
|
291
|
+
),
|
|
292
|
+
"containers_removed": (
|
|
293
|
+
[c["Id"][7:19] for c in result.get("ContainersDeleted", [])]
|
|
294
|
+
),
|
|
295
|
+
"volumes_removed": (
|
|
296
|
+
[v["Name"] for v in result.get("VolumesDeleted", [])]
|
|
297
|
+
),
|
|
298
|
+
"networks_removed": (
|
|
299
|
+
[n["Id"][7:19] for n in result.get("NetworksDeleted", [])]
|
|
300
|
+
),
|
|
301
|
+
}
|
|
302
|
+
self.log_action("prune_system", params, pruned)
|
|
303
|
+
return pruned
|
|
304
|
+
except Exception as e:
|
|
305
|
+
self.log_action("prune_system", params, error=e)
|
|
306
|
+
raise RuntimeError(f"Failed to prune system: {str(e)}")
|
|
307
|
+
|
|
308
|
+
# Other DockerManager methods remain unchanged (omitted for brevity)
|
|
196
309
|
def get_version(self) -> Dict:
|
|
197
310
|
params = {}
|
|
198
311
|
try:
|
|
199
|
-
|
|
312
|
+
version = self.client.version()
|
|
313
|
+
result = {
|
|
314
|
+
"version": version.get("Version", "unknown"),
|
|
315
|
+
"api_version": version.get("ApiVersion", "unknown"),
|
|
316
|
+
"os": version.get("Os", "unknown"),
|
|
317
|
+
"arch": version.get("Arch", "unknown"),
|
|
318
|
+
"build_time": version.get("BuildTime", "unknown"),
|
|
319
|
+
}
|
|
200
320
|
self.log_action("get_version", params, result)
|
|
201
321
|
return result
|
|
202
322
|
except Exception as e:
|
|
@@ -206,7 +326,16 @@ class DockerManager(ContainerManagerBase):
|
|
|
206
326
|
def get_info(self) -> Dict:
|
|
207
327
|
params = {}
|
|
208
328
|
try:
|
|
209
|
-
|
|
329
|
+
info = self.client.info()
|
|
330
|
+
result = {
|
|
331
|
+
"containers_total": info.get("Containers", 0),
|
|
332
|
+
"containers_running": info.get("ContainersRunning", 0),
|
|
333
|
+
"images": info.get("Images", 0),
|
|
334
|
+
"driver": info.get("Driver", "unknown"),
|
|
335
|
+
"platform": f"{info.get('OperatingSystem', 'unknown')} {info.get('Architecture', 'unknown')}",
|
|
336
|
+
"memory_total": self._format_size(info.get("MemTotal", 0)),
|
|
337
|
+
"swap_total": self._format_size(info.get("SwapTotal", 0)),
|
|
338
|
+
}
|
|
210
339
|
self.log_action("get_info", params, result)
|
|
211
340
|
return result
|
|
212
341
|
except Exception as e:
|
|
@@ -217,7 +346,34 @@ class DockerManager(ContainerManagerBase):
|
|
|
217
346
|
params = {}
|
|
218
347
|
try:
|
|
219
348
|
images = self.client.images.list()
|
|
220
|
-
result = [
|
|
349
|
+
result = []
|
|
350
|
+
for img in images:
|
|
351
|
+
attrs = img.attrs
|
|
352
|
+
repo_tags = attrs.get("RepoTags", [])
|
|
353
|
+
repo_tag = repo_tags[0] if repo_tags else "<none>:<none>"
|
|
354
|
+
repository, tag = (
|
|
355
|
+
repo_tag.rsplit(":", 1) if ":" in repo_tag else ("<none>", "<none>")
|
|
356
|
+
)
|
|
357
|
+
|
|
358
|
+
created = attrs.get("Created", None)
|
|
359
|
+
created_str = self._parse_timestamp(created)
|
|
360
|
+
|
|
361
|
+
size_bytes = attrs.get("Size", 0)
|
|
362
|
+
size_str = self._format_size(size_bytes) if size_bytes else "0B"
|
|
363
|
+
|
|
364
|
+
simplified = {
|
|
365
|
+
"repository": repository,
|
|
366
|
+
"tag": tag,
|
|
367
|
+
"id": (
|
|
368
|
+
attrs.get("Id", "unknown")[7:19]
|
|
369
|
+
if attrs.get("Id")
|
|
370
|
+
else "unknown"
|
|
371
|
+
),
|
|
372
|
+
"created": created_str,
|
|
373
|
+
"size": size_str,
|
|
374
|
+
}
|
|
375
|
+
result.append(simplified)
|
|
376
|
+
|
|
221
377
|
self.log_action("list_images", params, result)
|
|
222
378
|
return result
|
|
223
379
|
except Exception as e:
|
|
@@ -230,7 +386,25 @@ class DockerManager(ContainerManagerBase):
|
|
|
230
386
|
params = {"image": image, "tag": tag, "platform": platform}
|
|
231
387
|
try:
|
|
232
388
|
img = self.client.images.pull(f"{image}:{tag}", platform=platform)
|
|
233
|
-
|
|
389
|
+
attrs = img.attrs
|
|
390
|
+
repo_tags = attrs.get("RepoTags", [])
|
|
391
|
+
repo_tag = repo_tags[0] if repo_tags else f"{image}:{tag}"
|
|
392
|
+
repository, tag = (
|
|
393
|
+
repo_tag.rsplit(":", 1) if ":" in repo_tag else (image, tag)
|
|
394
|
+
)
|
|
395
|
+
created = attrs.get("Created", None)
|
|
396
|
+
created_str = self._parse_timestamp(created)
|
|
397
|
+
size_bytes = attrs.get("Size", 0)
|
|
398
|
+
size_str = self._format_size(size_bytes) if size_bytes else "0B"
|
|
399
|
+
result = {
|
|
400
|
+
"repository": repository,
|
|
401
|
+
"tag": tag,
|
|
402
|
+
"id": (
|
|
403
|
+
attrs.get("Id", "unknown")[7:19] if attrs.get("Id") else "unknown"
|
|
404
|
+
),
|
|
405
|
+
"created": created_str,
|
|
406
|
+
"size": size_str,
|
|
407
|
+
}
|
|
234
408
|
self.log_action("pull_image", params, result)
|
|
235
409
|
return result
|
|
236
410
|
except Exception as e:
|
|
@@ -248,11 +422,74 @@ class DockerManager(ContainerManagerBase):
|
|
|
248
422
|
self.log_action("remove_image", params, error=e)
|
|
249
423
|
raise RuntimeError(f"Failed to remove image: {str(e)}")
|
|
250
424
|
|
|
425
|
+
def prune_images(self, force: bool = False, all: bool = False) -> Dict:
|
|
426
|
+
params = {"force": force, "all": all}
|
|
427
|
+
try:
|
|
428
|
+
if all:
|
|
429
|
+
# Manually remove all unused images
|
|
430
|
+
images = self.client.images.list(all=True)
|
|
431
|
+
removed = []
|
|
432
|
+
for img in images:
|
|
433
|
+
try:
|
|
434
|
+
for tag in img.attrs.get("RepoTags", []):
|
|
435
|
+
self.client.images.remove(tag, force=force)
|
|
436
|
+
removed.append(img.attrs["Id"][7:19])
|
|
437
|
+
except Exception as e:
|
|
438
|
+
self.logger.info(
|
|
439
|
+
f"Info: Failed to remove image {img.attrs.get('Id', 'unknown')}: {e}"
|
|
440
|
+
)
|
|
441
|
+
continue
|
|
442
|
+
result = {
|
|
443
|
+
"images_removed": removed,
|
|
444
|
+
"space_reclaimed": "N/A (all images)",
|
|
445
|
+
}
|
|
446
|
+
else:
|
|
447
|
+
filters = {"dangling": True} if not all else {}
|
|
448
|
+
result = self.client.images.prune(filters=filters)
|
|
449
|
+
if result is None:
|
|
450
|
+
result = {"SpaceReclaimed": 0, "ImagesDeleted": []}
|
|
451
|
+
self.logger.debug(f"Raw prune_images result: {result}")
|
|
452
|
+
pruned = {
|
|
453
|
+
"space_reclaimed": self._format_size(
|
|
454
|
+
result.get("SpaceReclaimed", 0)
|
|
455
|
+
),
|
|
456
|
+
"images_removed": (
|
|
457
|
+
[img["Id"][7:19] for img in result.get("ImagesDeleted", [])]
|
|
458
|
+
),
|
|
459
|
+
}
|
|
460
|
+
result = pruned
|
|
461
|
+
self.log_action("prune_images", params, result)
|
|
462
|
+
return result
|
|
463
|
+
except Exception as e:
|
|
464
|
+
self.log_action("prune_images", params, error=e)
|
|
465
|
+
raise RuntimeError(f"Failed to prune images: {str(e)}")
|
|
466
|
+
|
|
251
467
|
def list_containers(self, all: bool = False) -> List[Dict]:
|
|
252
468
|
params = {"all": all}
|
|
253
469
|
try:
|
|
254
470
|
containers = self.client.containers.list(all=all)
|
|
255
|
-
result = [
|
|
471
|
+
result = []
|
|
472
|
+
for c in containers:
|
|
473
|
+
attrs = c.attrs
|
|
474
|
+
ports = attrs.get("NetworkSettings", {}).get("Ports", {})
|
|
475
|
+
port_mappings = []
|
|
476
|
+
for container_port, host_ports in ports.items():
|
|
477
|
+
if host_ports:
|
|
478
|
+
for hp in host_ports:
|
|
479
|
+
port_mappings.append(
|
|
480
|
+
f"{hp.get('HostIp', '0.0.0.0')}:{hp.get('HostPort')}->{container_port}"
|
|
481
|
+
)
|
|
482
|
+
created = attrs.get("Created", None)
|
|
483
|
+
created_str = self._parse_timestamp(created)
|
|
484
|
+
simplified = {
|
|
485
|
+
"id": attrs.get("Id", "unknown")[7:19],
|
|
486
|
+
"image": attrs.get("Config", {}).get("Image", "unknown"),
|
|
487
|
+
"name": attrs.get("Name", "unknown").lstrip("/"),
|
|
488
|
+
"status": attrs.get("State", {}).get("Status", "unknown"),
|
|
489
|
+
"ports": ", ".join(port_mappings) if port_mappings else "none",
|
|
490
|
+
"created": created_str,
|
|
491
|
+
}
|
|
492
|
+
result.append(simplified)
|
|
256
493
|
self.log_action("list_containers", params, result)
|
|
257
494
|
return result
|
|
258
495
|
except Exception as e:
|
|
@@ -280,7 +517,7 @@ class DockerManager(ContainerManagerBase):
|
|
|
280
517
|
}
|
|
281
518
|
try:
|
|
282
519
|
container = self.client.containers.run(
|
|
283
|
-
image,
|
|
520
|
+
image=image,
|
|
284
521
|
command=command,
|
|
285
522
|
name=name,
|
|
286
523
|
detach=detach,
|
|
@@ -288,9 +525,32 @@ class DockerManager(ContainerManagerBase):
|
|
|
288
525
|
volumes=volumes,
|
|
289
526
|
environment=environment,
|
|
290
527
|
)
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
528
|
+
if not detach:
|
|
529
|
+
result = {"output": container.decode("utf-8") if container else ""}
|
|
530
|
+
self.log_action("run_container", params, result)
|
|
531
|
+
return result
|
|
532
|
+
attrs = container.attrs
|
|
533
|
+
port_mappings = []
|
|
534
|
+
if ports: # Check if ports is not None
|
|
535
|
+
network_settings = attrs.get("NetworkSettings", {})
|
|
536
|
+
container_ports = network_settings.get("Ports", {})
|
|
537
|
+
if container_ports: # Check if Ports dictionary is not empty
|
|
538
|
+
for container_port, host_ports in container_ports.items():
|
|
539
|
+
if host_ports: # Check if host_ports is not None or empty
|
|
540
|
+
for hp in host_ports:
|
|
541
|
+
port_mappings.append(
|
|
542
|
+
f"{hp.get('HostIp', '0.0.0.0')}:{hp.get('HostPort')}->{container_port}"
|
|
543
|
+
)
|
|
544
|
+
created = attrs.get("Created", None)
|
|
545
|
+
created_str = self._parse_timestamp(created)
|
|
546
|
+
result = {
|
|
547
|
+
"id": attrs.get("Id", "unknown")[7:19],
|
|
548
|
+
"image": attrs.get("Config", {}).get("Image", image),
|
|
549
|
+
"name": attrs.get("Name", name or "unknown").lstrip("/"),
|
|
550
|
+
"status": attrs.get("State", {}).get("Status", "unknown"),
|
|
551
|
+
"ports": ", ".join(port_mappings) if port_mappings else "none",
|
|
552
|
+
"created": created_str,
|
|
553
|
+
}
|
|
294
554
|
self.log_action("run_container", params, result)
|
|
295
555
|
return result
|
|
296
556
|
except Exception as e:
|
|
@@ -321,12 +581,42 @@ class DockerManager(ContainerManagerBase):
|
|
|
321
581
|
self.log_action("remove_container", params, error=e)
|
|
322
582
|
raise RuntimeError(f"Failed to remove container: {str(e)}")
|
|
323
583
|
|
|
584
|
+
def prune_containers(self) -> Dict:
|
|
585
|
+
params = {}
|
|
586
|
+
try:
|
|
587
|
+
result = self.client.containers.prune()
|
|
588
|
+
self.logger.debug(f"Raw prune_containers result: {result}")
|
|
589
|
+
if result is None:
|
|
590
|
+
result = {"SpaceReclaimed": 0, "ContainersDeleted": []}
|
|
591
|
+
pruned = {
|
|
592
|
+
"space_reclaimed": self._format_size(result.get("SpaceReclaimed", 0)),
|
|
593
|
+
"containers_removed": (
|
|
594
|
+
[c["Id"][7:19] for c in result.get("ContainersDeleted", [])]
|
|
595
|
+
),
|
|
596
|
+
}
|
|
597
|
+
self.log_action("prune_containers", params, pruned)
|
|
598
|
+
return pruned
|
|
599
|
+
except TypeError as e:
|
|
600
|
+
self.logger.error(f"TypeError in prune_containers: {str(e)}")
|
|
601
|
+
self.logger.error(f"Traceback: {traceback.format_exc()}")
|
|
602
|
+
self.log_action("prune_containers", params, error=e)
|
|
603
|
+
raise RuntimeError(f"Failed to prune containers: {str(e)}")
|
|
604
|
+
except Exception as e:
|
|
605
|
+
self.logger.error(
|
|
606
|
+
f"Unexpected exception in prune_containers: {type(e).__name__}: {str(e)}"
|
|
607
|
+
)
|
|
608
|
+
self.logger.error(f"Traceback: {traceback.format_exc()}")
|
|
609
|
+
self.log_action("prune_containers", params, error=e)
|
|
610
|
+
raise RuntimeError(f"Failed to prune containers: {str(e)}")
|
|
611
|
+
|
|
324
612
|
def get_container_logs(self, container_id: str, tail: str = "all") -> str:
|
|
325
613
|
params = {"container_id": container_id, "tail": tail}
|
|
326
614
|
try:
|
|
327
615
|
container = self.client.containers.get(container_id)
|
|
328
616
|
logs = container.logs(tail=tail).decode("utf-8")
|
|
329
|
-
self.log_action(
|
|
617
|
+
self.log_action(
|
|
618
|
+
"get_container_logs", params, logs[:1000]
|
|
619
|
+
) # Truncate for logging
|
|
330
620
|
return logs
|
|
331
621
|
except Exception as e:
|
|
332
622
|
self.log_action("get_container_logs", params, error=e)
|
|
@@ -341,7 +631,8 @@ class DockerManager(ContainerManagerBase):
|
|
|
341
631
|
exit_code, output = container.exec_run(command, detach=detach)
|
|
342
632
|
result = {
|
|
343
633
|
"exit_code": exit_code,
|
|
344
|
-
"output": output.decode("utf-8") if output else None,
|
|
634
|
+
"output": output.decode("utf-8") if output and not detach else None,
|
|
635
|
+
"command": command,
|
|
345
636
|
}
|
|
346
637
|
self.log_action("exec_in_container", params, result)
|
|
347
638
|
return result
|
|
@@ -353,7 +644,17 @@ class DockerManager(ContainerManagerBase):
|
|
|
353
644
|
params = {}
|
|
354
645
|
try:
|
|
355
646
|
volumes = self.client.volumes.list()
|
|
356
|
-
result = {
|
|
647
|
+
result = {
|
|
648
|
+
"volumes": [
|
|
649
|
+
{
|
|
650
|
+
"name": v.attrs.get("Name", "unknown"),
|
|
651
|
+
"driver": v.attrs.get("Driver", "unknown"),
|
|
652
|
+
"mountpoint": v.attrs.get("Mountpoint", "unknown"),
|
|
653
|
+
"created": v.attrs.get("CreatedAt", "unknown"),
|
|
654
|
+
}
|
|
655
|
+
for v in volumes
|
|
656
|
+
]
|
|
657
|
+
}
|
|
357
658
|
self.log_action("list_volumes", params, result)
|
|
358
659
|
return result
|
|
359
660
|
except Exception as e:
|
|
@@ -364,7 +665,13 @@ class DockerManager(ContainerManagerBase):
|
|
|
364
665
|
params = {"name": name}
|
|
365
666
|
try:
|
|
366
667
|
volume = self.client.volumes.create(name=name)
|
|
367
|
-
|
|
668
|
+
attrs = volume.attrs
|
|
669
|
+
result = {
|
|
670
|
+
"name": attrs.get("Name", name),
|
|
671
|
+
"driver": attrs.get("Driver", "unknown"),
|
|
672
|
+
"mountpoint": attrs.get("Mountpoint", "unknown"),
|
|
673
|
+
"created": attrs.get("CreatedAt", "unknown"),
|
|
674
|
+
}
|
|
368
675
|
self.log_action("create_volume", params, result)
|
|
369
676
|
return result
|
|
370
677
|
except Exception as e:
|
|
@@ -383,11 +690,64 @@ class DockerManager(ContainerManagerBase):
|
|
|
383
690
|
self.log_action("remove_volume", params, error=e)
|
|
384
691
|
raise RuntimeError(f"Failed to remove volume: {str(e)}")
|
|
385
692
|
|
|
693
|
+
def prune_volumes(self, force: bool = False, all: bool = False) -> Dict:
|
|
694
|
+
params = {"force": force, "all": all}
|
|
695
|
+
try:
|
|
696
|
+
if all:
|
|
697
|
+
volumes = self.client.volumes.list(all=True)
|
|
698
|
+
removed = []
|
|
699
|
+
for v in volumes:
|
|
700
|
+
try:
|
|
701
|
+
v.remove(force=force)
|
|
702
|
+
removed.append(v.attrs["Name"])
|
|
703
|
+
except Exception as e:
|
|
704
|
+
self.logger.info(
|
|
705
|
+
f"Info: Failed to remove volume {v.attrs.get('Name', 'unknown')}: {e}"
|
|
706
|
+
)
|
|
707
|
+
continue
|
|
708
|
+
result = {
|
|
709
|
+
"volumes_removed": removed,
|
|
710
|
+
"space_reclaimed": "N/A (all volumes)",
|
|
711
|
+
}
|
|
712
|
+
else:
|
|
713
|
+
result = self.client.volumes.prune()
|
|
714
|
+
if result is None:
|
|
715
|
+
result = {"SpaceReclaimed": 0, "VolumesDeleted": []}
|
|
716
|
+
self.logger.debug(f"Raw prune_volumes result: {result}")
|
|
717
|
+
pruned = {
|
|
718
|
+
"space_reclaimed": self._format_size(
|
|
719
|
+
result.get("SpaceReclaimed", 0)
|
|
720
|
+
),
|
|
721
|
+
"volumes_removed": (
|
|
722
|
+
[v["Name"] for v in result.get("VolumesDeleted", [])]
|
|
723
|
+
),
|
|
724
|
+
}
|
|
725
|
+
result = pruned
|
|
726
|
+
self.log_action("prune_volumes", params, result)
|
|
727
|
+
return result
|
|
728
|
+
except Exception as e:
|
|
729
|
+
self.log_action("prune_volumes", params, error=e)
|
|
730
|
+
raise RuntimeError(f"Failed to prune volumes: {str(e)}")
|
|
731
|
+
|
|
386
732
|
def list_networks(self) -> List[Dict]:
|
|
387
733
|
params = {}
|
|
388
734
|
try:
|
|
389
735
|
networks = self.client.networks.list()
|
|
390
|
-
result = [
|
|
736
|
+
result = []
|
|
737
|
+
for net in networks:
|
|
738
|
+
attrs = net.attrs
|
|
739
|
+
containers = len(attrs.get("Containers", {}))
|
|
740
|
+
created = attrs.get("Created", None)
|
|
741
|
+
created_str = self._parse_timestamp(created)
|
|
742
|
+
simplified = {
|
|
743
|
+
"id": attrs.get("Id", "unknown")[7:19],
|
|
744
|
+
"name": attrs.get("Name", "unknown"),
|
|
745
|
+
"driver": attrs.get("Driver", "unknown"),
|
|
746
|
+
"scope": attrs.get("Scope", "unknown"),
|
|
747
|
+
"containers": containers,
|
|
748
|
+
"created": created_str,
|
|
749
|
+
}
|
|
750
|
+
result.append(simplified)
|
|
391
751
|
self.log_action("list_networks", params, result)
|
|
392
752
|
return result
|
|
393
753
|
except Exception as e:
|
|
@@ -398,7 +758,16 @@ class DockerManager(ContainerManagerBase):
|
|
|
398
758
|
params = {"name": name, "driver": driver}
|
|
399
759
|
try:
|
|
400
760
|
network = self.client.networks.create(name, driver=driver)
|
|
401
|
-
|
|
761
|
+
attrs = network.attrs
|
|
762
|
+
created = attrs.get("Created", None)
|
|
763
|
+
created_str = self._parse_timestamp(created)
|
|
764
|
+
result = {
|
|
765
|
+
"id": attrs.get("Id", "unknown")[7:19],
|
|
766
|
+
"name": attrs.get("Name", name),
|
|
767
|
+
"driver": attrs.get("Driver", driver),
|
|
768
|
+
"scope": attrs.get("Scope", "unknown"),
|
|
769
|
+
"created": created_str,
|
|
770
|
+
}
|
|
402
771
|
self.log_action("create_network", params, result)
|
|
403
772
|
return result
|
|
404
773
|
except Exception as e:
|
|
@@ -417,6 +786,25 @@ class DockerManager(ContainerManagerBase):
|
|
|
417
786
|
self.log_action("remove_network", params, error=e)
|
|
418
787
|
raise RuntimeError(f"Failed to remove network: {str(e)}")
|
|
419
788
|
|
|
789
|
+
def prune_networks(self) -> Dict:
|
|
790
|
+
params = {}
|
|
791
|
+
try:
|
|
792
|
+
result = self.client.networks.prune()
|
|
793
|
+
if result is None:
|
|
794
|
+
result = {"SpaceReclaimed": 0, "NetworksDeleted": []}
|
|
795
|
+
self.logger.debug(f"Raw prune_networks result: {result}")
|
|
796
|
+
pruned = {
|
|
797
|
+
"space_reclaimed": self._format_size(result.get("SpaceReclaimed", 0)),
|
|
798
|
+
"networks_removed": (
|
|
799
|
+
[n["Id"][7:19] for n in result.get("NetworksDeleted", [])]
|
|
800
|
+
),
|
|
801
|
+
}
|
|
802
|
+
self.log_action("prune_networks", params, pruned)
|
|
803
|
+
return pruned
|
|
804
|
+
except Exception as e:
|
|
805
|
+
self.log_action("prune_networks", params, error=e)
|
|
806
|
+
raise RuntimeError(f"Failed to prune networks: {str(e)}")
|
|
807
|
+
|
|
420
808
|
def compose_up(
|
|
421
809
|
self, compose_file: str, detach: bool = True, build: bool = False
|
|
422
810
|
) -> str:
|
|
@@ -503,7 +891,23 @@ class DockerManager(ContainerManagerBase):
|
|
|
503
891
|
params = {}
|
|
504
892
|
try:
|
|
505
893
|
nodes = self.client.nodes.list()
|
|
506
|
-
result = [
|
|
894
|
+
result = []
|
|
895
|
+
for node in nodes:
|
|
896
|
+
attrs = node.attrs
|
|
897
|
+
spec = attrs.get("Spec", {})
|
|
898
|
+
status = attrs.get("Status", {})
|
|
899
|
+
created = attrs.get("CreatedAt", "unknown")
|
|
900
|
+
updated = attrs.get("UpdatedAt", "unknown")
|
|
901
|
+
simplified = {
|
|
902
|
+
"id": attrs.get("ID", "unknown")[7:19],
|
|
903
|
+
"hostname": spec.get("Name", "unknown"),
|
|
904
|
+
"role": spec.get("Role", "unknown"),
|
|
905
|
+
"status": status.get("State", "unknown"),
|
|
906
|
+
"availability": spec.get("Availability", "unknown"),
|
|
907
|
+
"created": created,
|
|
908
|
+
"updated": updated,
|
|
909
|
+
}
|
|
910
|
+
result.append(simplified)
|
|
507
911
|
self.log_action("list_nodes", params, result)
|
|
508
912
|
return result
|
|
509
913
|
except Exception as e:
|
|
@@ -514,7 +918,33 @@ class DockerManager(ContainerManagerBase):
|
|
|
514
918
|
params = {}
|
|
515
919
|
try:
|
|
516
920
|
services = self.client.services.list()
|
|
517
|
-
result = [
|
|
921
|
+
result = []
|
|
922
|
+
for service in services:
|
|
923
|
+
attrs = service.attrs
|
|
924
|
+
spec = attrs.get("Spec", {})
|
|
925
|
+
endpoint = attrs.get("Endpoint", {})
|
|
926
|
+
ports = endpoint.get("Ports", [])
|
|
927
|
+
port_mappings = [
|
|
928
|
+
f"{p.get('PublishedPort')}->{p.get('TargetPort')}/{p.get('Protocol')}"
|
|
929
|
+
for p in ports
|
|
930
|
+
if p.get("PublishedPort")
|
|
931
|
+
]
|
|
932
|
+
created = attrs.get("CreatedAt", "unknown")
|
|
933
|
+
updated = attrs.get("UpdatedAt", "unknown")
|
|
934
|
+
simplified = {
|
|
935
|
+
"id": attrs.get("ID", "unknown")[7:19],
|
|
936
|
+
"name": spec.get("Name", "unknown"),
|
|
937
|
+
"image": spec.get("TaskTemplate", {})
|
|
938
|
+
.get("ContainerSpec", {})
|
|
939
|
+
.get("Image", "unknown"),
|
|
940
|
+
"replicas": spec.get("Mode", {})
|
|
941
|
+
.get("Replicated", {})
|
|
942
|
+
.get("Replicas", 0),
|
|
943
|
+
"ports": ", ".join(port_mappings) if port_mappings else "none",
|
|
944
|
+
"created": created,
|
|
945
|
+
"updated": updated,
|
|
946
|
+
}
|
|
947
|
+
result.append(simplified)
|
|
518
948
|
self.log_action("list_services", params, result)
|
|
519
949
|
return result
|
|
520
950
|
except Exception as e:
|
|
@@ -538,15 +968,46 @@ class DockerManager(ContainerManagerBase):
|
|
|
538
968
|
}
|
|
539
969
|
try:
|
|
540
970
|
mode = {"mode": "replicated", "replicas": replicas}
|
|
541
|
-
|
|
971
|
+
endpoint_spec = None
|
|
972
|
+
if ports:
|
|
973
|
+
port_list = [
|
|
974
|
+
{
|
|
975
|
+
"Protocol": "tcp",
|
|
976
|
+
"PublishedPort": int(host_port),
|
|
977
|
+
"TargetPort": int(container_port.split("/")[0]),
|
|
978
|
+
}
|
|
979
|
+
for container_port, host_port in ports.items()
|
|
980
|
+
]
|
|
981
|
+
endpoint_spec = docker.types.EndpointSpec(ports=port_list)
|
|
542
982
|
service = self.client.services.create(
|
|
543
983
|
image,
|
|
544
984
|
name=name,
|
|
545
985
|
mode=mode,
|
|
546
986
|
mounts=mounts,
|
|
547
|
-
endpoint_spec=
|
|
987
|
+
endpoint_spec=endpoint_spec,
|
|
548
988
|
)
|
|
549
|
-
|
|
989
|
+
attrs = service.attrs
|
|
990
|
+
spec = attrs.get("Spec", {})
|
|
991
|
+
endpoint = attrs.get("Endpoint", {})
|
|
992
|
+
ports = endpoint.get("Ports", [])
|
|
993
|
+
port_mappings = [
|
|
994
|
+
f"{p.get('PublishedPort')}->{p.get('TargetPort')}/{p.get('Protocol')}"
|
|
995
|
+
for p in ports
|
|
996
|
+
if p.get("PublishedPort")
|
|
997
|
+
]
|
|
998
|
+
created = attrs.get("CreatedAt", "unknown")
|
|
999
|
+
result = {
|
|
1000
|
+
"id": attrs.get("ID", "unknown")[7:19],
|
|
1001
|
+
"name": spec.get("Name", name),
|
|
1002
|
+
"image": spec.get("TaskTemplate", {})
|
|
1003
|
+
.get("ContainerSpec", {})
|
|
1004
|
+
.get("Image", image),
|
|
1005
|
+
"replicas": spec.get("Mode", {})
|
|
1006
|
+
.get("Replicated", {})
|
|
1007
|
+
.get("Replicas", replicas),
|
|
1008
|
+
"ports": ", ".join(port_mappings) if port_mappings else "none",
|
|
1009
|
+
"created": created,
|
|
1010
|
+
}
|
|
550
1011
|
self.log_action("create_service", params, result)
|
|
551
1012
|
return result
|
|
552
1013
|
except Exception as e:
|
|
@@ -567,20 +1028,268 @@ class DockerManager(ContainerManagerBase):
|
|
|
567
1028
|
|
|
568
1029
|
|
|
569
1030
|
class PodmanManager(ContainerManagerBase):
|
|
570
|
-
def __init__(self, silent: bool = False, log_file: str = None):
|
|
1031
|
+
def __init__(self, silent: bool = False, log_file: Optional[str] = None):
|
|
571
1032
|
super().__init__(silent, log_file)
|
|
572
1033
|
if PodmanClient is None:
|
|
573
1034
|
raise ImportError("Please install podman-py: pip install podman")
|
|
1035
|
+
base_url = self._autodetect_podman_url()
|
|
1036
|
+
if base_url is None:
|
|
1037
|
+
self.logger.error(
|
|
1038
|
+
"No valid Podman socket found after trying all known locations"
|
|
1039
|
+
)
|
|
1040
|
+
raise RuntimeError("Failed to connect to Podman: No valid socket found")
|
|
1041
|
+
try:
|
|
1042
|
+
self.client = PodmanClient(base_url=base_url)
|
|
1043
|
+
self.logger.info(f"Connected to Podman with base_url: {base_url}")
|
|
1044
|
+
except PodmanError as e:
|
|
1045
|
+
self.logger.error(
|
|
1046
|
+
f"Failed to connect to Podman daemon with {base_url}: {str(e)}"
|
|
1047
|
+
)
|
|
1048
|
+
raise RuntimeError(f"Failed to connect to Podman with {base_url}: {str(e)}")
|
|
1049
|
+
|
|
1050
|
+
def _is_wsl(self) -> bool:
|
|
1051
|
+
"""Check if running inside WSL2."""
|
|
1052
|
+
try:
|
|
1053
|
+
with open("/proc/version", "r") as f:
|
|
1054
|
+
return "WSL" in f.read()
|
|
1055
|
+
except FileNotFoundError:
|
|
1056
|
+
return "WSL_DISTRO_NAME" in os.environ
|
|
1057
|
+
|
|
1058
|
+
def _is_podman_machine_running(self) -> bool:
|
|
1059
|
+
"""Check if Podman machine is running (for Windows/WSL2)."""
|
|
1060
|
+
try:
|
|
1061
|
+
result = subprocess.run(
|
|
1062
|
+
["podman", "machine", "list", "--format", "{{.Running}}"],
|
|
1063
|
+
capture_output=True,
|
|
1064
|
+
text=True,
|
|
1065
|
+
check=False,
|
|
1066
|
+
)
|
|
1067
|
+
return "true" in result.stdout.lower()
|
|
1068
|
+
except (subprocess.SubprocessError, FileNotFoundError):
|
|
1069
|
+
return False
|
|
1070
|
+
|
|
1071
|
+
def _try_connect(self, base_url: str) -> Optional[PodmanClient]:
|
|
1072
|
+
"""Attempt to connect to Podman with the given base_url."""
|
|
574
1073
|
try:
|
|
575
|
-
|
|
1074
|
+
client = PodmanClient(base_url=base_url)
|
|
1075
|
+
client.version()
|
|
1076
|
+
return client
|
|
576
1077
|
except PodmanError as e:
|
|
577
|
-
self.logger.
|
|
578
|
-
|
|
1078
|
+
self.logger.debug(f"Connection failed for {base_url}: {str(e)}")
|
|
1079
|
+
return None
|
|
1080
|
+
|
|
1081
|
+
def _autodetect_podman_url(self) -> Optional[str]:
|
|
1082
|
+
"""Autodetect the appropriate Podman socket URL based on platform."""
|
|
1083
|
+
base_url = os.environ.get("CONTAINER_MANAGER_PODMAN_BASE_URL")
|
|
1084
|
+
if base_url:
|
|
1085
|
+
self.logger.info(
|
|
1086
|
+
f"Using CONTAINER_MANAGER_PODMAN_BASE_URL from environment: {base_url}"
|
|
1087
|
+
)
|
|
1088
|
+
return base_url
|
|
1089
|
+
system = platform.system()
|
|
1090
|
+
is_wsl = self._is_wsl()
|
|
1091
|
+
socket_candidates = []
|
|
1092
|
+
if system == "Windows" and not is_wsl:
|
|
1093
|
+
if not self._is_podman_machine_running():
|
|
1094
|
+
raise RuntimeError("Podman Machine is not running on Windows system")
|
|
1095
|
+
socket_candidates.extend(
|
|
1096
|
+
[
|
|
1097
|
+
"tcp://127.0.0.1:8080",
|
|
1098
|
+
"unix:///run/podman/podman.sock",
|
|
1099
|
+
"npipe:////./pipe/docker_engine",
|
|
1100
|
+
"unix:///mnt/wsl/podman-sockets/podman-machine-default/podman-user.sock",
|
|
1101
|
+
"unix:///mnt/wsl/podman-sockets/podman-machine-default/podman-root.sock",
|
|
1102
|
+
]
|
|
1103
|
+
)
|
|
1104
|
+
elif system == "Linux" or is_wsl:
|
|
1105
|
+
uid = os.getuid()
|
|
1106
|
+
socket_candidates.extend(
|
|
1107
|
+
[
|
|
1108
|
+
f"unix:///run/user/{uid}/podman/podman.sock",
|
|
1109
|
+
"unix:///run/podman/podman.sock",
|
|
1110
|
+
"unix:///mnt/wsl/podman-sockets/podman-machine-default/podman-user.sock",
|
|
1111
|
+
"unix:///mnt/wsl/podman-sockets/podman-machine-default/podman-root.sock",
|
|
1112
|
+
]
|
|
1113
|
+
)
|
|
1114
|
+
for url in socket_candidates:
|
|
1115
|
+
client = self._try_connect(url)
|
|
1116
|
+
if client:
|
|
1117
|
+
return url
|
|
1118
|
+
return None
|
|
1119
|
+
|
|
1120
|
+
def prune_images(self, force: bool = False, all: bool = False) -> Dict:
|
|
1121
|
+
params = {"force": force, "all": all}
|
|
1122
|
+
try:
|
|
1123
|
+
if all:
|
|
1124
|
+
# Manually remove all unused images
|
|
1125
|
+
images = self.client.images.list(all=True)
|
|
1126
|
+
removed = []
|
|
1127
|
+
for img in images:
|
|
1128
|
+
try:
|
|
1129
|
+
for tag in img.attrs.get("Names", []):
|
|
1130
|
+
self.client.images.remove(tag, force=force)
|
|
1131
|
+
removed.append(img.attrs["Id"][7:19])
|
|
1132
|
+
except Exception as e:
|
|
1133
|
+
self.logger.info(
|
|
1134
|
+
f"Info: Failed to remove image {img.attrs.get('Id', 'unknown')}: {e}"
|
|
1135
|
+
)
|
|
1136
|
+
continue
|
|
1137
|
+
result = {
|
|
1138
|
+
"images_removed": removed,
|
|
1139
|
+
"space_reclaimed": "N/A (all images)",
|
|
1140
|
+
}
|
|
1141
|
+
else:
|
|
1142
|
+
filters = {"dangling": True} if not all else {}
|
|
1143
|
+
result = self.client.images.prune(filters=filters)
|
|
1144
|
+
if result is None:
|
|
1145
|
+
result = {"SpaceReclaimed": 0, "ImagesRemoved": []}
|
|
1146
|
+
self.logger.debug(f"Raw prune_images result: {result}")
|
|
1147
|
+
pruned = {
|
|
1148
|
+
"space_reclaimed": self._format_size(
|
|
1149
|
+
result.get("SpaceReclaimed", 0)
|
|
1150
|
+
),
|
|
1151
|
+
"images_removed": (
|
|
1152
|
+
[img["Id"][7:19] for img in result.get("ImagesRemoved", [])]
|
|
1153
|
+
or [img["Id"][7:19] for img in result.get("ImagesDeleted", [])]
|
|
1154
|
+
),
|
|
1155
|
+
}
|
|
1156
|
+
result = pruned
|
|
1157
|
+
self.log_action("prune_images", params, result)
|
|
1158
|
+
return result
|
|
1159
|
+
except Exception as e:
|
|
1160
|
+
self.log_action("prune_images", params, error=e)
|
|
1161
|
+
raise RuntimeError(f"Failed to prune images: {str(e)}")
|
|
1162
|
+
|
|
1163
|
+
def prune_containers(self) -> Dict:
|
|
1164
|
+
params = {}
|
|
1165
|
+
try:
|
|
1166
|
+
result = self.client.containers.prune()
|
|
1167
|
+
self.logger.debug(f"Raw prune_containers result: {result}")
|
|
1168
|
+
if result is None:
|
|
1169
|
+
result = {"SpaceReclaimed": 0, "ContainersDeleted": []}
|
|
1170
|
+
pruned = {
|
|
1171
|
+
"space_reclaimed": self._format_size(result.get("SpaceReclaimed", 0)),
|
|
1172
|
+
"containers_removed": (
|
|
1173
|
+
[c["Id"][7:19] for c in result.get("ContainersDeleted", [])]
|
|
1174
|
+
or [c["Id"][7:19] for c in result.get("ContainersRemoved", [])]
|
|
1175
|
+
),
|
|
1176
|
+
}
|
|
1177
|
+
self.log_action("prune_containers", params, pruned)
|
|
1178
|
+
return pruned
|
|
1179
|
+
except PodmanError as e:
|
|
1180
|
+
self.logger.error(f"PodmanError in prune_containers: {str(e)}")
|
|
1181
|
+
self.logger.error(f"Traceback: {traceback.format_exc()}")
|
|
1182
|
+
self.log_action("prune_containers", params, error=e)
|
|
1183
|
+
raise RuntimeError(f"Failed to prune containers: {str(e)}")
|
|
1184
|
+
except Exception as e:
|
|
1185
|
+
self.logger.error(
|
|
1186
|
+
f"Unexpected exception in prune_containers: {type(e).__name__}: {str(e)}"
|
|
1187
|
+
)
|
|
1188
|
+
self.logger.error(f"Traceback: {traceback.format_exc()}")
|
|
1189
|
+
self.log_action("prune_containers", params, error=e)
|
|
1190
|
+
raise RuntimeError(f"Failed to prune containers: {str(e)}")
|
|
1191
|
+
|
|
1192
|
+
def prune_volumes(self, force: bool = False, all: bool = False) -> Dict:
|
|
1193
|
+
params = {"force": force, "all": all}
|
|
1194
|
+
try:
|
|
1195
|
+
if all:
|
|
1196
|
+
volumes = self.client.volumes.list(all=True)
|
|
1197
|
+
removed = []
|
|
1198
|
+
for v in volumes:
|
|
1199
|
+
try:
|
|
1200
|
+
v.remove(force=force)
|
|
1201
|
+
removed.append(v.attrs["Name"])
|
|
1202
|
+
except Exception as e:
|
|
1203
|
+
self.logger.info(
|
|
1204
|
+
f"Info: Failed to remove volume {v.attrs.get('Name', 'unknown')}: {e}"
|
|
1205
|
+
)
|
|
1206
|
+
continue
|
|
1207
|
+
result = {
|
|
1208
|
+
"volumes_removed": removed,
|
|
1209
|
+
"space_reclaimed": "N/A (all volumes)",
|
|
1210
|
+
}
|
|
1211
|
+
else:
|
|
1212
|
+
result = self.client.volumes.prune()
|
|
1213
|
+
if result is None:
|
|
1214
|
+
result = {"SpaceReclaimed": 0, "VolumesRemoved": []}
|
|
1215
|
+
self.logger.debug(f"Raw prune_volumes result: {result}")
|
|
1216
|
+
pruned = {
|
|
1217
|
+
"space_reclaimed": self._format_size(
|
|
1218
|
+
result.get("SpaceReclaimed", 0)
|
|
1219
|
+
),
|
|
1220
|
+
"volumes_removed": (
|
|
1221
|
+
[v["Name"] for v in result.get("VolumesRemoved", [])]
|
|
1222
|
+
or [v["Name"] for v in result.get("VolumesDeleted", [])]
|
|
1223
|
+
),
|
|
1224
|
+
}
|
|
1225
|
+
result = pruned
|
|
1226
|
+
self.log_action("prune_volumes", params, result)
|
|
1227
|
+
return result
|
|
1228
|
+
except Exception as e:
|
|
1229
|
+
self.log_action("prune_volumes", params, error=e)
|
|
1230
|
+
raise RuntimeError(f"Failed to prune volumes: {str(e)}")
|
|
1231
|
+
|
|
1232
|
+
def prune_networks(self) -> Dict:
|
|
1233
|
+
params = {}
|
|
1234
|
+
try:
|
|
1235
|
+
result = self.client.networks.prune()
|
|
1236
|
+
if result is None:
|
|
1237
|
+
result = {"SpaceReclaimed": 0, "NetworksRemoved": []}
|
|
1238
|
+
self.logger.debug(f"Raw prune_networks result: {result}")
|
|
1239
|
+
pruned = {
|
|
1240
|
+
"space_reclaimed": self._format_size(result.get("SpaceReclaimed", 0)),
|
|
1241
|
+
"networks_removed": (
|
|
1242
|
+
[n["Id"][7:19] for n in result.get("NetworksRemoved", [])]
|
|
1243
|
+
or [n["Id"][7:19] for n in result.get("NetworksDeleted", [])]
|
|
1244
|
+
),
|
|
1245
|
+
}
|
|
1246
|
+
self.log_action("prune_networks", params, pruned)
|
|
1247
|
+
return pruned
|
|
1248
|
+
except Exception as e:
|
|
1249
|
+
self.log_action("prune_networks", params, error=e)
|
|
1250
|
+
raise RuntimeError(f"Failed to prune networks: {str(e)}")
|
|
1251
|
+
|
|
1252
|
+
def prune_system(self, force: bool = False, all: bool = False) -> Dict:
|
|
1253
|
+
params = {"force": force, "all": all}
|
|
1254
|
+
try:
|
|
1255
|
+
cmd = (
|
|
1256
|
+
["podman", "system", "prune", "--force"]
|
|
1257
|
+
if force
|
|
1258
|
+
else ["podman", "system", "prune"]
|
|
1259
|
+
)
|
|
1260
|
+
if all:
|
|
1261
|
+
cmd.append("--all")
|
|
1262
|
+
if all: # Include volumes if all=True
|
|
1263
|
+
cmd.append("--volumes")
|
|
1264
|
+
result = subprocess.run(cmd, capture_output=True, text=True)
|
|
1265
|
+
if result.returncode != 0:
|
|
1266
|
+
raise RuntimeError(result.stderr)
|
|
1267
|
+
self.logger.debug(f"Raw prune_system result: {result.stdout}")
|
|
1268
|
+
pruned = {
|
|
1269
|
+
"output": result.stdout.strip(),
|
|
1270
|
+
"space_reclaimed": "Check output",
|
|
1271
|
+
"images_removed": [], # Podman CLI doesn't provide detailed breakdown
|
|
1272
|
+
"containers_removed": [],
|
|
1273
|
+
"volumes_removed": [],
|
|
1274
|
+
"networks_removed": [],
|
|
1275
|
+
}
|
|
1276
|
+
self.log_action("prune_system", params, pruned)
|
|
1277
|
+
return pruned
|
|
1278
|
+
except Exception as e:
|
|
1279
|
+
self.log_action("prune_system", params, error=e)
|
|
1280
|
+
raise RuntimeError(f"Failed to prune system: {str(e)}")
|
|
579
1281
|
|
|
580
1282
|
def get_version(self) -> Dict:
|
|
581
1283
|
params = {}
|
|
582
1284
|
try:
|
|
583
|
-
|
|
1285
|
+
version = self.client.version()
|
|
1286
|
+
result = {
|
|
1287
|
+
"version": version.get("Version", "unknown"),
|
|
1288
|
+
"api_version": version.get("APIVersion", "unknown"),
|
|
1289
|
+
"os": version.get("Os", "unknown"),
|
|
1290
|
+
"arch": version.get("Arch", "unknown"),
|
|
1291
|
+
"build_time": version.get("BuildTime", "unknown"),
|
|
1292
|
+
}
|
|
584
1293
|
self.log_action("get_version", params, result)
|
|
585
1294
|
return result
|
|
586
1295
|
except Exception as e:
|
|
@@ -590,7 +1299,17 @@ class PodmanManager(ContainerManagerBase):
|
|
|
590
1299
|
def get_info(self) -> Dict:
|
|
591
1300
|
params = {}
|
|
592
1301
|
try:
|
|
593
|
-
|
|
1302
|
+
info = self.client.info()
|
|
1303
|
+
host = info.get("host", {})
|
|
1304
|
+
result = {
|
|
1305
|
+
"containers_total": info.get("store", {}).get("containers", 0),
|
|
1306
|
+
"containers_running": host.get("runningContainers", 0),
|
|
1307
|
+
"images": info.get("store", {}).get("images", 0),
|
|
1308
|
+
"driver": host.get("graphDriverName", "unknown"),
|
|
1309
|
+
"platform": f"{host.get('os', 'unknown')} {host.get('arch', 'unknown')}",
|
|
1310
|
+
"memory_total": self._format_size(host.get("memTotal", 0)),
|
|
1311
|
+
"swap_total": self._format_size(host.get("swapTotal", 0)),
|
|
1312
|
+
}
|
|
594
1313
|
self.log_action("get_info", params, result)
|
|
595
1314
|
return result
|
|
596
1315
|
except Exception as e:
|
|
@@ -601,7 +1320,30 @@ class PodmanManager(ContainerManagerBase):
|
|
|
601
1320
|
params = {}
|
|
602
1321
|
try:
|
|
603
1322
|
images = self.client.images.list()
|
|
604
|
-
result = [
|
|
1323
|
+
result = []
|
|
1324
|
+
for img in images:
|
|
1325
|
+
attrs = img.attrs
|
|
1326
|
+
repo_tags = attrs.get("Names", [])
|
|
1327
|
+
repo_tag = repo_tags[0] if repo_tags else "<none>:<none>"
|
|
1328
|
+
repository, tag = (
|
|
1329
|
+
repo_tag.rsplit(":", 1) if ":" in repo_tag else ("<none>", "<none>")
|
|
1330
|
+
)
|
|
1331
|
+
created = attrs.get("Created", None)
|
|
1332
|
+
created_str = self._parse_timestamp(created)
|
|
1333
|
+
size_bytes = attrs.get("Size", 0)
|
|
1334
|
+
size_str = self._format_size(size_bytes) if size_bytes else "0B"
|
|
1335
|
+
simplified = {
|
|
1336
|
+
"repository": repository,
|
|
1337
|
+
"tag": tag,
|
|
1338
|
+
"id": (
|
|
1339
|
+
attrs.get("Id", "unknown")[7:19]
|
|
1340
|
+
if attrs.get("Id")
|
|
1341
|
+
else "unknown"
|
|
1342
|
+
),
|
|
1343
|
+
"created": created_str,
|
|
1344
|
+
"size": size_str,
|
|
1345
|
+
}
|
|
1346
|
+
result.append(simplified)
|
|
605
1347
|
self.log_action("list_images", params, result)
|
|
606
1348
|
return result
|
|
607
1349
|
except Exception as e:
|
|
@@ -614,7 +1356,25 @@ class PodmanManager(ContainerManagerBase):
|
|
|
614
1356
|
params = {"image": image, "tag": tag, "platform": platform}
|
|
615
1357
|
try:
|
|
616
1358
|
img = self.client.images.pull(f"{image}:{tag}", platform=platform)
|
|
617
|
-
|
|
1359
|
+
attrs = img[0].attrs if isinstance(img, list) else img.attrs
|
|
1360
|
+
repo_tags = attrs.get("Names", [])
|
|
1361
|
+
repo_tag = repo_tags[0] if repo_tags else f"{image}:{tag}"
|
|
1362
|
+
repository, tag = (
|
|
1363
|
+
repo_tag.rsplit(":", 1) if ":" in repo_tag else (image, tag)
|
|
1364
|
+
)
|
|
1365
|
+
created = attrs.get("Created", None)
|
|
1366
|
+
created_str = self._parse_timestamp(created)
|
|
1367
|
+
size_bytes = attrs.get("Size", 0)
|
|
1368
|
+
size_str = self._format_size(size_bytes) if size_bytes else "0B"
|
|
1369
|
+
result = {
|
|
1370
|
+
"repository": repository,
|
|
1371
|
+
"tag": tag,
|
|
1372
|
+
"id": (
|
|
1373
|
+
attrs.get("Id", "unknown")[7:19] if attrs.get("Id") else "unknown"
|
|
1374
|
+
),
|
|
1375
|
+
"created": created_str,
|
|
1376
|
+
"size": size_str,
|
|
1377
|
+
}
|
|
618
1378
|
self.log_action("pull_image", params, result)
|
|
619
1379
|
return result
|
|
620
1380
|
except Exception as e:
|
|
@@ -636,7 +1396,26 @@ class PodmanManager(ContainerManagerBase):
|
|
|
636
1396
|
params = {"all": all}
|
|
637
1397
|
try:
|
|
638
1398
|
containers = self.client.containers.list(all=all)
|
|
639
|
-
result = [
|
|
1399
|
+
result = []
|
|
1400
|
+
for c in containers:
|
|
1401
|
+
attrs = c.attrs
|
|
1402
|
+
ports = attrs.get("Ports", [])
|
|
1403
|
+
port_mappings = [
|
|
1404
|
+
f"{p.get('host_ip', '0.0.0.0')}:{p.get('host_port')}->{p.get('container_port')}/{p.get('protocol', 'tcp')}"
|
|
1405
|
+
for p in ports
|
|
1406
|
+
if p.get("host_port")
|
|
1407
|
+
]
|
|
1408
|
+
created = attrs.get("Created", None)
|
|
1409
|
+
created_str = self._parse_timestamp(created)
|
|
1410
|
+
simplified = {
|
|
1411
|
+
"id": attrs.get("Id", "unknown")[7:19],
|
|
1412
|
+
"image": attrs.get("Image", "unknown"),
|
|
1413
|
+
"name": attrs.get("Names", ["unknown"])[0].lstrip("/"),
|
|
1414
|
+
"status": attrs.get("State", "unknown"),
|
|
1415
|
+
"ports": ", ".join(port_mappings) if port_mappings else "none",
|
|
1416
|
+
"created": created_str,
|
|
1417
|
+
}
|
|
1418
|
+
result.append(simplified)
|
|
640
1419
|
self.log_action("list_containers", params, result)
|
|
641
1420
|
return result
|
|
642
1421
|
except Exception as e:
|
|
@@ -672,9 +1451,30 @@ class PodmanManager(ContainerManagerBase):
|
|
|
672
1451
|
volumes=volumes,
|
|
673
1452
|
environment=environment,
|
|
674
1453
|
)
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
1454
|
+
if not detach:
|
|
1455
|
+
result = {"output": container.decode("utf-8") if container else ""}
|
|
1456
|
+
self.log_action("run_container", params, result)
|
|
1457
|
+
return result
|
|
1458
|
+
attrs = container.attrs
|
|
1459
|
+
port_mappings = []
|
|
1460
|
+
if ports: # Check if ports is not None
|
|
1461
|
+
container_ports = attrs.get("Ports", [])
|
|
1462
|
+
if container_ports: # Check if Ports list is not empty
|
|
1463
|
+
port_mappings = [
|
|
1464
|
+
f"{p.get('host_ip', '0.0.0.0')}:{p.get('host_port')}->{p.get('container_port')}/{p.get('protocol', 'tcp')}"
|
|
1465
|
+
for p in container_ports
|
|
1466
|
+
if p.get("host_port")
|
|
1467
|
+
]
|
|
1468
|
+
created = attrs.get("Created", None)
|
|
1469
|
+
created_str = self._parse_timestamp(created)
|
|
1470
|
+
result = {
|
|
1471
|
+
"id": attrs.get("Id", "unknown")[7:19],
|
|
1472
|
+
"image": attrs.get("Image", image),
|
|
1473
|
+
"name": attrs.get("Names", [name or "unknown"])[0].lstrip("/"),
|
|
1474
|
+
"status": attrs.get("State", "unknown"),
|
|
1475
|
+
"ports": ", ".join(port_mappings) if port_mappings else "none",
|
|
1476
|
+
"created": created_str,
|
|
1477
|
+
}
|
|
678
1478
|
self.log_action("run_container", params, result)
|
|
679
1479
|
return result
|
|
680
1480
|
except Exception as e:
|
|
@@ -710,7 +1510,9 @@ class PodmanManager(ContainerManagerBase):
|
|
|
710
1510
|
try:
|
|
711
1511
|
container = self.client.containers.get(container_id)
|
|
712
1512
|
logs = container.logs(tail=tail).decode("utf-8")
|
|
713
|
-
self.log_action(
|
|
1513
|
+
self.log_action(
|
|
1514
|
+
"get_container_logs", params, logs[:1000]
|
|
1515
|
+
) # Truncate for logging
|
|
714
1516
|
return logs
|
|
715
1517
|
except Exception as e:
|
|
716
1518
|
self.log_action("get_container_logs", params, error=e)
|
|
@@ -725,7 +1527,8 @@ class PodmanManager(ContainerManagerBase):
|
|
|
725
1527
|
exit_code, output = container.exec_run(command, detach=detach)
|
|
726
1528
|
result = {
|
|
727
1529
|
"exit_code": exit_code,
|
|
728
|
-
"output": output.decode("utf-8") if output else None,
|
|
1530
|
+
"output": output.decode("utf-8") if output and not detach else None,
|
|
1531
|
+
"command": command,
|
|
729
1532
|
}
|
|
730
1533
|
self.log_action("exec_in_container", params, result)
|
|
731
1534
|
return result
|
|
@@ -737,7 +1540,17 @@ class PodmanManager(ContainerManagerBase):
|
|
|
737
1540
|
params = {}
|
|
738
1541
|
try:
|
|
739
1542
|
volumes = self.client.volumes.list()
|
|
740
|
-
result = {
|
|
1543
|
+
result = {
|
|
1544
|
+
"volumes": [
|
|
1545
|
+
{
|
|
1546
|
+
"name": v.attrs.get("Name", "unknown"),
|
|
1547
|
+
"driver": v.attrs.get("Driver", "unknown"),
|
|
1548
|
+
"mountpoint": v.attrs.get("Mountpoint", "unknown"),
|
|
1549
|
+
"created": v.attrs.get("CreatedAt", "unknown"),
|
|
1550
|
+
}
|
|
1551
|
+
for v in volumes
|
|
1552
|
+
]
|
|
1553
|
+
}
|
|
741
1554
|
self.log_action("list_volumes", params, result)
|
|
742
1555
|
return result
|
|
743
1556
|
except Exception as e:
|
|
@@ -748,7 +1561,13 @@ class PodmanManager(ContainerManagerBase):
|
|
|
748
1561
|
params = {"name": name}
|
|
749
1562
|
try:
|
|
750
1563
|
volume = self.client.volumes.create(name=name)
|
|
751
|
-
|
|
1564
|
+
attrs = volume.attrs
|
|
1565
|
+
result = {
|
|
1566
|
+
"name": attrs.get("Name", name),
|
|
1567
|
+
"driver": attrs.get("Driver", "unknown"),
|
|
1568
|
+
"mountpoint": attrs.get("Mountpoint", "unknown"),
|
|
1569
|
+
"created": attrs.get("CreatedAt", "unknown"),
|
|
1570
|
+
}
|
|
752
1571
|
self.log_action("create_volume", params, result)
|
|
753
1572
|
return result
|
|
754
1573
|
except Exception as e:
|
|
@@ -771,7 +1590,21 @@ class PodmanManager(ContainerManagerBase):
|
|
|
771
1590
|
params = {}
|
|
772
1591
|
try:
|
|
773
1592
|
networks = self.client.networks.list()
|
|
774
|
-
result = [
|
|
1593
|
+
result = []
|
|
1594
|
+
for net in networks:
|
|
1595
|
+
attrs = net.attrs
|
|
1596
|
+
containers = len(attrs.get("Containers", {}))
|
|
1597
|
+
created = attrs.get("Created", None)
|
|
1598
|
+
created_str = self._parse_timestamp(created)
|
|
1599
|
+
simplified = {
|
|
1600
|
+
"id": attrs.get("Id", "unknown")[7:19],
|
|
1601
|
+
"name": attrs.get("Name", "unknown"),
|
|
1602
|
+
"driver": attrs.get("Driver", "unknown"),
|
|
1603
|
+
"scope": attrs.get("Scope", "unknown"),
|
|
1604
|
+
"containers": containers,
|
|
1605
|
+
"created": created_str,
|
|
1606
|
+
}
|
|
1607
|
+
result.append(simplified)
|
|
775
1608
|
self.log_action("list_networks", params, result)
|
|
776
1609
|
return result
|
|
777
1610
|
except Exception as e:
|
|
@@ -782,7 +1615,16 @@ class PodmanManager(ContainerManagerBase):
|
|
|
782
1615
|
params = {"name": name, "driver": driver}
|
|
783
1616
|
try:
|
|
784
1617
|
network = self.client.networks.create(name, driver=driver)
|
|
785
|
-
|
|
1618
|
+
attrs = network.attrs
|
|
1619
|
+
created = attrs.get("Created", None)
|
|
1620
|
+
created_str = self._parse_timestamp(created)
|
|
1621
|
+
result = {
|
|
1622
|
+
"id": attrs.get("Id", "unknown")[7:19],
|
|
1623
|
+
"name": attrs.get("Name", name),
|
|
1624
|
+
"driver": attrs.get("Driver", driver),
|
|
1625
|
+
"scope": attrs.get("Scope", "unknown"),
|
|
1626
|
+
"created": created_str,
|
|
1627
|
+
}
|
|
786
1628
|
self.log_action("create_network", params, result)
|
|
787
1629
|
return result
|
|
788
1630
|
except Exception as e:
|
|
@@ -861,11 +1703,51 @@ class PodmanManager(ContainerManagerBase):
|
|
|
861
1703
|
self.log_action("compose_logs", params, error=e)
|
|
862
1704
|
raise RuntimeError(f"Failed to compose logs: {str(e)}")
|
|
863
1705
|
|
|
1706
|
+
def init_swarm(self, advertise_addr: Optional[str] = None) -> Dict:
|
|
1707
|
+
raise NotImplementedError("Swarm not supported in Podman")
|
|
1708
|
+
|
|
1709
|
+
def leave_swarm(self, force: bool = False) -> Dict:
|
|
1710
|
+
raise NotImplementedError("Swarm not supported in Podman")
|
|
1711
|
+
|
|
1712
|
+
def list_nodes(self) -> List[Dict]:
|
|
1713
|
+
raise NotImplementedError("Swarm not supported in Podman")
|
|
1714
|
+
|
|
1715
|
+
def list_services(self) -> List[Dict]:
|
|
1716
|
+
raise NotImplementedError("Swarm not supported in Podman")
|
|
1717
|
+
|
|
1718
|
+
def create_service(
|
|
1719
|
+
self,
|
|
1720
|
+
name: str,
|
|
1721
|
+
image: str,
|
|
1722
|
+
replicas: int = 1,
|
|
1723
|
+
ports: Optional[Dict[str, str]] = None,
|
|
1724
|
+
mounts: Optional[List[str]] = None,
|
|
1725
|
+
) -> Dict:
|
|
1726
|
+
raise NotImplementedError("Swarm not supported in Podman")
|
|
1727
|
+
|
|
1728
|
+
def remove_service(self, service_id: str) -> Dict:
|
|
1729
|
+
raise NotImplementedError("Swarm not supported in Podman")
|
|
1730
|
+
|
|
1731
|
+
|
|
1732
|
+
def is_app_installed(app_name: str = "docker") -> bool:
|
|
1733
|
+
return shutil.which(app_name.lower()) is not None
|
|
1734
|
+
|
|
864
1735
|
|
|
865
1736
|
def create_manager(
|
|
866
|
-
manager_type: str, silent: bool = False, log_file: str = None
|
|
1737
|
+
manager_type: Optional[str] = None, silent: bool = False, log_file: str = None
|
|
867
1738
|
) -> ContainerManagerBase:
|
|
868
|
-
if manager_type
|
|
1739
|
+
if manager_type is None:
|
|
1740
|
+
manager_type = os.environ.get("CONTAINER_MANAGER_TYPE", None)
|
|
1741
|
+
if manager_type is None:
|
|
1742
|
+
if is_app_installed("podman"):
|
|
1743
|
+
manager_type = "podman"
|
|
1744
|
+
if is_app_installed("docker"):
|
|
1745
|
+
manager_type = "docker"
|
|
1746
|
+
if manager_type is None:
|
|
1747
|
+
raise ValueError(
|
|
1748
|
+
"No supported container manager detected. Set CONTAINER_MANAGER_TYPE or install Docker/Podman."
|
|
1749
|
+
)
|
|
1750
|
+
if manager_type.lower() in ["docker", "swarm"]:
|
|
869
1751
|
return DockerManager(silent=silent, log_file=log_file)
|
|
870
1752
|
elif manager_type.lower() == "podman":
|
|
871
1753
|
return PodmanManager(silent=silent, log_file=log_file)
|
|
@@ -881,7 +1763,7 @@ Container Manager: A tool to manage containers with Docker, Podman, and Docker S
|
|
|
881
1763
|
Usage:
|
|
882
1764
|
-h | --help [ See usage for script ]
|
|
883
1765
|
-s | --silent [ Suppress output ]
|
|
884
|
-
-m | --manager <type> [ docker, podman, swarm; default:
|
|
1766
|
+
-m | --manager <type> [ docker, podman, swarm; default: auto-detect ]
|
|
885
1767
|
--log-file <path> [ Log to specified file (default: container_manager.log in script dir) ]
|
|
886
1768
|
|
|
887
1769
|
Actions:
|
|
@@ -893,6 +1775,8 @@ Actions:
|
|
|
893
1775
|
--platform <plat> [ Platform, e.g., linux/amd64 ]
|
|
894
1776
|
--remove-image <image> [ Remove image ]
|
|
895
1777
|
--force [ Force removal (global for remove actions) ]
|
|
1778
|
+
--prune-images [ Prune unused images ]
|
|
1779
|
+
--all [ Prune all unused images ]
|
|
896
1780
|
--list-containers [ List containers ]
|
|
897
1781
|
--all [ Show all containers ]
|
|
898
1782
|
--run-container <image> [ Run container ]
|
|
@@ -906,6 +1790,7 @@ Actions:
|
|
|
906
1790
|
--timeout <sec> [ Timeout, default 10 ]
|
|
907
1791
|
--remove-container <id>[ Remove container ]
|
|
908
1792
|
--force [ Force ]
|
|
1793
|
+
--prune-containers [ Prune stopped containers ]
|
|
909
1794
|
--get-container-logs <id> [ Get logs ]
|
|
910
1795
|
--tail <tail> [ Tail lines, default all ]
|
|
911
1796
|
--exec-in-container <id> [ Exec command ]
|
|
@@ -915,10 +1800,15 @@ Actions:
|
|
|
915
1800
|
--create-volume <name> [ Create volume ]
|
|
916
1801
|
--remove-volume <name> [ Remove volume ]
|
|
917
1802
|
--force [ Force ]
|
|
1803
|
+
--prune-volumes [ Prune unused volumes ]
|
|
1804
|
+
--all [ Remove all volumes (dangerous) ]
|
|
918
1805
|
--list-networks [ List networks ]
|
|
919
1806
|
--create-network <name>[ Create network ]
|
|
920
1807
|
--driver <driver> [ Driver, default bridge ]
|
|
921
1808
|
--remove-network <id> [ Remove network ]
|
|
1809
|
+
--prune-networks [ Prune unused networks ]
|
|
1810
|
+
--prune-system [ Prune system resources ]
|
|
1811
|
+
--all [ Prune all unused (including volumes, build cache) ]
|
|
922
1812
|
--compose-up <file> [ Compose up ]
|
|
923
1813
|
--build [ Build images ]
|
|
924
1814
|
--detach [ Detach mode, default true ]
|
|
@@ -945,263 +1835,194 @@ container_manager.py --manager docker --pull-image nginx --tag latest --list-con
|
|
|
945
1835
|
)
|
|
946
1836
|
|
|
947
1837
|
|
|
948
|
-
def
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
"create-volume=",
|
|
1055
|
-
"remove-volume=",
|
|
1056
|
-
"list-networks",
|
|
1057
|
-
"create-network=",
|
|
1058
|
-
"driver=",
|
|
1059
|
-
"remove-network=",
|
|
1060
|
-
"compose-up=",
|
|
1061
|
-
"build",
|
|
1062
|
-
"compose-down=",
|
|
1063
|
-
"compose-ps=",
|
|
1064
|
-
"compose-logs=",
|
|
1065
|
-
"service=",
|
|
1066
|
-
"init-swarm",
|
|
1067
|
-
"advertise-addr=",
|
|
1068
|
-
"leave-swarm",
|
|
1069
|
-
"list-nodes",
|
|
1070
|
-
"list-services",
|
|
1071
|
-
"create-service=",
|
|
1072
|
-
"image=",
|
|
1073
|
-
"replicas=",
|
|
1074
|
-
"mounts=",
|
|
1075
|
-
"remove-service=",
|
|
1076
|
-
],
|
|
1077
|
-
)
|
|
1078
|
-
except getopt.GetoptError:
|
|
1079
|
-
usage()
|
|
1080
|
-
sys.exit(2)
|
|
1838
|
+
def container_manager():
|
|
1839
|
+
parser = argparse.ArgumentParser(
|
|
1840
|
+
description="Container Manager: A tool to manage containers with Docker, Podman, and Docker Swarm!"
|
|
1841
|
+
)
|
|
1842
|
+
parser.add_argument("-s", "--silent", action="store_true", help="Suppress output")
|
|
1843
|
+
parser.add_argument(
|
|
1844
|
+
"-m",
|
|
1845
|
+
"--manager",
|
|
1846
|
+
type=str,
|
|
1847
|
+
default=None,
|
|
1848
|
+
help="Container manager type: docker, podman, swarm (default: auto-detect)",
|
|
1849
|
+
)
|
|
1850
|
+
parser.add_argument("--log-file", type=str, default=None, help="Path to log file")
|
|
1851
|
+
parser.add_argument("--get-version", action="store_true", help="Get version info")
|
|
1852
|
+
parser.add_argument("--get-info", action="store_true", help="Get system info")
|
|
1853
|
+
parser.add_argument("--list-images", action="store_true", help="List images")
|
|
1854
|
+
parser.add_argument("--pull-image", type=str, default=None, help="Image to pull")
|
|
1855
|
+
parser.add_argument("--tag", type=str, default="latest", help="Image tag")
|
|
1856
|
+
parser.add_argument("--platform", type=str, default=None, help="Platform")
|
|
1857
|
+
parser.add_argument(
|
|
1858
|
+
"--remove-image", type=str, default=None, help="Image to remove"
|
|
1859
|
+
)
|
|
1860
|
+
parser.add_argument("--prune-images", action="store_true", help="Prune images")
|
|
1861
|
+
parser.add_argument(
|
|
1862
|
+
"--list-containers", action="store_true", help="List containers"
|
|
1863
|
+
)
|
|
1864
|
+
parser.add_argument("--all", action="store_true", help="Show all containers")
|
|
1865
|
+
parser.add_argument("--run-container", type=str, default=None, help="Image to run")
|
|
1866
|
+
parser.add_argument("--name", type=str, default=None, help="Container name")
|
|
1867
|
+
parser.add_argument("--command", type=str, default=None, help="Command to run")
|
|
1868
|
+
parser.add_argument("--detach", action="store_true", help="Detach mode")
|
|
1869
|
+
parser.add_argument("--ports", type=str, default=None, help="Port mappings")
|
|
1870
|
+
parser.add_argument("--volumes", type=str, default=None, help="Volume mappings")
|
|
1871
|
+
parser.add_argument(
|
|
1872
|
+
"--environment", type=str, default=None, help="Environment vars"
|
|
1873
|
+
)
|
|
1874
|
+
parser.add_argument(
|
|
1875
|
+
"--stop-container", type=str, default=None, help="Container to stop"
|
|
1876
|
+
)
|
|
1877
|
+
parser.add_argument("--timeout", type=int, default=10, help="Timeout in seconds")
|
|
1878
|
+
parser.add_argument(
|
|
1879
|
+
"--remove-container", type=str, default=None, help="Container to remove"
|
|
1880
|
+
)
|
|
1881
|
+
parser.add_argument(
|
|
1882
|
+
"--prune-containers", action="store_true", help="Prune containers"
|
|
1883
|
+
)
|
|
1884
|
+
parser.add_argument(
|
|
1885
|
+
"--get-container-logs", type=str, default=None, help="Container logs"
|
|
1886
|
+
)
|
|
1887
|
+
parser.add_argument("--tail", type=str, default="all", help="Tail lines")
|
|
1888
|
+
parser.add_argument(
|
|
1889
|
+
"--exec-in-container", type=str, default=None, help="Container to exec"
|
|
1890
|
+
)
|
|
1891
|
+
parser.add_argument("--exec-command", type=str, default=None, help="Exec command")
|
|
1892
|
+
parser.add_argument("--exec-detach", action="store_true", help="Detach exec")
|
|
1893
|
+
parser.add_argument("--list-volumes", action="store_true", help="List volumes")
|
|
1894
|
+
parser.add_argument(
|
|
1895
|
+
"--create-volume", type=str, default=None, help="Volume to create"
|
|
1896
|
+
)
|
|
1897
|
+
parser.add_argument(
|
|
1898
|
+
"--remove-volume", type=str, default=None, help="Volume to remove"
|
|
1899
|
+
)
|
|
1900
|
+
parser.add_argument("--prune-volumes", action="store_true", help="Prune volumes")
|
|
1901
|
+
parser.add_argument("--list-networks", action="store_true", help="List networks")
|
|
1902
|
+
parser.add_argument(
|
|
1903
|
+
"--create-network", type=str, default=None, help="Network to create"
|
|
1904
|
+
)
|
|
1905
|
+
parser.add_argument("--driver", type=str, default="bridge", help="Network driver")
|
|
1906
|
+
parser.add_argument(
|
|
1907
|
+
"--remove-network", type=str, default=None, help="Network to remove"
|
|
1908
|
+
)
|
|
1909
|
+
parser.add_argument("--prune-networks", action="store_true", help="Prune networks")
|
|
1910
|
+
parser.add_argument("--prune-system", action="store_true", help="Prune system")
|
|
1911
|
+
parser.add_argument("--compose-up", type=str, default=None, help="Compose file up")
|
|
1912
|
+
parser.add_argument("--build", action="store_true", help="Build images")
|
|
1913
|
+
parser.add_argument(
|
|
1914
|
+
"--compose-detach", action="store_true", default=True, help="Detach compose"
|
|
1915
|
+
)
|
|
1916
|
+
parser.add_argument(
|
|
1917
|
+
"--compose-down", type=str, default=None, help="Compose file down"
|
|
1918
|
+
)
|
|
1919
|
+
parser.add_argument("--compose-ps", type=str, default=None, help="Compose ps")
|
|
1920
|
+
parser.add_argument("--compose-logs", type=str, default=None, help="Compose logs")
|
|
1921
|
+
parser.add_argument("--service", type=str, default=None, help="Specific service")
|
|
1922
|
+
parser.add_argument("--init-swarm", action="store_true", help="Init swarm")
|
|
1923
|
+
parser.add_argument(
|
|
1924
|
+
"--advertise-addr", type=str, default=None, help="Advertise address"
|
|
1925
|
+
)
|
|
1926
|
+
parser.add_argument("--leave-swarm", action="store_true", help="Leave swarm")
|
|
1927
|
+
parser.add_argument("--list-nodes", action="store_true", help="List swarm nodes")
|
|
1928
|
+
parser.add_argument(
|
|
1929
|
+
"--list-services", action="store_true", help="List swarm services"
|
|
1930
|
+
)
|
|
1931
|
+
parser.add_argument(
|
|
1932
|
+
"--create-service", type=str, default=None, help="Service to create"
|
|
1933
|
+
)
|
|
1934
|
+
parser.add_argument("--image", type=str, default=None, help="Service image")
|
|
1935
|
+
parser.add_argument("--replicas", type=int, default=1, help="Replicas")
|
|
1936
|
+
parser.add_argument("--mounts", type=str, default=None, help="Mounts")
|
|
1937
|
+
parser.add_argument(
|
|
1938
|
+
"--remove-service", type=str, default=None, help="Service to remove"
|
|
1939
|
+
)
|
|
1940
|
+
parser.add_argument("--force", action="store_true", help="Force removal")
|
|
1941
|
+
parser.add_argument("-h", "--help", action="store_true", help="Show help")
|
|
1942
|
+
|
|
1943
|
+
args = parser.parse_args()
|
|
1081
1944
|
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
driver = arg
|
|
1164
|
-
elif opt == "--remove-network":
|
|
1165
|
-
remove_network = True
|
|
1166
|
-
remove_network_id = arg
|
|
1167
|
-
elif opt == "--compose-up":
|
|
1168
|
-
compose_up = True
|
|
1169
|
-
compose_up_file = arg
|
|
1170
|
-
elif opt == "--build":
|
|
1171
|
-
compose_build = True
|
|
1172
|
-
elif opt == "--compose-down":
|
|
1173
|
-
compose_down = True
|
|
1174
|
-
compose_down_file = arg
|
|
1175
|
-
elif opt == "--compose-ps":
|
|
1176
|
-
compose_ps = True
|
|
1177
|
-
compose_ps_file = arg
|
|
1178
|
-
elif opt == "--compose-logs":
|
|
1179
|
-
compose_logs = True
|
|
1180
|
-
compose_logs_file = arg
|
|
1181
|
-
elif opt == "--service":
|
|
1182
|
-
compose_service = arg
|
|
1183
|
-
elif opt == "--init-swarm":
|
|
1184
|
-
init_swarm = True
|
|
1185
|
-
elif opt == "--advertise-addr":
|
|
1186
|
-
advertise_addr = arg
|
|
1187
|
-
elif opt == "--leave-swarm":
|
|
1188
|
-
leave_swarm = True
|
|
1189
|
-
elif opt == "--list-nodes":
|
|
1190
|
-
list_nodes = True
|
|
1191
|
-
elif opt == "--list-services":
|
|
1192
|
-
list_services = True
|
|
1193
|
-
elif opt == "--create-service":
|
|
1194
|
-
create_service = True
|
|
1195
|
-
create_service_name = arg
|
|
1196
|
-
elif opt == "--image":
|
|
1197
|
-
service_image = arg
|
|
1198
|
-
elif opt == "--replicas":
|
|
1199
|
-
replicas = int(arg)
|
|
1200
|
-
elif opt == "--mounts":
|
|
1201
|
-
mounts_str = arg
|
|
1202
|
-
elif opt == "--remove-service":
|
|
1203
|
-
remove_service = True
|
|
1204
|
-
remove_service_id = arg
|
|
1945
|
+
if args.help:
|
|
1946
|
+
usage()
|
|
1947
|
+
sys.exit(0)
|
|
1948
|
+
|
|
1949
|
+
get_version = args.get_version
|
|
1950
|
+
get_info = args.get_info
|
|
1951
|
+
list_images = args.list_images
|
|
1952
|
+
pull_image = args.pull_image is not None
|
|
1953
|
+
pull_image_str = args.pull_image
|
|
1954
|
+
tag = args.tag
|
|
1955
|
+
platform = args.platform
|
|
1956
|
+
remove_image = args.remove_image is not None
|
|
1957
|
+
remove_image_str = args.remove_image
|
|
1958
|
+
prune_images = args.prune_images
|
|
1959
|
+
prune_images_all = args.all if prune_images else False
|
|
1960
|
+
force = args.force
|
|
1961
|
+
list_containers = args.list_containers
|
|
1962
|
+
all_containers = args.all if list_containers else False
|
|
1963
|
+
run_container = args.run_container is not None
|
|
1964
|
+
run_image = args.run_container
|
|
1965
|
+
name = args.name
|
|
1966
|
+
command = args.command
|
|
1967
|
+
detach = args.detach
|
|
1968
|
+
ports_str = args.ports
|
|
1969
|
+
volumes_str = args.volumes
|
|
1970
|
+
environment_str = args.environment
|
|
1971
|
+
stop_container = args.stop_container is not None
|
|
1972
|
+
stop_container_id = args.stop_container
|
|
1973
|
+
timeout = args.timeout
|
|
1974
|
+
remove_container = args.remove_container is not None
|
|
1975
|
+
remove_container_id = args.remove_container
|
|
1976
|
+
prune_containers = args.prune_containers
|
|
1977
|
+
get_container_logs = args.get_container_logs is not None
|
|
1978
|
+
container_logs_id = args.get_container_logs
|
|
1979
|
+
tail = args.tail
|
|
1980
|
+
exec_in_container = args.exec_in_container is not None
|
|
1981
|
+
exec_container_id = args.exec_in_container
|
|
1982
|
+
exec_command = args.exec_command
|
|
1983
|
+
exec_detach = args.exec_detach
|
|
1984
|
+
list_volumes = args.list_volumes
|
|
1985
|
+
create_volume = args.create_volume is not None
|
|
1986
|
+
create_volume_name = args.create_volume
|
|
1987
|
+
remove_volume = args.remove_volume is not None
|
|
1988
|
+
remove_volume_name = args.remove_volume
|
|
1989
|
+
prune_volumes = args.prune_volumes
|
|
1990
|
+
prune_volumes_all = args.all if prune_volumes else False
|
|
1991
|
+
list_networks = args.list_networks
|
|
1992
|
+
create_network = args.create_network is not None
|
|
1993
|
+
create_network_name = args.create_network
|
|
1994
|
+
driver = args.driver
|
|
1995
|
+
remove_network = args.remove_network is not None
|
|
1996
|
+
remove_network_id = args.remove_network
|
|
1997
|
+
prune_networks = args.prune_networks
|
|
1998
|
+
prune_system = args.prune_system
|
|
1999
|
+
prune_system_all = args.all if prune_system else False
|
|
2000
|
+
compose_up = args.compose_up is not None
|
|
2001
|
+
compose_up_file = args.compose_up
|
|
2002
|
+
compose_build = args.build
|
|
2003
|
+
compose_detach = args.compose_detach
|
|
2004
|
+
compose_down = args.compose_down is not None
|
|
2005
|
+
compose_down_file = args.compose_down
|
|
2006
|
+
compose_ps = args.compose_ps is not None
|
|
2007
|
+
compose_ps_file = args.compose_ps
|
|
2008
|
+
compose_logs = args.compose_logs is not None
|
|
2009
|
+
compose_logs_file = args.compose_logs
|
|
2010
|
+
compose_service = args.service
|
|
2011
|
+
init_swarm = args.init_swarm
|
|
2012
|
+
advertise_addr = args.advertise_addr
|
|
2013
|
+
leave_swarm = args.leave_swarm
|
|
2014
|
+
list_nodes = args.list_nodes
|
|
2015
|
+
list_services = args.list_services
|
|
2016
|
+
create_service = args.create_service is not None
|
|
2017
|
+
create_service_name = args.create_service
|
|
2018
|
+
service_image = args.image
|
|
2019
|
+
replicas = args.replicas
|
|
2020
|
+
mounts_str = args.mounts
|
|
2021
|
+
remove_service = args.remove_service is not None
|
|
2022
|
+
remove_service_id = args.remove_service
|
|
2023
|
+
manager_type = args.manager
|
|
2024
|
+
silent = args.silent
|
|
2025
|
+
log_file = args.log_file
|
|
1205
2026
|
|
|
1206
2027
|
manager = create_manager(manager_type, silent, log_file)
|
|
1207
2028
|
|
|
@@ -1224,6 +2045,9 @@ def main(argv):
|
|
|
1224
2045
|
raise ValueError("Image required for remove-image")
|
|
1225
2046
|
print(json.dumps(manager.remove_image(remove_image_str, force), indent=2))
|
|
1226
2047
|
|
|
2048
|
+
if prune_images:
|
|
2049
|
+
print(json.dumps(manager.prune_images(force, prune_images_all), indent=2))
|
|
2050
|
+
|
|
1227
2051
|
if list_containers:
|
|
1228
2052
|
print(json.dumps(manager.list_containers(all_containers), indent=2))
|
|
1229
2053
|
|
|
@@ -1269,6 +2093,9 @@ def main(argv):
|
|
|
1269
2093
|
json.dumps(manager.remove_container(remove_container_id, force), indent=2)
|
|
1270
2094
|
)
|
|
1271
2095
|
|
|
2096
|
+
if prune_containers:
|
|
2097
|
+
print(json.dumps(manager.prune_containers(), indent=2))
|
|
2098
|
+
|
|
1272
2099
|
if get_container_logs:
|
|
1273
2100
|
if not container_logs_id:
|
|
1274
2101
|
raise ValueError("Container ID required for get-container-logs")
|
|
@@ -1298,6 +2125,9 @@ def main(argv):
|
|
|
1298
2125
|
raise ValueError("Name required for remove-volume")
|
|
1299
2126
|
print(json.dumps(manager.remove_volume(remove_volume_name, force), indent=2))
|
|
1300
2127
|
|
|
2128
|
+
if prune_volumes:
|
|
2129
|
+
print(json.dumps(manager.prune_volumes(force, prune_volumes_all), indent=2))
|
|
2130
|
+
|
|
1301
2131
|
if list_networks:
|
|
1302
2132
|
print(json.dumps(manager.list_networks(), indent=2))
|
|
1303
2133
|
|
|
@@ -1311,6 +2141,12 @@ def main(argv):
|
|
|
1311
2141
|
raise ValueError("ID required for remove-network")
|
|
1312
2142
|
print(json.dumps(manager.remove_network(remove_network_id), indent=2))
|
|
1313
2143
|
|
|
2144
|
+
if prune_networks:
|
|
2145
|
+
print(json.dumps(manager.prune_networks(), indent=2))
|
|
2146
|
+
|
|
2147
|
+
if prune_system:
|
|
2148
|
+
print(json.dumps(manager.prune_system(force, prune_system_all), indent=2))
|
|
2149
|
+
|
|
1314
2150
|
if compose_up:
|
|
1315
2151
|
if not compose_up_file:
|
|
1316
2152
|
raise ValueError("File required for compose-up")
|
|
@@ -1378,4 +2214,4 @@ if __name__ == "__main__":
|
|
|
1378
2214
|
if len(sys.argv) < 2:
|
|
1379
2215
|
usage()
|
|
1380
2216
|
sys.exit(2)
|
|
1381
|
-
|
|
2217
|
+
container_manager()
|