container-manager-mcp 1.0.3__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 -17
- container_manager_mcp/__main__.py +2 -2
- container_manager_mcp/container_manager.py +555 -441
- container_manager_mcp/container_manager_a2a.py +339 -0
- container_manager_mcp/container_manager_mcp.py +2055 -1323
- 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-1.0.3.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-1.0.3.dist-info/METADATA +0 -243
- container_manager_mcp-1.0.3.dist-info/RECORD +0 -10
- container_manager_mcp-1.0.3.dist-info/entry_points.txt +0 -3
- {container_manager_mcp-1.0.3.dist-info → container_manager_mcp-1.2.0.dist-info}/WHEEL +0 -0
- {container_manager_mcp-1.0.3.dist-info → container_manager_mcp-1.2.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -8,10 +8,11 @@ from abc import ABC, abstractmethod
|
|
|
8
8
|
from typing import List, Dict, Optional, Any
|
|
9
9
|
import argparse
|
|
10
10
|
import json
|
|
11
|
+
import shutil
|
|
11
12
|
import subprocess
|
|
12
13
|
from datetime import datetime
|
|
13
|
-
import dateutil.parser
|
|
14
14
|
import platform
|
|
15
|
+
import traceback
|
|
15
16
|
|
|
16
17
|
try:
|
|
17
18
|
import docker
|
|
@@ -40,7 +41,7 @@ class ContainerManagerBase(ABC):
|
|
|
40
41
|
log_file = os.path.join(script_dir, "container_manager.log")
|
|
41
42
|
logging.basicConfig(
|
|
42
43
|
filename=log_file,
|
|
43
|
-
level=logging.
|
|
44
|
+
level=logging.DEBUG, # Changed to DEBUG for more detailed logging
|
|
44
45
|
format="%(asctime)s - %(levelname)s - %(message)s",
|
|
45
46
|
)
|
|
46
47
|
self.logger = logging.getLogger(__name__)
|
|
@@ -57,7 +58,10 @@ class ContainerManagerBase(ABC):
|
|
|
57
58
|
if result:
|
|
58
59
|
self.logger.info(f"Result: {result}")
|
|
59
60
|
if error:
|
|
60
|
-
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()}")
|
|
61
65
|
|
|
62
66
|
def _format_size(self, size_bytes: int) -> str:
|
|
63
67
|
"""Helper to format bytes to human-readable (e.g., 1.23GB)."""
|
|
@@ -70,17 +74,38 @@ class ContainerManagerBase(ABC):
|
|
|
70
74
|
return f"{size_bytes:.2f}PB"
|
|
71
75
|
|
|
72
76
|
def _parse_timestamp(self, timestamp: Any) -> str:
|
|
73
|
-
"""Parse timestamp (integer or string) to ISO 8601 string."""
|
|
77
|
+
"""Parse timestamp (integer, float, or string) to ISO 8601 string."""
|
|
74
78
|
if not timestamp:
|
|
75
79
|
return "unknown"
|
|
80
|
+
|
|
81
|
+
# Handle numeric timestamps (Unix timestamps in seconds)
|
|
76
82
|
if isinstance(timestamp, (int, float)):
|
|
77
|
-
return datetime.fromtimestamp(timestamp).strftime("%Y-%m-%dT%H:%M:%S")
|
|
78
|
-
if isinstance(timestamp, str):
|
|
79
83
|
try:
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
except ValueError:
|
|
84
|
+
return datetime.fromtimestamp(timestamp).strftime("%Y-%m-%dT%H:%M:%S")
|
|
85
|
+
except (ValueError, OSError):
|
|
83
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
|
+
|
|
84
109
|
return "unknown"
|
|
85
110
|
|
|
86
111
|
@abstractmethod
|
|
@@ -135,7 +160,7 @@ class ContainerManagerBase(ABC):
|
|
|
135
160
|
pass
|
|
136
161
|
|
|
137
162
|
@abstractmethod
|
|
138
|
-
def prune_containers(self
|
|
163
|
+
def prune_containers(self) -> Dict:
|
|
139
164
|
pass
|
|
140
165
|
|
|
141
166
|
@abstractmethod
|
|
@@ -245,6 +270,78 @@ class DockerManager(ContainerManagerBase):
|
|
|
245
270
|
self.logger.error(f"Failed to connect to Docker daemon: {str(e)}")
|
|
246
271
|
raise RuntimeError(f"Failed to connect to Docker: {str(e)}")
|
|
247
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)
|
|
309
|
+
def get_version(self) -> Dict:
|
|
310
|
+
params = {}
|
|
311
|
+
try:
|
|
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
|
+
}
|
|
320
|
+
self.log_action("get_version", params, result)
|
|
321
|
+
return result
|
|
322
|
+
except Exception as e:
|
|
323
|
+
self.log_action("get_version", params, error=e)
|
|
324
|
+
raise RuntimeError(f"Failed to get version: {str(e)}")
|
|
325
|
+
|
|
326
|
+
def get_info(self) -> Dict:
|
|
327
|
+
params = {}
|
|
328
|
+
try:
|
|
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
|
+
}
|
|
339
|
+
self.log_action("get_info", params, result)
|
|
340
|
+
return result
|
|
341
|
+
except Exception as e:
|
|
342
|
+
self.log_action("get_info", params, error=e)
|
|
343
|
+
raise RuntimeError(f"Failed to get info: {str(e)}")
|
|
344
|
+
|
|
248
345
|
def list_images(self) -> List[Dict]:
|
|
249
346
|
params = {}
|
|
250
347
|
try:
|
|
@@ -314,21 +411,55 @@ class DockerManager(ContainerManagerBase):
|
|
|
314
411
|
self.log_action("pull_image", params, error=e)
|
|
315
412
|
raise RuntimeError(f"Failed to pull image: {str(e)}")
|
|
316
413
|
|
|
414
|
+
def remove_image(self, image: str, force: bool = False) -> Dict:
|
|
415
|
+
params = {"image": image, "force": force}
|
|
416
|
+
try:
|
|
417
|
+
self.client.images.remove(image, force=force)
|
|
418
|
+
result = {"removed": image}
|
|
419
|
+
self.log_action("remove_image", params, result)
|
|
420
|
+
return result
|
|
421
|
+
except Exception as e:
|
|
422
|
+
self.log_action("remove_image", params, error=e)
|
|
423
|
+
raise RuntimeError(f"Failed to remove image: {str(e)}")
|
|
424
|
+
|
|
317
425
|
def prune_images(self, force: bool = False, all: bool = False) -> Dict:
|
|
318
426
|
params = {"force": force, "all": all}
|
|
319
427
|
try:
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
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
|
|
332
463
|
except Exception as e:
|
|
333
464
|
self.log_action("prune_images", params, error=e)
|
|
334
465
|
raise RuntimeError(f"Failed to prune images: {str(e)}")
|
|
@@ -386,7 +517,7 @@ class DockerManager(ContainerManagerBase):
|
|
|
386
517
|
}
|
|
387
518
|
try:
|
|
388
519
|
container = self.client.containers.run(
|
|
389
|
-
image,
|
|
520
|
+
image=image,
|
|
390
521
|
command=command,
|
|
391
522
|
name=name,
|
|
392
523
|
detach=detach,
|
|
@@ -399,14 +530,17 @@ class DockerManager(ContainerManagerBase):
|
|
|
399
530
|
self.log_action("run_container", params, result)
|
|
400
531
|
return result
|
|
401
532
|
attrs = container.attrs
|
|
402
|
-
ports = attrs.get("NetworkSettings", {}).get("Ports", {})
|
|
403
533
|
port_mappings = []
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
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
|
+
)
|
|
410
544
|
created = attrs.get("Created", None)
|
|
411
545
|
created_str = self._parse_timestamp(created)
|
|
412
546
|
result = {
|
|
@@ -423,134 +557,6 @@ class DockerManager(ContainerManagerBase):
|
|
|
423
557
|
self.log_action("run_container", params, error=e)
|
|
424
558
|
raise RuntimeError(f"Failed to run container: {str(e)}")
|
|
425
559
|
|
|
426
|
-
def prune_containers(self, force: bool = False) -> Dict:
|
|
427
|
-
params = {"force": force}
|
|
428
|
-
try:
|
|
429
|
-
result = self.client.containers.prune()
|
|
430
|
-
pruned = {
|
|
431
|
-
"space_reclaimed": self._format_size(result["SpaceReclaimed"]),
|
|
432
|
-
"containers_removed": (
|
|
433
|
-
[c["Id"][7:19] for c in result["ContainersRemoved"]]
|
|
434
|
-
if result["ContainersRemoved"]
|
|
435
|
-
else []
|
|
436
|
-
),
|
|
437
|
-
}
|
|
438
|
-
self.log_action("prune_containers", params, pruned)
|
|
439
|
-
return pruned
|
|
440
|
-
except Exception as e:
|
|
441
|
-
self.log_action("prune_containers", params, error=e)
|
|
442
|
-
raise RuntimeError(f"Failed to prune containers: {str(e)}")
|
|
443
|
-
|
|
444
|
-
def list_networks(self) -> List[Dict]:
|
|
445
|
-
params = {}
|
|
446
|
-
try:
|
|
447
|
-
networks = self.client.networks.list()
|
|
448
|
-
result = []
|
|
449
|
-
for net in networks:
|
|
450
|
-
attrs = net.attrs
|
|
451
|
-
containers = len(attrs.get("Containers", {}))
|
|
452
|
-
created = attrs.get("Created", None)
|
|
453
|
-
created_str = self._parse_timestamp(created)
|
|
454
|
-
simplified = {
|
|
455
|
-
"id": attrs.get("Id", "unknown")[7:19],
|
|
456
|
-
"name": attrs.get("Name", "unknown"),
|
|
457
|
-
"driver": attrs.get("Driver", "unknown"),
|
|
458
|
-
"scope": attrs.get("Scope", "unknown"),
|
|
459
|
-
"containers": containers,
|
|
460
|
-
"created": created_str,
|
|
461
|
-
}
|
|
462
|
-
result.append(simplified)
|
|
463
|
-
self.log_action("list_networks", params, result)
|
|
464
|
-
return result
|
|
465
|
-
except Exception as e:
|
|
466
|
-
self.log_action("list_networks", params, error=e)
|
|
467
|
-
raise RuntimeError(f"Failed to list networks: {str(e)}")
|
|
468
|
-
|
|
469
|
-
def create_network(self, name: str, driver: str = "bridge") -> Dict:
|
|
470
|
-
params = {"name": name, "driver": driver}
|
|
471
|
-
try:
|
|
472
|
-
network = self.client.networks.create(name, driver=driver)
|
|
473
|
-
attrs = network.attrs
|
|
474
|
-
created = attrs.get("Created", None)
|
|
475
|
-
created_str = self._parse_timestamp(created)
|
|
476
|
-
result = {
|
|
477
|
-
"id": attrs.get("Id", "unknown")[7:19],
|
|
478
|
-
"name": attrs.get("Name", name),
|
|
479
|
-
"driver": attrs.get("Driver", driver),
|
|
480
|
-
"scope": attrs.get("Scope", "unknown"),
|
|
481
|
-
"created": created_str,
|
|
482
|
-
}
|
|
483
|
-
self.log_action("create_network", params, result)
|
|
484
|
-
return result
|
|
485
|
-
except Exception as e:
|
|
486
|
-
self.log_action("create_network", params, error=e)
|
|
487
|
-
raise RuntimeError(f"Failed to create network: {str(e)}")
|
|
488
|
-
|
|
489
|
-
def prune_networks(self) -> Dict:
|
|
490
|
-
params = {}
|
|
491
|
-
try:
|
|
492
|
-
result = self.client.networks.prune()
|
|
493
|
-
pruned = {
|
|
494
|
-
"space_reclaimed": self._format_size(result["SpaceReclaimed"]),
|
|
495
|
-
"networks_removed": (
|
|
496
|
-
[n["Id"][7:19] for n in result["NetworksRemoved"]]
|
|
497
|
-
if result["NetworksRemoved"]
|
|
498
|
-
else []
|
|
499
|
-
),
|
|
500
|
-
}
|
|
501
|
-
self.log_action("prune_networks", params, pruned)
|
|
502
|
-
return pruned
|
|
503
|
-
except Exception as e:
|
|
504
|
-
self.log_action("prune_networks", params, error=e)
|
|
505
|
-
raise RuntimeError(f"Failed to prune networks: {str(e)}")
|
|
506
|
-
|
|
507
|
-
def get_version(self) -> Dict:
|
|
508
|
-
params = {}
|
|
509
|
-
try:
|
|
510
|
-
version = self.client.version()
|
|
511
|
-
result = {
|
|
512
|
-
"version": version.get("Version", "unknown"),
|
|
513
|
-
"api_version": version.get("ApiVersion", "unknown"),
|
|
514
|
-
"os": version.get("Os", "unknown"),
|
|
515
|
-
"arch": version.get("Arch", "unknown"),
|
|
516
|
-
"build_time": version.get("BuildTime", "unknown"),
|
|
517
|
-
}
|
|
518
|
-
self.log_action("get_version", params, result)
|
|
519
|
-
return result
|
|
520
|
-
except Exception as e:
|
|
521
|
-
self.log_action("get_version", params, error=e)
|
|
522
|
-
raise RuntimeError(f"Failed to get version: {str(e)}")
|
|
523
|
-
|
|
524
|
-
def get_info(self) -> Dict:
|
|
525
|
-
params = {}
|
|
526
|
-
try:
|
|
527
|
-
info = self.client.info()
|
|
528
|
-
result = {
|
|
529
|
-
"containers_total": info.get("Containers", 0),
|
|
530
|
-
"containers_running": info.get("ContainersRunning", 0),
|
|
531
|
-
"images": info.get("Images", 0),
|
|
532
|
-
"driver": info.get("Driver", "unknown"),
|
|
533
|
-
"platform": f"{info.get('OperatingSystem', 'unknown')} {info.get('Architecture', 'unknown')}",
|
|
534
|
-
"memory_total": self._format_size(info.get("MemTotal", 0)),
|
|
535
|
-
"swap_total": self._format_size(info.get("SwapTotal", 0)),
|
|
536
|
-
}
|
|
537
|
-
self.log_action("get_info", params, result)
|
|
538
|
-
return result
|
|
539
|
-
except Exception as e:
|
|
540
|
-
self.log_action("get_info", params, error=e)
|
|
541
|
-
raise RuntimeError(f"Failed to get info: {str(e)}")
|
|
542
|
-
|
|
543
|
-
def remove_image(self, image: str, force: bool = False) -> Dict:
|
|
544
|
-
params = {"image": image, "force": force}
|
|
545
|
-
try:
|
|
546
|
-
self.client.images.remove(image, force=force)
|
|
547
|
-
result = {"removed": image}
|
|
548
|
-
self.log_action("remove_image", params, result)
|
|
549
|
-
return result
|
|
550
|
-
except Exception as e:
|
|
551
|
-
self.log_action("remove_image", params, error=e)
|
|
552
|
-
raise RuntimeError(f"Failed to remove image: {str(e)}")
|
|
553
|
-
|
|
554
560
|
def stop_container(self, container_id: str, timeout: int = 10) -> Dict:
|
|
555
561
|
params = {"container_id": container_id, "timeout": timeout}
|
|
556
562
|
try:
|
|
@@ -575,6 +581,34 @@ class DockerManager(ContainerManagerBase):
|
|
|
575
581
|
self.log_action("remove_container", params, error=e)
|
|
576
582
|
raise RuntimeError(f"Failed to remove container: {str(e)}")
|
|
577
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
|
+
|
|
578
612
|
def get_container_logs(self, container_id: str, tail: str = "all") -> str:
|
|
579
613
|
params = {"container_id": container_id, "tail": tail}
|
|
580
614
|
try:
|
|
@@ -660,7 +694,6 @@ class DockerManager(ContainerManagerBase):
|
|
|
660
694
|
params = {"force": force, "all": all}
|
|
661
695
|
try:
|
|
662
696
|
if all:
|
|
663
|
-
# Remove all volumes (equivalent to --all, but docker doesn't have --all for prune; we list and remove)
|
|
664
697
|
volumes = self.client.volumes.list(all=True)
|
|
665
698
|
removed = []
|
|
666
699
|
for v in volumes:
|
|
@@ -668,20 +701,25 @@ class DockerManager(ContainerManagerBase):
|
|
|
668
701
|
v.remove(force=force)
|
|
669
702
|
removed.append(v.attrs["Name"])
|
|
670
703
|
except Exception as e:
|
|
671
|
-
|
|
672
|
-
|
|
704
|
+
self.logger.info(
|
|
705
|
+
f"Info: Failed to remove volume {v.attrs.get('Name', 'unknown')}: {e}"
|
|
706
|
+
)
|
|
707
|
+
continue
|
|
673
708
|
result = {
|
|
674
709
|
"volumes_removed": removed,
|
|
675
710
|
"space_reclaimed": "N/A (all volumes)",
|
|
676
711
|
}
|
|
677
712
|
else:
|
|
678
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}")
|
|
679
717
|
pruned = {
|
|
680
|
-
"space_reclaimed": self._format_size(
|
|
718
|
+
"space_reclaimed": self._format_size(
|
|
719
|
+
result.get("SpaceReclaimed", 0)
|
|
720
|
+
),
|
|
681
721
|
"volumes_removed": (
|
|
682
|
-
[v["Name"] for v in result
|
|
683
|
-
if result["VolumesRemoved"]
|
|
684
|
-
else []
|
|
722
|
+
[v["Name"] for v in result.get("VolumesDeleted", [])]
|
|
685
723
|
),
|
|
686
724
|
}
|
|
687
725
|
result = pruned
|
|
@@ -691,46 +729,81 @@ class DockerManager(ContainerManagerBase):
|
|
|
691
729
|
self.log_action("prune_volumes", params, error=e)
|
|
692
730
|
raise RuntimeError(f"Failed to prune volumes: {str(e)}")
|
|
693
731
|
|
|
694
|
-
def
|
|
695
|
-
params = {
|
|
732
|
+
def list_networks(self) -> List[Dict]:
|
|
733
|
+
params = {}
|
|
696
734
|
try:
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
735
|
+
networks = self.client.networks.list()
|
|
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)
|
|
751
|
+
self.log_action("list_networks", params, result)
|
|
752
|
+
return result
|
|
753
|
+
except Exception as e:
|
|
754
|
+
self.log_action("list_networks", params, error=e)
|
|
755
|
+
raise RuntimeError(f"Failed to list networks: {str(e)}")
|
|
705
756
|
|
|
706
|
-
def
|
|
707
|
-
params = {"
|
|
757
|
+
def create_network(self, name: str, driver: str = "bridge") -> Dict:
|
|
758
|
+
params = {"name": name, "driver": driver}
|
|
708
759
|
try:
|
|
709
|
-
|
|
710
|
-
|
|
760
|
+
network = self.client.networks.create(name, driver=driver)
|
|
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
|
+
}
|
|
771
|
+
self.log_action("create_network", params, result)
|
|
772
|
+
return result
|
|
773
|
+
except Exception as e:
|
|
774
|
+
self.log_action("create_network", params, error=e)
|
|
775
|
+
raise RuntimeError(f"Failed to create network: {str(e)}")
|
|
776
|
+
|
|
777
|
+
def remove_network(self, network_id: str) -> Dict:
|
|
778
|
+
params = {"network_id": network_id}
|
|
779
|
+
try:
|
|
780
|
+
network = self.client.networks.get(network_id)
|
|
781
|
+
network.remove()
|
|
782
|
+
result = {"removed": network_id}
|
|
783
|
+
self.log_action("remove_network", params, result)
|
|
784
|
+
return result
|
|
785
|
+
except Exception as e:
|
|
786
|
+
self.log_action("remove_network", params, error=e)
|
|
787
|
+
raise RuntimeError(f"Failed to remove network: {str(e)}")
|
|
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}")
|
|
711
796
|
pruned = {
|
|
712
|
-
"space_reclaimed": self._format_size(result
|
|
713
|
-
"
|
|
714
|
-
[
|
|
715
|
-
if "ImagesDeleted" in result
|
|
716
|
-
else []
|
|
717
|
-
),
|
|
718
|
-
"containers_removed": (
|
|
719
|
-
[c["Id"][7:19] for c in result["ContainersDeleted"]]
|
|
720
|
-
if "ContainersDeleted" in result
|
|
721
|
-
else []
|
|
722
|
-
),
|
|
723
|
-
"volumes_removed": (
|
|
724
|
-
[v["Name"] for v in result["VolumesDeleted"]]
|
|
725
|
-
if "VolumesDeleted" in result
|
|
726
|
-
else []
|
|
797
|
+
"space_reclaimed": self._format_size(result.get("SpaceReclaimed", 0)),
|
|
798
|
+
"networks_removed": (
|
|
799
|
+
[n["Id"][7:19] for n in result.get("NetworksDeleted", [])]
|
|
727
800
|
),
|
|
728
801
|
}
|
|
729
|
-
self.log_action("
|
|
802
|
+
self.log_action("prune_networks", params, pruned)
|
|
730
803
|
return pruned
|
|
731
804
|
except Exception as e:
|
|
732
|
-
self.log_action("
|
|
733
|
-
raise RuntimeError(f"Failed to prune
|
|
805
|
+
self.log_action("prune_networks", params, error=e)
|
|
806
|
+
raise RuntimeError(f"Failed to prune networks: {str(e)}")
|
|
734
807
|
|
|
735
808
|
def compose_up(
|
|
736
809
|
self, compose_file: str, detach: bool = True, build: bool = False
|
|
@@ -957,17 +1030,14 @@ class DockerManager(ContainerManagerBase):
|
|
|
957
1030
|
class PodmanManager(ContainerManagerBase):
|
|
958
1031
|
def __init__(self, silent: bool = False, log_file: Optional[str] = None):
|
|
959
1032
|
super().__init__(silent, log_file)
|
|
960
|
-
|
|
961
1033
|
if PodmanClient is None:
|
|
962
1034
|
raise ImportError("Please install podman-py: pip install podman")
|
|
963
|
-
|
|
964
1035
|
base_url = self._autodetect_podman_url()
|
|
965
1036
|
if base_url is None:
|
|
966
1037
|
self.logger.error(
|
|
967
1038
|
"No valid Podman socket found after trying all known locations"
|
|
968
1039
|
)
|
|
969
1040
|
raise RuntimeError("Failed to connect to Podman: No valid socket found")
|
|
970
|
-
|
|
971
1041
|
try:
|
|
972
1042
|
self.client = PodmanClient(base_url=base_url)
|
|
973
1043
|
self.logger.info(f"Connected to Podman with base_url: {base_url}")
|
|
@@ -1002,7 +1072,6 @@ class PodmanManager(ContainerManagerBase):
|
|
|
1002
1072
|
"""Attempt to connect to Podman with the given base_url."""
|
|
1003
1073
|
try:
|
|
1004
1074
|
client = PodmanClient(base_url=base_url)
|
|
1005
|
-
# Test connection
|
|
1006
1075
|
client.version()
|
|
1007
1076
|
return client
|
|
1008
1077
|
except PodmanError as e:
|
|
@@ -1011,52 +1080,242 @@ class PodmanManager(ContainerManagerBase):
|
|
|
1011
1080
|
|
|
1012
1081
|
def _autodetect_podman_url(self) -> Optional[str]:
|
|
1013
1082
|
"""Autodetect the appropriate Podman socket URL based on platform."""
|
|
1014
|
-
|
|
1015
|
-
base_url = os.environ.get("PODMAN_BASE_URL")
|
|
1083
|
+
base_url = os.environ.get("CONTAINER_MANAGER_PODMAN_BASE_URL")
|
|
1016
1084
|
if base_url:
|
|
1017
|
-
self.logger.info(
|
|
1085
|
+
self.logger.info(
|
|
1086
|
+
f"Using CONTAINER_MANAGER_PODMAN_BASE_URL from environment: {base_url}"
|
|
1087
|
+
)
|
|
1018
1088
|
return base_url
|
|
1019
|
-
|
|
1020
1089
|
system = platform.system()
|
|
1021
1090
|
is_wsl = self._is_wsl()
|
|
1022
|
-
|
|
1023
|
-
# Define socket candidates based on platform
|
|
1024
1091
|
socket_candidates = []
|
|
1025
1092
|
if system == "Windows" and not is_wsl:
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
socket_candidates.append("npipe:////./pipe/docker_engine")
|
|
1029
|
-
# Fallback to WSL2 distro sockets if running in a mixed setup
|
|
1093
|
+
if not self._is_podman_machine_running():
|
|
1094
|
+
raise RuntimeError("Podman Machine is not running on Windows system")
|
|
1030
1095
|
socket_candidates.extend(
|
|
1031
1096
|
[
|
|
1032
|
-
"
|
|
1033
|
-
"unix:///
|
|
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",
|
|
1034
1102
|
]
|
|
1035
1103
|
)
|
|
1036
1104
|
elif system == "Linux" or is_wsl:
|
|
1037
|
-
# Linux or WSL2 distro: prioritize rootless, then rootful
|
|
1038
1105
|
uid = os.getuid()
|
|
1039
1106
|
socket_candidates.extend(
|
|
1040
1107
|
[
|
|
1041
|
-
f"unix:///run/user/{uid}/podman/podman.sock",
|
|
1042
|
-
"unix:///run/podman/podman.sock",
|
|
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",
|
|
1043
1112
|
]
|
|
1044
1113
|
)
|
|
1045
|
-
|
|
1046
|
-
# Try each socket candidate
|
|
1047
1114
|
for url in socket_candidates:
|
|
1048
|
-
# For Unix sockets, check if the file exists (on Linux/WSL2)
|
|
1049
|
-
if url.startswith("unix://") and (system == "Linux" or is_wsl):
|
|
1050
|
-
socket_path = url.replace("unix://", "")
|
|
1051
|
-
if not os.path.exists(socket_path):
|
|
1052
|
-
self.logger.debug(f"Socket {socket_path} does not exist")
|
|
1053
|
-
continue
|
|
1054
1115
|
client = self._try_connect(url)
|
|
1055
1116
|
if client:
|
|
1056
1117
|
return url
|
|
1057
|
-
|
|
1058
1118
|
return None
|
|
1059
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)}")
|
|
1281
|
+
|
|
1282
|
+
def get_version(self) -> Dict:
|
|
1283
|
+
params = {}
|
|
1284
|
+
try:
|
|
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
|
+
}
|
|
1293
|
+
self.log_action("get_version", params, result)
|
|
1294
|
+
return result
|
|
1295
|
+
except Exception as e:
|
|
1296
|
+
self.log_action("get_version", params, error=e)
|
|
1297
|
+
raise RuntimeError(f"Failed to get version: {str(e)}")
|
|
1298
|
+
|
|
1299
|
+
def get_info(self) -> Dict:
|
|
1300
|
+
params = {}
|
|
1301
|
+
try:
|
|
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
|
+
}
|
|
1313
|
+
self.log_action("get_info", params, result)
|
|
1314
|
+
return result
|
|
1315
|
+
except Exception as e:
|
|
1316
|
+
self.log_action("get_info", params, error=e)
|
|
1317
|
+
raise RuntimeError(f"Failed to get info: {str(e)}")
|
|
1318
|
+
|
|
1060
1319
|
def list_images(self) -> List[Dict]:
|
|
1061
1320
|
params = {}
|
|
1062
1321
|
try:
|
|
@@ -1122,22 +1381,16 @@ class PodmanManager(ContainerManagerBase):
|
|
|
1122
1381
|
self.log_action("pull_image", params, error=e)
|
|
1123
1382
|
raise RuntimeError(f"Failed to pull image: {str(e)}")
|
|
1124
1383
|
|
|
1125
|
-
def
|
|
1126
|
-
params = {"
|
|
1384
|
+
def remove_image(self, image: str, force: bool = False) -> Dict:
|
|
1385
|
+
params = {"image": image, "force": force}
|
|
1127
1386
|
try:
|
|
1128
|
-
|
|
1129
|
-
result =
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
"images_removed": [
|
|
1133
|
-
img["Id"][7:19] for img in result.get("ImagesRemoved", [])
|
|
1134
|
-
],
|
|
1135
|
-
}
|
|
1136
|
-
self.log_action("prune_images", params, pruned)
|
|
1137
|
-
return pruned
|
|
1387
|
+
self.client.images.remove(image, force=force)
|
|
1388
|
+
result = {"removed": image}
|
|
1389
|
+
self.log_action("remove_image", params, result)
|
|
1390
|
+
return result
|
|
1138
1391
|
except Exception as e:
|
|
1139
|
-
self.log_action("
|
|
1140
|
-
raise RuntimeError(f"Failed to
|
|
1392
|
+
self.log_action("remove_image", params, error=e)
|
|
1393
|
+
raise RuntimeError(f"Failed to remove image: {str(e)}")
|
|
1141
1394
|
|
|
1142
1395
|
def list_containers(self, all: bool = False) -> List[Dict]:
|
|
1143
1396
|
params = {"all": all}
|
|
@@ -1203,12 +1456,15 @@ class PodmanManager(ContainerManagerBase):
|
|
|
1203
1456
|
self.log_action("run_container", params, result)
|
|
1204
1457
|
return result
|
|
1205
1458
|
attrs = container.attrs
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
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
|
+
]
|
|
1212
1468
|
created = attrs.get("Created", None)
|
|
1213
1469
|
created_str = self._parse_timestamp(created)
|
|
1214
1470
|
result = {
|
|
@@ -1225,131 +1481,6 @@ class PodmanManager(ContainerManagerBase):
|
|
|
1225
1481
|
self.log_action("run_container", params, error=e)
|
|
1226
1482
|
raise RuntimeError(f"Failed to run container: {str(e)}")
|
|
1227
1483
|
|
|
1228
|
-
def prune_containers(self, force: bool = False) -> Dict:
|
|
1229
|
-
params = {"force": force}
|
|
1230
|
-
try:
|
|
1231
|
-
result = self.client.containers.prune()
|
|
1232
|
-
pruned = {
|
|
1233
|
-
"space_reclaimed": self._format_size(result.get("SpaceReclaimed", 0)),
|
|
1234
|
-
"containers_removed": [
|
|
1235
|
-
c["Id"][7:19] for c in result.get("ContainersRemoved", [])
|
|
1236
|
-
],
|
|
1237
|
-
}
|
|
1238
|
-
self.log_action("prune_containers", params, pruned)
|
|
1239
|
-
return pruned
|
|
1240
|
-
except Exception as e:
|
|
1241
|
-
self.log_action("prune_containers", params, error=e)
|
|
1242
|
-
raise RuntimeError(f"Failed to prune containers: {str(e)}")
|
|
1243
|
-
|
|
1244
|
-
def list_networks(self) -> List[Dict]:
|
|
1245
|
-
params = {}
|
|
1246
|
-
try:
|
|
1247
|
-
networks = self.client.networks.list()
|
|
1248
|
-
result = []
|
|
1249
|
-
for net in networks:
|
|
1250
|
-
attrs = net.attrs
|
|
1251
|
-
containers = len(attrs.get("Containers", {}))
|
|
1252
|
-
created = attrs.get("Created", None)
|
|
1253
|
-
created_str = self._parse_timestamp(created)
|
|
1254
|
-
simplified = {
|
|
1255
|
-
"id": attrs.get("Id", "unknown")[7:19],
|
|
1256
|
-
"name": attrs.get("Name", "unknown"),
|
|
1257
|
-
"driver": attrs.get("Driver", "unknown"),
|
|
1258
|
-
"scope": attrs.get("Scope", "unknown"),
|
|
1259
|
-
"containers": containers,
|
|
1260
|
-
"created": created_str,
|
|
1261
|
-
}
|
|
1262
|
-
result.append(simplified)
|
|
1263
|
-
self.log_action("list_networks", params, result)
|
|
1264
|
-
return result
|
|
1265
|
-
except Exception as e:
|
|
1266
|
-
self.log_action("list_networks", params, error=e)
|
|
1267
|
-
raise RuntimeError(f"Failed to list networks: {str(e)}")
|
|
1268
|
-
|
|
1269
|
-
def create_network(self, name: str, driver: str = "bridge") -> Dict:
|
|
1270
|
-
params = {"name": name, "driver": driver}
|
|
1271
|
-
try:
|
|
1272
|
-
network = self.client.networks.create(name, driver=driver)
|
|
1273
|
-
attrs = network.attrs
|
|
1274
|
-
created = attrs.get("Created", None)
|
|
1275
|
-
created_str = self._parse_timestamp(created)
|
|
1276
|
-
result = {
|
|
1277
|
-
"id": attrs.get("Id", "unknown")[7:19],
|
|
1278
|
-
"name": attrs.get("Name", name),
|
|
1279
|
-
"driver": attrs.get("Driver", driver),
|
|
1280
|
-
"scope": attrs.get("Scope", "unknown"),
|
|
1281
|
-
"created": created_str,
|
|
1282
|
-
}
|
|
1283
|
-
self.log_action("create_network", params, result)
|
|
1284
|
-
return result
|
|
1285
|
-
except Exception as e:
|
|
1286
|
-
self.log_action("create_network", params, error=e)
|
|
1287
|
-
raise RuntimeError(f"Failed to create network: {str(e)}")
|
|
1288
|
-
|
|
1289
|
-
def prune_networks(self) -> Dict:
|
|
1290
|
-
params = {}
|
|
1291
|
-
try:
|
|
1292
|
-
result = self.client.networks.prune()
|
|
1293
|
-
pruned = {
|
|
1294
|
-
"space_reclaimed": self._format_size(result.get("SpaceReclaimed", 0)),
|
|
1295
|
-
"networks_removed": [
|
|
1296
|
-
n["Id"][7:19] for n in result.get("NetworksRemoved", [])
|
|
1297
|
-
],
|
|
1298
|
-
}
|
|
1299
|
-
self.log_action("prune_networks", params, pruned)
|
|
1300
|
-
return pruned
|
|
1301
|
-
except Exception as e:
|
|
1302
|
-
self.log_action("prune_networks", params, error=e)
|
|
1303
|
-
raise RuntimeError(f"Failed to prune networks: {str(e)}")
|
|
1304
|
-
|
|
1305
|
-
def get_version(self) -> Dict:
|
|
1306
|
-
params = {}
|
|
1307
|
-
try:
|
|
1308
|
-
version = self.client.version()
|
|
1309
|
-
result = {
|
|
1310
|
-
"version": version.get("Version", "unknown"),
|
|
1311
|
-
"api_version": version.get("APIVersion", "unknown"),
|
|
1312
|
-
"os": version.get("Os", "unknown"),
|
|
1313
|
-
"arch": version.get("Arch", "unknown"),
|
|
1314
|
-
"build_time": version.get("BuildTime", "unknown"),
|
|
1315
|
-
}
|
|
1316
|
-
self.log_action("get_version", params, result)
|
|
1317
|
-
return result
|
|
1318
|
-
except Exception as e:
|
|
1319
|
-
self.log_action("get_version", params, error=e)
|
|
1320
|
-
raise RuntimeError(f"Failed to get version: {str(e)}")
|
|
1321
|
-
|
|
1322
|
-
def get_info(self) -> Dict:
|
|
1323
|
-
params = {}
|
|
1324
|
-
try:
|
|
1325
|
-
info = self.client.info()
|
|
1326
|
-
host = info.get("host", {})
|
|
1327
|
-
result = {
|
|
1328
|
-
"containers_total": info.get("store", {}).get("containers", 0),
|
|
1329
|
-
"containers_running": host.get("runningContainers", 0),
|
|
1330
|
-
"images": info.get("store", {}).get("images", 0),
|
|
1331
|
-
"driver": host.get("graphDriverName", "unknown"),
|
|
1332
|
-
"platform": f"{host.get('os', 'unknown')} {host.get('arch', 'unknown')}",
|
|
1333
|
-
"memory_total": self._format_size(host.get("memTotal", 0)),
|
|
1334
|
-
"swap_total": self._format_size(host.get("swapTotal", 0)),
|
|
1335
|
-
}
|
|
1336
|
-
self.log_action("get_info", params, result)
|
|
1337
|
-
return result
|
|
1338
|
-
except Exception as e:
|
|
1339
|
-
self.log_action("get_info", params, error=e)
|
|
1340
|
-
raise RuntimeError(f"Failed to get info: {str(e)}")
|
|
1341
|
-
|
|
1342
|
-
def remove_image(self, image: str, force: bool = False) -> Dict:
|
|
1343
|
-
params = {"image": image, "force": force}
|
|
1344
|
-
try:
|
|
1345
|
-
self.client.images.remove(image, force=force)
|
|
1346
|
-
result = {"removed": image}
|
|
1347
|
-
self.log_action("remove_image", params, result)
|
|
1348
|
-
return result
|
|
1349
|
-
except Exception as e:
|
|
1350
|
-
self.log_action("remove_image", params, error=e)
|
|
1351
|
-
raise RuntimeError(f"Failed to remove image: {str(e)}")
|
|
1352
|
-
|
|
1353
1484
|
def stop_container(self, container_id: str, timeout: int = 10) -> Dict:
|
|
1354
1485
|
params = {"container_id": container_id, "timeout": timeout}
|
|
1355
1486
|
try:
|
|
@@ -1455,40 +1586,50 @@ class PodmanManager(ContainerManagerBase):
|
|
|
1455
1586
|
self.log_action("remove_volume", params, error=e)
|
|
1456
1587
|
raise RuntimeError(f"Failed to remove volume: {str(e)}")
|
|
1457
1588
|
|
|
1458
|
-
def
|
|
1459
|
-
params = {
|
|
1589
|
+
def list_networks(self) -> List[Dict]:
|
|
1590
|
+
params = {}
|
|
1460
1591
|
try:
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
"
|
|
1474
|
-
"
|
|
1475
|
-
}
|
|
1476
|
-
else:
|
|
1477
|
-
result = self.client.volumes.prune()
|
|
1478
|
-
pruned = {
|
|
1479
|
-
"space_reclaimed": self._format_size(
|
|
1480
|
-
result.get("SpaceReclaimed", 0)
|
|
1481
|
-
),
|
|
1482
|
-
"volumes_removed": [
|
|
1483
|
-
v["Name"] for v in result.get("VolumesRemoved", [])
|
|
1484
|
-
],
|
|
1592
|
+
networks = self.client.networks.list()
|
|
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,
|
|
1485
1606
|
}
|
|
1486
|
-
result
|
|
1487
|
-
self.log_action("
|
|
1607
|
+
result.append(simplified)
|
|
1608
|
+
self.log_action("list_networks", params, result)
|
|
1488
1609
|
return result
|
|
1489
1610
|
except Exception as e:
|
|
1490
|
-
self.log_action("
|
|
1491
|
-
raise RuntimeError(f"Failed to
|
|
1611
|
+
self.log_action("list_networks", params, error=e)
|
|
1612
|
+
raise RuntimeError(f"Failed to list networks: {str(e)}")
|
|
1613
|
+
|
|
1614
|
+
def create_network(self, name: str, driver: str = "bridge") -> Dict:
|
|
1615
|
+
params = {"name": name, "driver": driver}
|
|
1616
|
+
try:
|
|
1617
|
+
network = self.client.networks.create(name, driver=driver)
|
|
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
|
+
}
|
|
1628
|
+
self.log_action("create_network", params, result)
|
|
1629
|
+
return result
|
|
1630
|
+
except Exception as e:
|
|
1631
|
+
self.log_action("create_network", params, error=e)
|
|
1632
|
+
raise RuntimeError(f"Failed to create network: {str(e)}")
|
|
1492
1633
|
|
|
1493
1634
|
def remove_network(self, network_id: str) -> Dict:
|
|
1494
1635
|
params = {"network_id": network_id}
|
|
@@ -1502,26 +1643,6 @@ class PodmanManager(ContainerManagerBase):
|
|
|
1502
1643
|
self.log_action("remove_network", params, error=e)
|
|
1503
1644
|
raise RuntimeError(f"Failed to remove network: {str(e)}")
|
|
1504
1645
|
|
|
1505
|
-
def prune_system(self, force: bool = False, all: bool = False) -> Dict:
|
|
1506
|
-
params = {"force": force, "all": all}
|
|
1507
|
-
try:
|
|
1508
|
-
# Podman system prune uses CLI, as podman-py may not have direct support
|
|
1509
|
-
cmd = ["podman", "system", "prune"]
|
|
1510
|
-
if all:
|
|
1511
|
-
cmd.append("--all")
|
|
1512
|
-
result = subprocess.run(cmd, capture_output=True, text=True)
|
|
1513
|
-
if result.returncode != 0:
|
|
1514
|
-
raise RuntimeError(result.stderr)
|
|
1515
|
-
pruned = {
|
|
1516
|
-
"output": result.stdout.strip(),
|
|
1517
|
-
"space_reclaimed": "Check output",
|
|
1518
|
-
}
|
|
1519
|
-
self.log_action("prune_system", params, pruned)
|
|
1520
|
-
return pruned
|
|
1521
|
-
except Exception as e:
|
|
1522
|
-
self.log_action("prune_system", params, error=e)
|
|
1523
|
-
raise RuntimeError(f"Failed to prune system: {str(e)}")
|
|
1524
|
-
|
|
1525
1646
|
def compose_up(
|
|
1526
1647
|
self, compose_file: str, detach: bool = True, build: bool = False
|
|
1527
1648
|
) -> str:
|
|
@@ -1608,27 +1729,20 @@ class PodmanManager(ContainerManagerBase):
|
|
|
1608
1729
|
raise NotImplementedError("Swarm not supported in Podman")
|
|
1609
1730
|
|
|
1610
1731
|
|
|
1732
|
+
def is_app_installed(app_name: str = "docker") -> bool:
|
|
1733
|
+
return shutil.which(app_name.lower()) is not None
|
|
1734
|
+
|
|
1735
|
+
|
|
1611
1736
|
def create_manager(
|
|
1612
1737
|
manager_type: Optional[str] = None, silent: bool = False, log_file: str = None
|
|
1613
1738
|
) -> ContainerManagerBase:
|
|
1614
1739
|
if manager_type is None:
|
|
1615
|
-
manager_type = os.environ.get("CONTAINER_MANAGER_TYPE")
|
|
1740
|
+
manager_type = os.environ.get("CONTAINER_MANAGER_TYPE", None)
|
|
1616
1741
|
if manager_type is None:
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
test_client.close()
|
|
1622
|
-
manager_type = "podman"
|
|
1623
|
-
except Exception:
|
|
1624
|
-
pass
|
|
1625
|
-
if manager_type is None and docker is not None:
|
|
1626
|
-
try:
|
|
1627
|
-
test_client = docker.from_env()
|
|
1628
|
-
test_client.close()
|
|
1629
|
-
manager_type = "docker"
|
|
1630
|
-
except Exception:
|
|
1631
|
-
pass
|
|
1742
|
+
if is_app_installed("podman"):
|
|
1743
|
+
manager_type = "podman"
|
|
1744
|
+
if is_app_installed("docker"):
|
|
1745
|
+
manager_type = "docker"
|
|
1632
1746
|
if manager_type is None:
|
|
1633
1747
|
raise ValueError(
|
|
1634
1748
|
"No supported container manager detected. Set CONTAINER_MANAGER_TYPE or install Docker/Podman."
|
|
@@ -1721,7 +1835,7 @@ container_manager.py --manager docker --pull-image nginx --tag latest --list-con
|
|
|
1721
1835
|
)
|
|
1722
1836
|
|
|
1723
1837
|
|
|
1724
|
-
def container_manager(
|
|
1838
|
+
def container_manager():
|
|
1725
1839
|
parser = argparse.ArgumentParser(
|
|
1726
1840
|
description="Container Manager: A tool to manage containers with Docker, Podman, and Docker Swarm!"
|
|
1727
1841
|
)
|
|
@@ -1826,7 +1940,7 @@ def container_manager(argv):
|
|
|
1826
1940
|
parser.add_argument("--force", action="store_true", help="Force removal")
|
|
1827
1941
|
parser.add_argument("-h", "--help", action="store_true", help="Show help")
|
|
1828
1942
|
|
|
1829
|
-
args = parser.parse_args(
|
|
1943
|
+
args = parser.parse_args()
|
|
1830
1944
|
|
|
1831
1945
|
if args.help:
|
|
1832
1946
|
usage()
|
|
@@ -1980,7 +2094,7 @@ def container_manager(argv):
|
|
|
1980
2094
|
)
|
|
1981
2095
|
|
|
1982
2096
|
if prune_containers:
|
|
1983
|
-
print(json.dumps(manager.prune_containers(
|
|
2097
|
+
print(json.dumps(manager.prune_containers(), indent=2))
|
|
1984
2098
|
|
|
1985
2099
|
if get_container_logs:
|
|
1986
2100
|
if not container_logs_id:
|
|
@@ -2028,7 +2142,7 @@ def container_manager(argv):
|
|
|
2028
2142
|
print(json.dumps(manager.remove_network(remove_network_id), indent=2))
|
|
2029
2143
|
|
|
2030
2144
|
if prune_networks:
|
|
2031
|
-
print(json.dumps(manager.prune_networks(
|
|
2145
|
+
print(json.dumps(manager.prune_networks(), indent=2))
|
|
2032
2146
|
|
|
2033
2147
|
if prune_system:
|
|
2034
2148
|
print(json.dumps(manager.prune_system(force, prune_system_all), indent=2))
|
|
@@ -2100,4 +2214,4 @@ if __name__ == "__main__":
|
|
|
2100
2214
|
if len(sys.argv) < 2:
|
|
2101
2215
|
usage()
|
|
2102
2216
|
sys.exit(2)
|
|
2103
|
-
container_manager(
|
|
2217
|
+
container_manager()
|