container-manager-mcp 1.0.2__py3-none-any.whl → 1.0.4__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/container_manager.py +592 -215
- container_manager_mcp/container_manager_mcp.py +223 -4
- {container_manager_mcp-1.0.2.dist-info → container_manager_mcp-1.0.4.dist-info}/METADATA +2 -2
- container_manager_mcp-1.0.4.dist-info/RECORD +10 -0
- container_manager_mcp-1.0.2.dist-info/RECORD +0 -10
- {container_manager_mcp-1.0.2.dist-info → container_manager_mcp-1.0.4.dist-info}/WHEEL +0 -0
- {container_manager_mcp-1.0.2.dist-info → container_manager_mcp-1.0.4.dist-info}/entry_points.txt +0 -0
- {container_manager_mcp-1.0.2.dist-info → container_manager_mcp-1.0.4.dist-info}/licenses/LICENSE +0 -0
- {container_manager_mcp-1.0.2.dist-info → container_manager_mcp-1.0.4.dist-info}/top_level.txt +0 -0
@@ -8,10 +8,12 @@ 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
14
|
import dateutil.parser
|
14
15
|
import platform
|
16
|
+
import traceback
|
15
17
|
|
16
18
|
try:
|
17
19
|
import docker
|
@@ -40,7 +42,7 @@ class ContainerManagerBase(ABC):
|
|
40
42
|
log_file = os.path.join(script_dir, "container_manager.log")
|
41
43
|
logging.basicConfig(
|
42
44
|
filename=log_file,
|
43
|
-
level=logging.
|
45
|
+
level=logging.DEBUG, # Changed to DEBUG for more detailed logging
|
44
46
|
format="%(asctime)s - %(levelname)s - %(message)s",
|
45
47
|
)
|
46
48
|
self.logger = logging.getLogger(__name__)
|
@@ -57,7 +59,10 @@ class ContainerManagerBase(ABC):
|
|
57
59
|
if result:
|
58
60
|
self.logger.info(f"Result: {result}")
|
59
61
|
if error:
|
60
|
-
self.logger.error(
|
62
|
+
self.logger.error(
|
63
|
+
f"Error in {action}: {type(error).__name__}: {str(error)}"
|
64
|
+
)
|
65
|
+
self.logger.error(f"Traceback: {traceback.format_exc()}")
|
61
66
|
|
62
67
|
def _format_size(self, size_bytes: int) -> str:
|
63
68
|
"""Helper to format bytes to human-readable (e.g., 1.23GB)."""
|
@@ -105,6 +110,10 @@ class ContainerManagerBase(ABC):
|
|
105
110
|
def remove_image(self, image: str, force: bool = False) -> Dict:
|
106
111
|
pass
|
107
112
|
|
113
|
+
@abstractmethod
|
114
|
+
def prune_images(self, force: bool = False, all: bool = False) -> Dict:
|
115
|
+
pass
|
116
|
+
|
108
117
|
@abstractmethod
|
109
118
|
def list_containers(self, all: bool = False) -> List[Dict]:
|
110
119
|
pass
|
@@ -130,6 +139,10 @@ class ContainerManagerBase(ABC):
|
|
130
139
|
def remove_container(self, container_id: str, force: bool = False) -> Dict:
|
131
140
|
pass
|
132
141
|
|
142
|
+
@abstractmethod
|
143
|
+
def prune_containers(self) -> Dict:
|
144
|
+
pass
|
145
|
+
|
133
146
|
@abstractmethod
|
134
147
|
def get_container_logs(self, container_id: str, tail: str = "all") -> str:
|
135
148
|
pass
|
@@ -152,6 +165,10 @@ class ContainerManagerBase(ABC):
|
|
152
165
|
def remove_volume(self, name: str, force: bool = False) -> Dict:
|
153
166
|
pass
|
154
167
|
|
168
|
+
@abstractmethod
|
169
|
+
def prune_volumes(self, force: bool = False, all: bool = False) -> Dict:
|
170
|
+
pass
|
171
|
+
|
155
172
|
@abstractmethod
|
156
173
|
def list_networks(self) -> List[Dict]:
|
157
174
|
pass
|
@@ -164,6 +181,14 @@ class ContainerManagerBase(ABC):
|
|
164
181
|
def remove_network(self, network_id: str) -> Dict:
|
165
182
|
pass
|
166
183
|
|
184
|
+
@abstractmethod
|
185
|
+
def prune_networks(self) -> Dict:
|
186
|
+
pass
|
187
|
+
|
188
|
+
@abstractmethod
|
189
|
+
def prune_system(self, force: bool = False, all: bool = False) -> Dict:
|
190
|
+
pass
|
191
|
+
|
167
192
|
@abstractmethod
|
168
193
|
def compose_up(
|
169
194
|
self, compose_file: str, detach: bool = True, build: bool = False
|
@@ -225,6 +250,78 @@ class DockerManager(ContainerManagerBase):
|
|
225
250
|
self.logger.error(f"Failed to connect to Docker daemon: {str(e)}")
|
226
251
|
raise RuntimeError(f"Failed to connect to Docker: {str(e)}")
|
227
252
|
|
253
|
+
def prune_system(self, force: bool = False, all: bool = False) -> Dict:
|
254
|
+
params = {"force": force, "all": all}
|
255
|
+
try:
|
256
|
+
filters = {"until": None} if all else {}
|
257
|
+
result = self.client.system.prune(filters=filters, volumes=all)
|
258
|
+
if result is None:
|
259
|
+
result = {
|
260
|
+
"SpaceReclaimed": 0,
|
261
|
+
"ImagesDeleted": [],
|
262
|
+
"ContainersDeleted": [],
|
263
|
+
"VolumesDeleted": [],
|
264
|
+
"NetworksDeleted": [],
|
265
|
+
}
|
266
|
+
self.logger.debug(f"Raw prune_system result: {result}")
|
267
|
+
pruned = {
|
268
|
+
"space_reclaimed": self._format_size(result.get("SpaceReclaimed", 0)),
|
269
|
+
"images_removed": (
|
270
|
+
[img["Id"][7:19] for img in result.get("ImagesDeleted", [])]
|
271
|
+
),
|
272
|
+
"containers_removed": (
|
273
|
+
[c["Id"][7:19] for c in result.get("ContainersDeleted", [])]
|
274
|
+
),
|
275
|
+
"volumes_removed": (
|
276
|
+
[v["Name"] for v in result.get("VolumesDeleted", [])]
|
277
|
+
),
|
278
|
+
"networks_removed": (
|
279
|
+
[n["Id"][7:19] for n in result.get("NetworksDeleted", [])]
|
280
|
+
),
|
281
|
+
}
|
282
|
+
self.log_action("prune_system", params, pruned)
|
283
|
+
return pruned
|
284
|
+
except Exception as e:
|
285
|
+
self.log_action("prune_system", params, error=e)
|
286
|
+
raise RuntimeError(f"Failed to prune system: {str(e)}")
|
287
|
+
|
288
|
+
# Other DockerManager methods remain unchanged (omitted for brevity)
|
289
|
+
def get_version(self) -> Dict:
|
290
|
+
params = {}
|
291
|
+
try:
|
292
|
+
version = self.client.version()
|
293
|
+
result = {
|
294
|
+
"version": version.get("Version", "unknown"),
|
295
|
+
"api_version": version.get("ApiVersion", "unknown"),
|
296
|
+
"os": version.get("Os", "unknown"),
|
297
|
+
"arch": version.get("Arch", "unknown"),
|
298
|
+
"build_time": version.get("BuildTime", "unknown"),
|
299
|
+
}
|
300
|
+
self.log_action("get_version", params, result)
|
301
|
+
return result
|
302
|
+
except Exception as e:
|
303
|
+
self.log_action("get_version", params, error=e)
|
304
|
+
raise RuntimeError(f"Failed to get version: {str(e)}")
|
305
|
+
|
306
|
+
def get_info(self) -> Dict:
|
307
|
+
params = {}
|
308
|
+
try:
|
309
|
+
info = self.client.info()
|
310
|
+
result = {
|
311
|
+
"containers_total": info.get("Containers", 0),
|
312
|
+
"containers_running": info.get("ContainersRunning", 0),
|
313
|
+
"images": info.get("Images", 0),
|
314
|
+
"driver": info.get("Driver", "unknown"),
|
315
|
+
"platform": f"{info.get('OperatingSystem', 'unknown')} {info.get('Architecture', 'unknown')}",
|
316
|
+
"memory_total": self._format_size(info.get("MemTotal", 0)),
|
317
|
+
"swap_total": self._format_size(info.get("SwapTotal", 0)),
|
318
|
+
}
|
319
|
+
self.log_action("get_info", params, result)
|
320
|
+
return result
|
321
|
+
except Exception as e:
|
322
|
+
self.log_action("get_info", params, error=e)
|
323
|
+
raise RuntimeError(f"Failed to get info: {str(e)}")
|
324
|
+
|
228
325
|
def list_images(self) -> List[Dict]:
|
229
326
|
params = {}
|
230
327
|
try:
|
@@ -294,6 +391,59 @@ class DockerManager(ContainerManagerBase):
|
|
294
391
|
self.log_action("pull_image", params, error=e)
|
295
392
|
raise RuntimeError(f"Failed to pull image: {str(e)}")
|
296
393
|
|
394
|
+
def remove_image(self, image: str, force: bool = False) -> Dict:
|
395
|
+
params = {"image": image, "force": force}
|
396
|
+
try:
|
397
|
+
self.client.images.remove(image, force=force)
|
398
|
+
result = {"removed": image}
|
399
|
+
self.log_action("remove_image", params, result)
|
400
|
+
return result
|
401
|
+
except Exception as e:
|
402
|
+
self.log_action("remove_image", params, error=e)
|
403
|
+
raise RuntimeError(f"Failed to remove image: {str(e)}")
|
404
|
+
|
405
|
+
def prune_images(self, force: bool = False, all: bool = False) -> Dict:
|
406
|
+
params = {"force": force, "all": all}
|
407
|
+
try:
|
408
|
+
if all:
|
409
|
+
# Manually remove all unused images
|
410
|
+
images = self.client.images.list(all=True)
|
411
|
+
removed = []
|
412
|
+
for img in images:
|
413
|
+
try:
|
414
|
+
for tag in img.attrs.get("RepoTags", []):
|
415
|
+
self.client.images.remove(tag, force=force)
|
416
|
+
removed.append(img.attrs["Id"][7:19])
|
417
|
+
except Exception as e:
|
418
|
+
self.logger.info(
|
419
|
+
f"Info: Failed to remove image {img.attrs.get('Id', 'unknown')}: {e}"
|
420
|
+
)
|
421
|
+
continue
|
422
|
+
result = {
|
423
|
+
"images_removed": removed,
|
424
|
+
"space_reclaimed": "N/A (all images)",
|
425
|
+
}
|
426
|
+
else:
|
427
|
+
filters = {"dangling": True} if not all else {}
|
428
|
+
result = self.client.images.prune(filters=filters)
|
429
|
+
if result is None:
|
430
|
+
result = {"SpaceReclaimed": 0, "ImagesDeleted": []}
|
431
|
+
self.logger.debug(f"Raw prune_images result: {result}")
|
432
|
+
pruned = {
|
433
|
+
"space_reclaimed": self._format_size(
|
434
|
+
result.get("SpaceReclaimed", 0)
|
435
|
+
),
|
436
|
+
"images_removed": (
|
437
|
+
[img["Id"][7:19] for img in result.get("ImagesDeleted", [])]
|
438
|
+
),
|
439
|
+
}
|
440
|
+
result = pruned
|
441
|
+
self.log_action("prune_images", params, result)
|
442
|
+
return result
|
443
|
+
except Exception as e:
|
444
|
+
self.log_action("prune_images", params, error=e)
|
445
|
+
raise RuntimeError(f"Failed to prune images: {str(e)}")
|
446
|
+
|
297
447
|
def list_containers(self, all: bool = False) -> List[Dict]:
|
298
448
|
params = {"all": all}
|
299
449
|
try:
|
@@ -384,98 +534,6 @@ class DockerManager(ContainerManagerBase):
|
|
384
534
|
self.log_action("run_container", params, error=e)
|
385
535
|
raise RuntimeError(f"Failed to run container: {str(e)}")
|
386
536
|
|
387
|
-
def list_networks(self) -> List[Dict]:
|
388
|
-
params = {}
|
389
|
-
try:
|
390
|
-
networks = self.client.networks.list()
|
391
|
-
result = []
|
392
|
-
for net in networks:
|
393
|
-
attrs = net.attrs
|
394
|
-
containers = len(attrs.get("Containers", {}))
|
395
|
-
created = attrs.get("Created", None)
|
396
|
-
created_str = self._parse_timestamp(created)
|
397
|
-
simplified = {
|
398
|
-
"id": attrs.get("Id", "unknown")[7:19],
|
399
|
-
"name": attrs.get("Name", "unknown"),
|
400
|
-
"driver": attrs.get("Driver", "unknown"),
|
401
|
-
"scope": attrs.get("Scope", "unknown"),
|
402
|
-
"containers": containers,
|
403
|
-
"created": created_str,
|
404
|
-
}
|
405
|
-
result.append(simplified)
|
406
|
-
self.log_action("list_networks", params, result)
|
407
|
-
return result
|
408
|
-
except Exception as e:
|
409
|
-
self.log_action("list_networks", params, error=e)
|
410
|
-
raise RuntimeError(f"Failed to list networks: {str(e)}")
|
411
|
-
|
412
|
-
def create_network(self, name: str, driver: str = "bridge") -> Dict:
|
413
|
-
params = {"name": name, "driver": driver}
|
414
|
-
try:
|
415
|
-
network = self.client.networks.create(name, driver=driver)
|
416
|
-
attrs = network.attrs
|
417
|
-
created = attrs.get("Created", None)
|
418
|
-
created_str = self._parse_timestamp(created)
|
419
|
-
result = {
|
420
|
-
"id": attrs.get("Id", "unknown")[7:19],
|
421
|
-
"name": attrs.get("Name", name),
|
422
|
-
"driver": attrs.get("Driver", driver),
|
423
|
-
"scope": attrs.get("Scope", "unknown"),
|
424
|
-
"created": created_str,
|
425
|
-
}
|
426
|
-
self.log_action("create_network", params, result)
|
427
|
-
return result
|
428
|
-
except Exception as e:
|
429
|
-
self.log_action("create_network", params, error=e)
|
430
|
-
raise RuntimeError(f"Failed to create network: {str(e)}")
|
431
|
-
|
432
|
-
def get_version(self) -> Dict:
|
433
|
-
params = {}
|
434
|
-
try:
|
435
|
-
version = self.client.version()
|
436
|
-
result = {
|
437
|
-
"version": version.get("Version", "unknown"),
|
438
|
-
"api_version": version.get("ApiVersion", "unknown"),
|
439
|
-
"os": version.get("Os", "unknown"),
|
440
|
-
"arch": version.get("Arch", "unknown"),
|
441
|
-
"build_time": version.get("BuildTime", "unknown"),
|
442
|
-
}
|
443
|
-
self.log_action("get_version", params, result)
|
444
|
-
return result
|
445
|
-
except Exception as e:
|
446
|
-
self.log_action("get_version", params, error=e)
|
447
|
-
raise RuntimeError(f"Failed to get version: {str(e)}")
|
448
|
-
|
449
|
-
def get_info(self) -> Dict:
|
450
|
-
params = {}
|
451
|
-
try:
|
452
|
-
info = self.client.info()
|
453
|
-
result = {
|
454
|
-
"containers_total": info.get("Containers", 0),
|
455
|
-
"containers_running": info.get("ContainersRunning", 0),
|
456
|
-
"images": info.get("Images", 0),
|
457
|
-
"driver": info.get("Driver", "unknown"),
|
458
|
-
"platform": f"{info.get('OperatingSystem', 'unknown')} {info.get('Architecture', 'unknown')}",
|
459
|
-
"memory_total": self._format_size(info.get("MemTotal", 0)),
|
460
|
-
"swap_total": self._format_size(info.get("SwapTotal", 0)),
|
461
|
-
}
|
462
|
-
self.log_action("get_info", params, result)
|
463
|
-
return result
|
464
|
-
except Exception as e:
|
465
|
-
self.log_action("get_info", params, error=e)
|
466
|
-
raise RuntimeError(f"Failed to get info: {str(e)}")
|
467
|
-
|
468
|
-
def remove_image(self, image: str, force: bool = False) -> Dict:
|
469
|
-
params = {"image": image, "force": force}
|
470
|
-
try:
|
471
|
-
self.client.images.remove(image, force=force)
|
472
|
-
result = {"removed": image}
|
473
|
-
self.log_action("remove_image", params, result)
|
474
|
-
return result
|
475
|
-
except Exception as e:
|
476
|
-
self.log_action("remove_image", params, error=e)
|
477
|
-
raise RuntimeError(f"Failed to remove image: {str(e)}")
|
478
|
-
|
479
537
|
def stop_container(self, container_id: str, timeout: int = 10) -> Dict:
|
480
538
|
params = {"container_id": container_id, "timeout": timeout}
|
481
539
|
try:
|
@@ -500,6 +558,34 @@ class DockerManager(ContainerManagerBase):
|
|
500
558
|
self.log_action("remove_container", params, error=e)
|
501
559
|
raise RuntimeError(f"Failed to remove container: {str(e)}")
|
502
560
|
|
561
|
+
def prune_containers(self) -> Dict:
|
562
|
+
params = {}
|
563
|
+
try:
|
564
|
+
result = self.client.containers.prune()
|
565
|
+
self.logger.debug(f"Raw prune_containers result: {result}")
|
566
|
+
if result is None:
|
567
|
+
result = {"SpaceReclaimed": 0, "ContainersDeleted": []}
|
568
|
+
pruned = {
|
569
|
+
"space_reclaimed": self._format_size(result.get("SpaceReclaimed", 0)),
|
570
|
+
"containers_removed": (
|
571
|
+
[c["Id"][7:19] for c in result.get("ContainersDeleted", [])]
|
572
|
+
),
|
573
|
+
}
|
574
|
+
self.log_action("prune_containers", params, pruned)
|
575
|
+
return pruned
|
576
|
+
except TypeError as e:
|
577
|
+
self.logger.error(f"TypeError in prune_containers: {str(e)}")
|
578
|
+
self.logger.error(f"Traceback: {traceback.format_exc()}")
|
579
|
+
self.log_action("prune_containers", params, error=e)
|
580
|
+
raise RuntimeError(f"Failed to prune containers: {str(e)}")
|
581
|
+
except Exception as e:
|
582
|
+
self.logger.error(
|
583
|
+
f"Unexpected exception in prune_containers: {type(e).__name__}: {str(e)}"
|
584
|
+
)
|
585
|
+
self.logger.error(f"Traceback: {traceback.format_exc()}")
|
586
|
+
self.log_action("prune_containers", params, error=e)
|
587
|
+
raise RuntimeError(f"Failed to prune containers: {str(e)}")
|
588
|
+
|
503
589
|
def get_container_logs(self, container_id: str, tail: str = "all") -> str:
|
504
590
|
params = {"container_id": container_id, "tail": tail}
|
505
591
|
try:
|
@@ -581,6 +667,90 @@ class DockerManager(ContainerManagerBase):
|
|
581
667
|
self.log_action("remove_volume", params, error=e)
|
582
668
|
raise RuntimeError(f"Failed to remove volume: {str(e)}")
|
583
669
|
|
670
|
+
def prune_volumes(self, force: bool = False, all: bool = False) -> Dict:
|
671
|
+
params = {"force": force, "all": all}
|
672
|
+
try:
|
673
|
+
if all:
|
674
|
+
volumes = self.client.volumes.list(all=True)
|
675
|
+
removed = []
|
676
|
+
for v in volumes:
|
677
|
+
try:
|
678
|
+
v.remove(force=force)
|
679
|
+
removed.append(v.attrs["Name"])
|
680
|
+
except Exception as e:
|
681
|
+
self.logger.info(
|
682
|
+
f"Info: Failed to remove volume {v.attrs.get('Name', 'unknown')}: {e}"
|
683
|
+
)
|
684
|
+
continue
|
685
|
+
result = {
|
686
|
+
"volumes_removed": removed,
|
687
|
+
"space_reclaimed": "N/A (all volumes)",
|
688
|
+
}
|
689
|
+
else:
|
690
|
+
result = self.client.volumes.prune()
|
691
|
+
if result is None:
|
692
|
+
result = {"SpaceReclaimed": 0, "VolumesDeleted": []}
|
693
|
+
self.logger.debug(f"Raw prune_volumes result: {result}")
|
694
|
+
pruned = {
|
695
|
+
"space_reclaimed": self._format_size(
|
696
|
+
result.get("SpaceReclaimed", 0)
|
697
|
+
),
|
698
|
+
"volumes_removed": (
|
699
|
+
[v["Name"] for v in result.get("VolumesDeleted", [])]
|
700
|
+
),
|
701
|
+
}
|
702
|
+
result = pruned
|
703
|
+
self.log_action("prune_volumes", params, result)
|
704
|
+
return result
|
705
|
+
except Exception as e:
|
706
|
+
self.log_action("prune_volumes", params, error=e)
|
707
|
+
raise RuntimeError(f"Failed to prune volumes: {str(e)}")
|
708
|
+
|
709
|
+
def list_networks(self) -> List[Dict]:
|
710
|
+
params = {}
|
711
|
+
try:
|
712
|
+
networks = self.client.networks.list()
|
713
|
+
result = []
|
714
|
+
for net in networks:
|
715
|
+
attrs = net.attrs
|
716
|
+
containers = len(attrs.get("Containers", {}))
|
717
|
+
created = attrs.get("Created", None)
|
718
|
+
created_str = self._parse_timestamp(created)
|
719
|
+
simplified = {
|
720
|
+
"id": attrs.get("Id", "unknown")[7:19],
|
721
|
+
"name": attrs.get("Name", "unknown"),
|
722
|
+
"driver": attrs.get("Driver", "unknown"),
|
723
|
+
"scope": attrs.get("Scope", "unknown"),
|
724
|
+
"containers": containers,
|
725
|
+
"created": created_str,
|
726
|
+
}
|
727
|
+
result.append(simplified)
|
728
|
+
self.log_action("list_networks", params, result)
|
729
|
+
return result
|
730
|
+
except Exception as e:
|
731
|
+
self.log_action("list_networks", params, error=e)
|
732
|
+
raise RuntimeError(f"Failed to list networks: {str(e)}")
|
733
|
+
|
734
|
+
def create_network(self, name: str, driver: str = "bridge") -> Dict:
|
735
|
+
params = {"name": name, "driver": driver}
|
736
|
+
try:
|
737
|
+
network = self.client.networks.create(name, driver=driver)
|
738
|
+
attrs = network.attrs
|
739
|
+
created = attrs.get("Created", None)
|
740
|
+
created_str = self._parse_timestamp(created)
|
741
|
+
result = {
|
742
|
+
"id": attrs.get("Id", "unknown")[7:19],
|
743
|
+
"name": attrs.get("Name", name),
|
744
|
+
"driver": attrs.get("Driver", driver),
|
745
|
+
"scope": attrs.get("Scope", "unknown"),
|
746
|
+
"created": created_str,
|
747
|
+
}
|
748
|
+
self.log_action("create_network", params, result)
|
749
|
+
return result
|
750
|
+
except Exception as e:
|
751
|
+
self.log_action("create_network", params, error=e)
|
752
|
+
raise RuntimeError(f"Failed to create network: {str(e)}")
|
753
|
+
|
584
754
|
def remove_network(self, network_id: str) -> Dict:
|
585
755
|
params = {"network_id": network_id}
|
586
756
|
try:
|
@@ -593,6 +763,25 @@ class DockerManager(ContainerManagerBase):
|
|
593
763
|
self.log_action("remove_network", params, error=e)
|
594
764
|
raise RuntimeError(f"Failed to remove network: {str(e)}")
|
595
765
|
|
766
|
+
def prune_networks(self) -> Dict:
|
767
|
+
params = {}
|
768
|
+
try:
|
769
|
+
result = self.client.networks.prune()
|
770
|
+
if result is None:
|
771
|
+
result = {"SpaceReclaimed": 0, "NetworksDeleted": []}
|
772
|
+
self.logger.debug(f"Raw prune_networks result: {result}")
|
773
|
+
pruned = {
|
774
|
+
"space_reclaimed": self._format_size(result.get("SpaceReclaimed", 0)),
|
775
|
+
"networks_removed": (
|
776
|
+
[n["Id"][7:19] for n in result.get("NetworksDeleted", [])]
|
777
|
+
),
|
778
|
+
}
|
779
|
+
self.log_action("prune_networks", params, pruned)
|
780
|
+
return pruned
|
781
|
+
except Exception as e:
|
782
|
+
self.log_action("prune_networks", params, error=e)
|
783
|
+
raise RuntimeError(f"Failed to prune networks: {str(e)}")
|
784
|
+
|
596
785
|
def compose_up(
|
597
786
|
self, compose_file: str, detach: bool = True, build: bool = False
|
598
787
|
) -> str:
|
@@ -818,17 +1007,14 @@ class DockerManager(ContainerManagerBase):
|
|
818
1007
|
class PodmanManager(ContainerManagerBase):
|
819
1008
|
def __init__(self, silent: bool = False, log_file: Optional[str] = None):
|
820
1009
|
super().__init__(silent, log_file)
|
821
|
-
|
822
1010
|
if PodmanClient is None:
|
823
1011
|
raise ImportError("Please install podman-py: pip install podman")
|
824
|
-
|
825
1012
|
base_url = self._autodetect_podman_url()
|
826
1013
|
if base_url is None:
|
827
1014
|
self.logger.error(
|
828
1015
|
"No valid Podman socket found after trying all known locations"
|
829
1016
|
)
|
830
1017
|
raise RuntimeError("Failed to connect to Podman: No valid socket found")
|
831
|
-
|
832
1018
|
try:
|
833
1019
|
self.client = PodmanClient(base_url=base_url)
|
834
1020
|
self.logger.info(f"Connected to Podman with base_url: {base_url}")
|
@@ -863,7 +1049,6 @@ class PodmanManager(ContainerManagerBase):
|
|
863
1049
|
"""Attempt to connect to Podman with the given base_url."""
|
864
1050
|
try:
|
865
1051
|
client = PodmanClient(base_url=base_url)
|
866
|
-
# Test connection
|
867
1052
|
client.version()
|
868
1053
|
return client
|
869
1054
|
except PodmanError as e:
|
@@ -872,41 +1057,31 @@ class PodmanManager(ContainerManagerBase):
|
|
872
1057
|
|
873
1058
|
def _autodetect_podman_url(self) -> Optional[str]:
|
874
1059
|
"""Autodetect the appropriate Podman socket URL based on platform."""
|
875
|
-
# Check for environment variable override
|
876
1060
|
base_url = os.environ.get("PODMAN_BASE_URL")
|
877
1061
|
if base_url:
|
878
1062
|
self.logger.info(f"Using PODMAN_BASE_URL from environment: {base_url}")
|
879
1063
|
return base_url
|
880
|
-
|
881
1064
|
system = platform.system()
|
882
1065
|
is_wsl = self._is_wsl()
|
883
|
-
|
884
|
-
# Define socket candidates based on platform
|
885
1066
|
socket_candidates = []
|
886
1067
|
if system == "Windows" and not is_wsl:
|
887
|
-
# Windows with Podman machine
|
888
1068
|
if self._is_podman_machine_running():
|
889
1069
|
socket_candidates.append("npipe:////./pipe/docker_engine")
|
890
|
-
# Fallback to WSL2 distro sockets if running in a mixed setup
|
891
1070
|
socket_candidates.extend(
|
892
1071
|
[
|
893
|
-
"unix:///mnt/wsl/podman-sockets/podman-machine-default/podman-user.sock",
|
894
|
-
"unix:///mnt/wsl/podman-sockets/podman-machine-default/podman-root.sock",
|
1072
|
+
"unix:///mnt/wsl/podman-sockets/podman-machine-default/podman-user.sock",
|
1073
|
+
"unix:///mnt/wsl/podman-sockets/podman-machine-default/podman-root.sock",
|
895
1074
|
]
|
896
1075
|
)
|
897
1076
|
elif system == "Linux" or is_wsl:
|
898
|
-
# Linux or WSL2 distro: prioritize rootless, then rootful
|
899
1077
|
uid = os.getuid()
|
900
1078
|
socket_candidates.extend(
|
901
1079
|
[
|
902
|
-
f"unix:///run/user/{uid}/podman/podman.sock",
|
903
|
-
"unix:///run/podman/podman.sock",
|
1080
|
+
f"unix:///run/user/{uid}/podman/podman.sock",
|
1081
|
+
"unix:///run/podman/podman.sock",
|
904
1082
|
]
|
905
1083
|
)
|
906
|
-
|
907
|
-
# Try each socket candidate
|
908
1084
|
for url in socket_candidates:
|
909
|
-
# For Unix sockets, check if the file exists (on Linux/WSL2)
|
910
1085
|
if url.startswith("unix://") and (system == "Linux" or is_wsl):
|
911
1086
|
socket_path = url.replace("unix://", "")
|
912
1087
|
if not os.path.exists(socket_path):
|
@@ -915,9 +1090,207 @@ class PodmanManager(ContainerManagerBase):
|
|
915
1090
|
client = self._try_connect(url)
|
916
1091
|
if client:
|
917
1092
|
return url
|
918
|
-
|
919
1093
|
return None
|
920
1094
|
|
1095
|
+
def prune_images(self, force: bool = False, all: bool = False) -> Dict:
|
1096
|
+
params = {"force": force, "all": all}
|
1097
|
+
try:
|
1098
|
+
if all:
|
1099
|
+
# Manually remove all unused images
|
1100
|
+
images = self.client.images.list(all=True)
|
1101
|
+
removed = []
|
1102
|
+
for img in images:
|
1103
|
+
try:
|
1104
|
+
for tag in img.attrs.get("Names", []):
|
1105
|
+
self.client.images.remove(tag, force=force)
|
1106
|
+
removed.append(img.attrs["Id"][7:19])
|
1107
|
+
except Exception as e:
|
1108
|
+
self.logger.info(
|
1109
|
+
f"Info: Failed to remove image {img.attrs.get('Id', 'unknown')}: {e}"
|
1110
|
+
)
|
1111
|
+
continue
|
1112
|
+
result = {
|
1113
|
+
"images_removed": removed,
|
1114
|
+
"space_reclaimed": "N/A (all images)",
|
1115
|
+
}
|
1116
|
+
else:
|
1117
|
+
filters = {"dangling": True} if not all else {}
|
1118
|
+
result = self.client.images.prune(filters=filters)
|
1119
|
+
if result is None:
|
1120
|
+
result = {"SpaceReclaimed": 0, "ImagesRemoved": []}
|
1121
|
+
self.logger.debug(f"Raw prune_images result: {result}")
|
1122
|
+
pruned = {
|
1123
|
+
"space_reclaimed": self._format_size(
|
1124
|
+
result.get("SpaceReclaimed", 0)
|
1125
|
+
),
|
1126
|
+
"images_removed": (
|
1127
|
+
[img["Id"][7:19] for img in result.get("ImagesRemoved", [])]
|
1128
|
+
or [img["Id"][7:19] for img in result.get("ImagesDeleted", [])]
|
1129
|
+
),
|
1130
|
+
}
|
1131
|
+
result = pruned
|
1132
|
+
self.log_action("prune_images", params, result)
|
1133
|
+
return result
|
1134
|
+
except Exception as e:
|
1135
|
+
self.log_action("prune_images", params, error=e)
|
1136
|
+
raise RuntimeError(f"Failed to prune images: {str(e)}")
|
1137
|
+
|
1138
|
+
def prune_containers(self) -> Dict:
|
1139
|
+
params = {}
|
1140
|
+
try:
|
1141
|
+
result = self.client.containers.prune()
|
1142
|
+
self.logger.debug(f"Raw prune_containers result: {result}")
|
1143
|
+
if result is None:
|
1144
|
+
result = {"SpaceReclaimed": 0, "ContainersDeleted": []}
|
1145
|
+
pruned = {
|
1146
|
+
"space_reclaimed": self._format_size(result.get("SpaceReclaimed", 0)),
|
1147
|
+
"containers_removed": (
|
1148
|
+
[c["Id"][7:19] for c in result.get("ContainersDeleted", [])]
|
1149
|
+
or [c["Id"][7:19] for c in result.get("ContainersRemoved", [])]
|
1150
|
+
),
|
1151
|
+
}
|
1152
|
+
self.log_action("prune_containers", params, pruned)
|
1153
|
+
return pruned
|
1154
|
+
except PodmanError as e:
|
1155
|
+
self.logger.error(f"PodmanError in prune_containers: {str(e)}")
|
1156
|
+
self.logger.error(f"Traceback: {traceback.format_exc()}")
|
1157
|
+
self.log_action("prune_containers", params, error=e)
|
1158
|
+
raise RuntimeError(f"Failed to prune containers: {str(e)}")
|
1159
|
+
except Exception as e:
|
1160
|
+
self.logger.error(
|
1161
|
+
f"Unexpected exception in prune_containers: {type(e).__name__}: {str(e)}"
|
1162
|
+
)
|
1163
|
+
self.logger.error(f"Traceback: {traceback.format_exc()}")
|
1164
|
+
self.log_action("prune_containers", params, error=e)
|
1165
|
+
raise RuntimeError(f"Failed to prune containers: {str(e)}")
|
1166
|
+
|
1167
|
+
def prune_volumes(self, force: bool = False, all: bool = False) -> Dict:
|
1168
|
+
params = {"force": force, "all": all}
|
1169
|
+
try:
|
1170
|
+
if all:
|
1171
|
+
volumes = self.client.volumes.list(all=True)
|
1172
|
+
removed = []
|
1173
|
+
for v in volumes:
|
1174
|
+
try:
|
1175
|
+
v.remove(force=force)
|
1176
|
+
removed.append(v.attrs["Name"])
|
1177
|
+
except Exception as e:
|
1178
|
+
self.logger.info(
|
1179
|
+
f"Info: Failed to remove volume {v.attrs.get('Name', 'unknown')}: {e}"
|
1180
|
+
)
|
1181
|
+
continue
|
1182
|
+
result = {
|
1183
|
+
"volumes_removed": removed,
|
1184
|
+
"space_reclaimed": "N/A (all volumes)",
|
1185
|
+
}
|
1186
|
+
else:
|
1187
|
+
result = self.client.volumes.prune()
|
1188
|
+
if result is None:
|
1189
|
+
result = {"SpaceReclaimed": 0, "VolumesRemoved": []}
|
1190
|
+
self.logger.debug(f"Raw prune_volumes result: {result}")
|
1191
|
+
pruned = {
|
1192
|
+
"space_reclaimed": self._format_size(
|
1193
|
+
result.get("SpaceReclaimed", 0)
|
1194
|
+
),
|
1195
|
+
"volumes_removed": (
|
1196
|
+
[v["Name"] for v in result.get("VolumesRemoved", [])]
|
1197
|
+
or [v["Name"] for v in result.get("VolumesDeleted", [])]
|
1198
|
+
),
|
1199
|
+
}
|
1200
|
+
result = pruned
|
1201
|
+
self.log_action("prune_volumes", params, result)
|
1202
|
+
return result
|
1203
|
+
except Exception as e:
|
1204
|
+
self.log_action("prune_volumes", params, error=e)
|
1205
|
+
raise RuntimeError(f"Failed to prune volumes: {str(e)}")
|
1206
|
+
|
1207
|
+
def prune_networks(self) -> Dict:
|
1208
|
+
params = {}
|
1209
|
+
try:
|
1210
|
+
result = self.client.networks.prune()
|
1211
|
+
if result is None:
|
1212
|
+
result = {"SpaceReclaimed": 0, "NetworksRemoved": []}
|
1213
|
+
self.logger.debug(f"Raw prune_networks result: {result}")
|
1214
|
+
pruned = {
|
1215
|
+
"space_reclaimed": self._format_size(result.get("SpaceReclaimed", 0)),
|
1216
|
+
"networks_removed": (
|
1217
|
+
[n["Id"][7:19] for n in result.get("NetworksRemoved", [])]
|
1218
|
+
or [n["Id"][7:19] for n in result.get("NetworksDeleted", [])]
|
1219
|
+
),
|
1220
|
+
}
|
1221
|
+
self.log_action("prune_networks", params, pruned)
|
1222
|
+
return pruned
|
1223
|
+
except Exception as e:
|
1224
|
+
self.log_action("prune_networks", params, error=e)
|
1225
|
+
raise RuntimeError(f"Failed to prune networks: {str(e)}")
|
1226
|
+
|
1227
|
+
def prune_system(self, force: bool = False, all: bool = False) -> Dict:
|
1228
|
+
params = {"force": force, "all": all}
|
1229
|
+
try:
|
1230
|
+
cmd = (
|
1231
|
+
["podman", "system", "prune", "--force"]
|
1232
|
+
if force
|
1233
|
+
else ["podman", "system", "prune"]
|
1234
|
+
)
|
1235
|
+
if all:
|
1236
|
+
cmd.append("--all")
|
1237
|
+
if all: # Include volumes if all=True
|
1238
|
+
cmd.append("--volumes")
|
1239
|
+
result = subprocess.run(cmd, capture_output=True, text=True)
|
1240
|
+
if result.returncode != 0:
|
1241
|
+
raise RuntimeError(result.stderr)
|
1242
|
+
self.logger.debug(f"Raw prune_system result: {result.stdout}")
|
1243
|
+
pruned = {
|
1244
|
+
"output": result.stdout.strip(),
|
1245
|
+
"space_reclaimed": "Check output",
|
1246
|
+
"images_removed": [], # Podman CLI doesn't provide detailed breakdown
|
1247
|
+
"containers_removed": [],
|
1248
|
+
"volumes_removed": [],
|
1249
|
+
"networks_removed": [],
|
1250
|
+
}
|
1251
|
+
self.log_action("prune_system", params, pruned)
|
1252
|
+
return pruned
|
1253
|
+
except Exception as e:
|
1254
|
+
self.log_action("prune_system", params, error=e)
|
1255
|
+
raise RuntimeError(f"Failed to prune system: {str(e)}")
|
1256
|
+
|
1257
|
+
def get_version(self) -> Dict:
|
1258
|
+
params = {}
|
1259
|
+
try:
|
1260
|
+
version = self.client.version()
|
1261
|
+
result = {
|
1262
|
+
"version": version.get("Version", "unknown"),
|
1263
|
+
"api_version": version.get("APIVersion", "unknown"),
|
1264
|
+
"os": version.get("Os", "unknown"),
|
1265
|
+
"arch": version.get("Arch", "unknown"),
|
1266
|
+
"build_time": version.get("BuildTime", "unknown"),
|
1267
|
+
}
|
1268
|
+
self.log_action("get_version", params, result)
|
1269
|
+
return result
|
1270
|
+
except Exception as e:
|
1271
|
+
self.log_action("get_version", params, error=e)
|
1272
|
+
raise RuntimeError(f"Failed to get version: {str(e)}")
|
1273
|
+
|
1274
|
+
def get_info(self) -> Dict:
|
1275
|
+
params = {}
|
1276
|
+
try:
|
1277
|
+
info = self.client.info()
|
1278
|
+
host = info.get("host", {})
|
1279
|
+
result = {
|
1280
|
+
"containers_total": info.get("store", {}).get("containers", 0),
|
1281
|
+
"containers_running": host.get("runningContainers", 0),
|
1282
|
+
"images": info.get("store", {}).get("images", 0),
|
1283
|
+
"driver": host.get("graphDriverName", "unknown"),
|
1284
|
+
"platform": f"{host.get('os', 'unknown')} {host.get('arch', 'unknown')}",
|
1285
|
+
"memory_total": self._format_size(host.get("memTotal", 0)),
|
1286
|
+
"swap_total": self._format_size(host.get("swapTotal", 0)),
|
1287
|
+
}
|
1288
|
+
self.log_action("get_info", params, result)
|
1289
|
+
return result
|
1290
|
+
except Exception as e:
|
1291
|
+
self.log_action("get_info", params, error=e)
|
1292
|
+
raise RuntimeError(f"Failed to get info: {str(e)}")
|
1293
|
+
|
921
1294
|
def list_images(self) -> List[Dict]:
|
922
1295
|
params = {}
|
923
1296
|
try:
|
@@ -983,6 +1356,17 @@ class PodmanManager(ContainerManagerBase):
|
|
983
1356
|
self.log_action("pull_image", params, error=e)
|
984
1357
|
raise RuntimeError(f"Failed to pull image: {str(e)}")
|
985
1358
|
|
1359
|
+
def remove_image(self, image: str, force: bool = False) -> Dict:
|
1360
|
+
params = {"image": image, "force": force}
|
1361
|
+
try:
|
1362
|
+
self.client.images.remove(image, force=force)
|
1363
|
+
result = {"removed": image}
|
1364
|
+
self.log_action("remove_image", params, result)
|
1365
|
+
return result
|
1366
|
+
except Exception as e:
|
1367
|
+
self.log_action("remove_image", params, error=e)
|
1368
|
+
raise RuntimeError(f"Failed to remove image: {str(e)}")
|
1369
|
+
|
986
1370
|
def list_containers(self, all: bool = False) -> List[Dict]:
|
987
1371
|
params = {"all": all}
|
988
1372
|
try:
|
@@ -1069,99 +1453,6 @@ class PodmanManager(ContainerManagerBase):
|
|
1069
1453
|
self.log_action("run_container", params, error=e)
|
1070
1454
|
raise RuntimeError(f"Failed to run container: {str(e)}")
|
1071
1455
|
|
1072
|
-
def list_networks(self) -> List[Dict]:
|
1073
|
-
params = {}
|
1074
|
-
try:
|
1075
|
-
networks = self.client.networks.list()
|
1076
|
-
result = []
|
1077
|
-
for net in networks:
|
1078
|
-
attrs = net.attrs
|
1079
|
-
containers = len(attrs.get("Containers", {}))
|
1080
|
-
created = attrs.get("Created", None)
|
1081
|
-
created_str = self._parse_timestamp(created)
|
1082
|
-
simplified = {
|
1083
|
-
"id": attrs.get("Id", "unknown")[7:19],
|
1084
|
-
"name": attrs.get("Name", "unknown"),
|
1085
|
-
"driver": attrs.get("Driver", "unknown"),
|
1086
|
-
"scope": attrs.get("Scope", "unknown"),
|
1087
|
-
"containers": containers,
|
1088
|
-
"created": created_str,
|
1089
|
-
}
|
1090
|
-
result.append(simplified)
|
1091
|
-
self.log_action("list_networks", params, result)
|
1092
|
-
return result
|
1093
|
-
except Exception as e:
|
1094
|
-
self.log_action("list_networks", params, error=e)
|
1095
|
-
raise RuntimeError(f"Failed to list networks: {str(e)}")
|
1096
|
-
|
1097
|
-
def create_network(self, name: str, driver: str = "bridge") -> Dict:
|
1098
|
-
params = {"name": name, "driver": driver}
|
1099
|
-
try:
|
1100
|
-
network = self.client.networks.create(name, driver=driver)
|
1101
|
-
attrs = network.attrs
|
1102
|
-
created = attrs.get("Created", None)
|
1103
|
-
created_str = self._parse_timestamp(created)
|
1104
|
-
result = {
|
1105
|
-
"id": attrs.get("Id", "unknown")[7:19],
|
1106
|
-
"name": attrs.get("Name", name),
|
1107
|
-
"driver": attrs.get("Driver", driver),
|
1108
|
-
"scope": attrs.get("Scope", "unknown"),
|
1109
|
-
"created": created_str,
|
1110
|
-
}
|
1111
|
-
self.log_action("create_network", params, result)
|
1112
|
-
return result
|
1113
|
-
except Exception as e:
|
1114
|
-
self.log_action("create_network", params, error=e)
|
1115
|
-
raise RuntimeError(f"Failed to create network: {str(e)}")
|
1116
|
-
|
1117
|
-
def get_version(self) -> Dict:
|
1118
|
-
params = {}
|
1119
|
-
try:
|
1120
|
-
version = self.client.version()
|
1121
|
-
result = {
|
1122
|
-
"version": version.get("Version", "unknown"),
|
1123
|
-
"api_version": version.get("APIVersion", "unknown"),
|
1124
|
-
"os": version.get("Os", "unknown"),
|
1125
|
-
"arch": version.get("Arch", "unknown"),
|
1126
|
-
"build_time": version.get("BuildTime", "unknown"),
|
1127
|
-
}
|
1128
|
-
self.log_action("get_version", params, result)
|
1129
|
-
return result
|
1130
|
-
except Exception as e:
|
1131
|
-
self.log_action("get_version", params, error=e)
|
1132
|
-
raise RuntimeError(f"Failed to get version: {str(e)}")
|
1133
|
-
|
1134
|
-
def get_info(self) -> Dict:
|
1135
|
-
params = {}
|
1136
|
-
try:
|
1137
|
-
info = self.client.info()
|
1138
|
-
host = info.get("host", {})
|
1139
|
-
result = {
|
1140
|
-
"containers_total": info.get("store", {}).get("containers", 0),
|
1141
|
-
"containers_running": host.get("runningContainers", 0),
|
1142
|
-
"images": info.get("store", {}).get("images", 0),
|
1143
|
-
"driver": host.get("graphDriverName", "unknown"),
|
1144
|
-
"platform": f"{host.get('os', 'unknown')} {host.get('arch', 'unknown')}",
|
1145
|
-
"memory_total": self._format_size(host.get("memTotal", 0)),
|
1146
|
-
"swap_total": self._format_size(host.get("swapTotal", 0)),
|
1147
|
-
}
|
1148
|
-
self.log_action("get_info", params, result)
|
1149
|
-
return result
|
1150
|
-
except Exception as e:
|
1151
|
-
self.log_action("get_info", params, error=e)
|
1152
|
-
raise RuntimeError(f"Failed to get info: {str(e)}")
|
1153
|
-
|
1154
|
-
def remove_image(self, image: str, force: bool = False) -> Dict:
|
1155
|
-
params = {"image": image, "force": force}
|
1156
|
-
try:
|
1157
|
-
self.client.images.remove(image, force=force)
|
1158
|
-
result = {"removed": image}
|
1159
|
-
self.log_action("remove_image", params, result)
|
1160
|
-
return result
|
1161
|
-
except Exception as e:
|
1162
|
-
self.log_action("remove_image", params, error=e)
|
1163
|
-
raise RuntimeError(f"Failed to remove image: {str(e)}")
|
1164
|
-
|
1165
1456
|
def stop_container(self, container_id: str, timeout: int = 10) -> Dict:
|
1166
1457
|
params = {"container_id": container_id, "timeout": timeout}
|
1167
1458
|
try:
|
@@ -1267,6 +1558,51 @@ class PodmanManager(ContainerManagerBase):
|
|
1267
1558
|
self.log_action("remove_volume", params, error=e)
|
1268
1559
|
raise RuntimeError(f"Failed to remove volume: {str(e)}")
|
1269
1560
|
|
1561
|
+
def list_networks(self) -> List[Dict]:
|
1562
|
+
params = {}
|
1563
|
+
try:
|
1564
|
+
networks = self.client.networks.list()
|
1565
|
+
result = []
|
1566
|
+
for net in networks:
|
1567
|
+
attrs = net.attrs
|
1568
|
+
containers = len(attrs.get("Containers", {}))
|
1569
|
+
created = attrs.get("Created", None)
|
1570
|
+
created_str = self._parse_timestamp(created)
|
1571
|
+
simplified = {
|
1572
|
+
"id": attrs.get("Id", "unknown")[7:19],
|
1573
|
+
"name": attrs.get("Name", "unknown"),
|
1574
|
+
"driver": attrs.get("Driver", "unknown"),
|
1575
|
+
"scope": attrs.get("Scope", "unknown"),
|
1576
|
+
"containers": containers,
|
1577
|
+
"created": created_str,
|
1578
|
+
}
|
1579
|
+
result.append(simplified)
|
1580
|
+
self.log_action("list_networks", params, result)
|
1581
|
+
return result
|
1582
|
+
except Exception as e:
|
1583
|
+
self.log_action("list_networks", params, error=e)
|
1584
|
+
raise RuntimeError(f"Failed to list networks: {str(e)}")
|
1585
|
+
|
1586
|
+
def create_network(self, name: str, driver: str = "bridge") -> Dict:
|
1587
|
+
params = {"name": name, "driver": driver}
|
1588
|
+
try:
|
1589
|
+
network = self.client.networks.create(name, driver=driver)
|
1590
|
+
attrs = network.attrs
|
1591
|
+
created = attrs.get("Created", None)
|
1592
|
+
created_str = self._parse_timestamp(created)
|
1593
|
+
result = {
|
1594
|
+
"id": attrs.get("Id", "unknown")[7:19],
|
1595
|
+
"name": attrs.get("Name", name),
|
1596
|
+
"driver": attrs.get("Driver", driver),
|
1597
|
+
"scope": attrs.get("Scope", "unknown"),
|
1598
|
+
"created": created_str,
|
1599
|
+
}
|
1600
|
+
self.log_action("create_network", params, result)
|
1601
|
+
return result
|
1602
|
+
except Exception as e:
|
1603
|
+
self.log_action("create_network", params, error=e)
|
1604
|
+
raise RuntimeError(f"Failed to create network: {str(e)}")
|
1605
|
+
|
1270
1606
|
def remove_network(self, network_id: str) -> Dict:
|
1271
1607
|
params = {"network_id": network_id}
|
1272
1608
|
try:
|
@@ -1365,21 +1701,24 @@ class PodmanManager(ContainerManagerBase):
|
|
1365
1701
|
raise NotImplementedError("Swarm not supported in Podman")
|
1366
1702
|
|
1367
1703
|
|
1704
|
+
def is_app_installed(app_name: str = "docker") -> bool:
|
1705
|
+
return shutil.which(app_name.lower()) is not None
|
1706
|
+
|
1707
|
+
|
1368
1708
|
def create_manager(
|
1369
1709
|
manager_type: Optional[str] = None, silent: bool = False, log_file: str = None
|
1370
1710
|
) -> ContainerManagerBase:
|
1371
1711
|
if manager_type is None:
|
1372
|
-
manager_type = os.environ.get("CONTAINER_MANAGER_TYPE")
|
1712
|
+
manager_type = os.environ.get("CONTAINER_MANAGER_TYPE", None)
|
1373
1713
|
if manager_type is None:
|
1374
|
-
|
1375
|
-
if PodmanClient is not None:
|
1714
|
+
if is_app_installed("podman"):
|
1376
1715
|
try:
|
1377
1716
|
test_client = PodmanClient()
|
1378
1717
|
test_client.close()
|
1379
1718
|
manager_type = "podman"
|
1380
1719
|
except Exception:
|
1381
1720
|
pass
|
1382
|
-
if
|
1721
|
+
if is_app_installed("docker"):
|
1383
1722
|
try:
|
1384
1723
|
test_client = docker.from_env()
|
1385
1724
|
test_client.close()
|
@@ -1418,6 +1757,8 @@ Actions:
|
|
1418
1757
|
--platform <plat> [ Platform, e.g., linux/amd64 ]
|
1419
1758
|
--remove-image <image> [ Remove image ]
|
1420
1759
|
--force [ Force removal (global for remove actions) ]
|
1760
|
+
--prune-images [ Prune unused images ]
|
1761
|
+
--all [ Prune all unused images ]
|
1421
1762
|
--list-containers [ List containers ]
|
1422
1763
|
--all [ Show all containers ]
|
1423
1764
|
--run-container <image> [ Run container ]
|
@@ -1431,6 +1772,7 @@ Actions:
|
|
1431
1772
|
--timeout <sec> [ Timeout, default 10 ]
|
1432
1773
|
--remove-container <id>[ Remove container ]
|
1433
1774
|
--force [ Force ]
|
1775
|
+
--prune-containers [ Prune stopped containers ]
|
1434
1776
|
--get-container-logs <id> [ Get logs ]
|
1435
1777
|
--tail <tail> [ Tail lines, default all ]
|
1436
1778
|
--exec-in-container <id> [ Exec command ]
|
@@ -1440,10 +1782,15 @@ Actions:
|
|
1440
1782
|
--create-volume <name> [ Create volume ]
|
1441
1783
|
--remove-volume <name> [ Remove volume ]
|
1442
1784
|
--force [ Force ]
|
1785
|
+
--prune-volumes [ Prune unused volumes ]
|
1786
|
+
--all [ Remove all volumes (dangerous) ]
|
1443
1787
|
--list-networks [ List networks ]
|
1444
1788
|
--create-network <name>[ Create network ]
|
1445
1789
|
--driver <driver> [ Driver, default bridge ]
|
1446
1790
|
--remove-network <id> [ Remove network ]
|
1791
|
+
--prune-networks [ Prune unused networks ]
|
1792
|
+
--prune-system [ Prune system resources ]
|
1793
|
+
--all [ Prune all unused (including volumes, build cache) ]
|
1447
1794
|
--compose-up <file> [ Compose up ]
|
1448
1795
|
--build [ Build images ]
|
1449
1796
|
--detach [ Detach mode, default true ]
|
@@ -1470,7 +1817,7 @@ container_manager.py --manager docker --pull-image nginx --tag latest --list-con
|
|
1470
1817
|
)
|
1471
1818
|
|
1472
1819
|
|
1473
|
-
def container_manager(
|
1820
|
+
def container_manager():
|
1474
1821
|
parser = argparse.ArgumentParser(
|
1475
1822
|
description="Container Manager: A tool to manage containers with Docker, Podman, and Docker Swarm!"
|
1476
1823
|
)
|
@@ -1492,7 +1839,7 @@ def container_manager(argv):
|
|
1492
1839
|
parser.add_argument(
|
1493
1840
|
"--remove-image", type=str, default=None, help="Image to remove"
|
1494
1841
|
)
|
1495
|
-
parser.add_argument("--
|
1842
|
+
parser.add_argument("--prune-images", action="store_true", help="Prune images")
|
1496
1843
|
parser.add_argument(
|
1497
1844
|
"--list-containers", action="store_true", help="List containers"
|
1498
1845
|
)
|
@@ -1513,6 +1860,9 @@ def container_manager(argv):
|
|
1513
1860
|
parser.add_argument(
|
1514
1861
|
"--remove-container", type=str, default=None, help="Container to remove"
|
1515
1862
|
)
|
1863
|
+
parser.add_argument(
|
1864
|
+
"--prune-containers", action="store_true", help="Prune containers"
|
1865
|
+
)
|
1516
1866
|
parser.add_argument(
|
1517
1867
|
"--get-container-logs", type=str, default=None, help="Container logs"
|
1518
1868
|
)
|
@@ -1529,6 +1879,7 @@ def container_manager(argv):
|
|
1529
1879
|
parser.add_argument(
|
1530
1880
|
"--remove-volume", type=str, default=None, help="Volume to remove"
|
1531
1881
|
)
|
1882
|
+
parser.add_argument("--prune-volumes", action="store_true", help="Prune volumes")
|
1532
1883
|
parser.add_argument("--list-networks", action="store_true", help="List networks")
|
1533
1884
|
parser.add_argument(
|
1534
1885
|
"--create-network", type=str, default=None, help="Network to create"
|
@@ -1537,6 +1888,8 @@ def container_manager(argv):
|
|
1537
1888
|
parser.add_argument(
|
1538
1889
|
"--remove-network", type=str, default=None, help="Network to remove"
|
1539
1890
|
)
|
1891
|
+
parser.add_argument("--prune-networks", action="store_true", help="Prune networks")
|
1892
|
+
parser.add_argument("--prune-system", action="store_true", help="Prune system")
|
1540
1893
|
parser.add_argument("--compose-up", type=str, default=None, help="Compose file up")
|
1541
1894
|
parser.add_argument("--build", action="store_true", help="Build images")
|
1542
1895
|
parser.add_argument(
|
@@ -1566,9 +1919,10 @@ def container_manager(argv):
|
|
1566
1919
|
parser.add_argument(
|
1567
1920
|
"--remove-service", type=str, default=None, help="Service to remove"
|
1568
1921
|
)
|
1922
|
+
parser.add_argument("--force", action="store_true", help="Force removal")
|
1569
1923
|
parser.add_argument("-h", "--help", action="store_true", help="Show help")
|
1570
1924
|
|
1571
|
-
args = parser.parse_args(
|
1925
|
+
args = parser.parse_args()
|
1572
1926
|
|
1573
1927
|
if args.help:
|
1574
1928
|
usage()
|
@@ -1583,9 +1937,11 @@ def container_manager(argv):
|
|
1583
1937
|
platform = args.platform
|
1584
1938
|
remove_image = args.remove_image is not None
|
1585
1939
|
remove_image_str = args.remove_image
|
1940
|
+
prune_images = args.prune_images
|
1941
|
+
prune_images_all = args.all if prune_images else False
|
1586
1942
|
force = args.force
|
1587
1943
|
list_containers = args.list_containers
|
1588
|
-
all_containers = args.all
|
1944
|
+
all_containers = args.all if list_containers else False
|
1589
1945
|
run_container = args.run_container is not None
|
1590
1946
|
run_image = args.run_container
|
1591
1947
|
name = args.name
|
@@ -1599,6 +1955,7 @@ def container_manager(argv):
|
|
1599
1955
|
timeout = args.timeout
|
1600
1956
|
remove_container = args.remove_container is not None
|
1601
1957
|
remove_container_id = args.remove_container
|
1958
|
+
prune_containers = args.prune_containers
|
1602
1959
|
get_container_logs = args.get_container_logs is not None
|
1603
1960
|
container_logs_id = args.get_container_logs
|
1604
1961
|
tail = args.tail
|
@@ -1611,12 +1968,17 @@ def container_manager(argv):
|
|
1611
1968
|
create_volume_name = args.create_volume
|
1612
1969
|
remove_volume = args.remove_volume is not None
|
1613
1970
|
remove_volume_name = args.remove_volume
|
1971
|
+
prune_volumes = args.prune_volumes
|
1972
|
+
prune_volumes_all = args.all if prune_volumes else False
|
1614
1973
|
list_networks = args.list_networks
|
1615
1974
|
create_network = args.create_network is not None
|
1616
1975
|
create_network_name = args.create_network
|
1617
1976
|
driver = args.driver
|
1618
1977
|
remove_network = args.remove_network is not None
|
1619
1978
|
remove_network_id = args.remove_network
|
1979
|
+
prune_networks = args.prune_networks
|
1980
|
+
prune_system = args.prune_system
|
1981
|
+
prune_system_all = args.all if prune_system else False
|
1620
1982
|
compose_up = args.compose_up is not None
|
1621
1983
|
compose_up_file = args.compose_up
|
1622
1984
|
compose_build = args.build
|
@@ -1665,6 +2027,9 @@ def container_manager(argv):
|
|
1665
2027
|
raise ValueError("Image required for remove-image")
|
1666
2028
|
print(json.dumps(manager.remove_image(remove_image_str, force), indent=2))
|
1667
2029
|
|
2030
|
+
if prune_images:
|
2031
|
+
print(json.dumps(manager.prune_images(force, prune_images_all), indent=2))
|
2032
|
+
|
1668
2033
|
if list_containers:
|
1669
2034
|
print(json.dumps(manager.list_containers(all_containers), indent=2))
|
1670
2035
|
|
@@ -1710,6 +2075,9 @@ def container_manager(argv):
|
|
1710
2075
|
json.dumps(manager.remove_container(remove_container_id, force), indent=2)
|
1711
2076
|
)
|
1712
2077
|
|
2078
|
+
if prune_containers:
|
2079
|
+
print(json.dumps(manager.prune_containers(), indent=2))
|
2080
|
+
|
1713
2081
|
if get_container_logs:
|
1714
2082
|
if not container_logs_id:
|
1715
2083
|
raise ValueError("Container ID required for get-container-logs")
|
@@ -1739,6 +2107,9 @@ def container_manager(argv):
|
|
1739
2107
|
raise ValueError("Name required for remove-volume")
|
1740
2108
|
print(json.dumps(manager.remove_volume(remove_volume_name, force), indent=2))
|
1741
2109
|
|
2110
|
+
if prune_volumes:
|
2111
|
+
print(json.dumps(manager.prune_volumes(force, prune_volumes_all), indent=2))
|
2112
|
+
|
1742
2113
|
if list_networks:
|
1743
2114
|
print(json.dumps(manager.list_networks(), indent=2))
|
1744
2115
|
|
@@ -1752,6 +2123,12 @@ def container_manager(argv):
|
|
1752
2123
|
raise ValueError("ID required for remove-network")
|
1753
2124
|
print(json.dumps(manager.remove_network(remove_network_id), indent=2))
|
1754
2125
|
|
2126
|
+
if prune_networks:
|
2127
|
+
print(json.dumps(manager.prune_networks(), indent=2))
|
2128
|
+
|
2129
|
+
if prune_system:
|
2130
|
+
print(json.dumps(manager.prune_system(force, prune_system_all), indent=2))
|
2131
|
+
|
1755
2132
|
if compose_up:
|
1756
2133
|
if not compose_up_file:
|
1757
2134
|
raise ValueError("File required for compose-up")
|
@@ -1819,4 +2196,4 @@ if __name__ == "__main__":
|
|
1819
2196
|
if len(sys.argv) < 2:
|
1820
2197
|
usage()
|
1821
2198
|
sys.exit(2)
|
1822
|
-
container_manager(
|
2199
|
+
container_manager()
|