container-manager-mcp 1.0.3__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 +498 -402
- container_manager_mcp/container_manager_mcp.py +34 -4
- {container_manager_mcp-1.0.3.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.3.dist-info/RECORD +0 -10
- {container_manager_mcp-1.0.3.dist-info → container_manager_mcp-1.0.4.dist-info}/WHEEL +0 -0
- {container_manager_mcp-1.0.3.dist-info → container_manager_mcp-1.0.4.dist-info}/entry_points.txt +0 -0
- {container_manager_mcp-1.0.3.dist-info → container_manager_mcp-1.0.4.dist-info}/licenses/LICENSE +0 -0
- {container_manager_mcp-1.0.3.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)."""
|
@@ -135,7 +140,7 @@ class ContainerManagerBase(ABC):
|
|
135
140
|
pass
|
136
141
|
|
137
142
|
@abstractmethod
|
138
|
-
def prune_containers(self
|
143
|
+
def prune_containers(self) -> Dict:
|
139
144
|
pass
|
140
145
|
|
141
146
|
@abstractmethod
|
@@ -245,6 +250,78 @@ class DockerManager(ContainerManagerBase):
|
|
245
250
|
self.logger.error(f"Failed to connect to Docker daemon: {str(e)}")
|
246
251
|
raise RuntimeError(f"Failed to connect to Docker: {str(e)}")
|
247
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
|
+
|
248
325
|
def list_images(self) -> List[Dict]:
|
249
326
|
params = {}
|
250
327
|
try:
|
@@ -314,21 +391,55 @@ class DockerManager(ContainerManagerBase):
|
|
314
391
|
self.log_action("pull_image", params, error=e)
|
315
392
|
raise RuntimeError(f"Failed to pull image: {str(e)}")
|
316
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
|
+
|
317
405
|
def prune_images(self, force: bool = False, all: bool = False) -> Dict:
|
318
406
|
params = {"force": force, "all": all}
|
319
407
|
try:
|
320
|
-
|
321
|
-
|
322
|
-
|
323
|
-
|
324
|
-
|
325
|
-
|
326
|
-
|
327
|
-
|
328
|
-
|
329
|
-
|
330
|
-
|
331
|
-
|
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
|
332
443
|
except Exception as e:
|
333
444
|
self.log_action("prune_images", params, error=e)
|
334
445
|
raise RuntimeError(f"Failed to prune images: {str(e)}")
|
@@ -423,134 +534,6 @@ class DockerManager(ContainerManagerBase):
|
|
423
534
|
self.log_action("run_container", params, error=e)
|
424
535
|
raise RuntimeError(f"Failed to run container: {str(e)}")
|
425
536
|
|
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
537
|
def stop_container(self, container_id: str, timeout: int = 10) -> Dict:
|
555
538
|
params = {"container_id": container_id, "timeout": timeout}
|
556
539
|
try:
|
@@ -575,6 +558,34 @@ class DockerManager(ContainerManagerBase):
|
|
575
558
|
self.log_action("remove_container", params, error=e)
|
576
559
|
raise RuntimeError(f"Failed to remove container: {str(e)}")
|
577
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
|
+
|
578
589
|
def get_container_logs(self, container_id: str, tail: str = "all") -> str:
|
579
590
|
params = {"container_id": container_id, "tail": tail}
|
580
591
|
try:
|
@@ -660,7 +671,6 @@ class DockerManager(ContainerManagerBase):
|
|
660
671
|
params = {"force": force, "all": all}
|
661
672
|
try:
|
662
673
|
if all:
|
663
|
-
# Remove all volumes (equivalent to --all, but docker doesn't have --all for prune; we list and remove)
|
664
674
|
volumes = self.client.volumes.list(all=True)
|
665
675
|
removed = []
|
666
676
|
for v in volumes:
|
@@ -668,20 +678,25 @@ class DockerManager(ContainerManagerBase):
|
|
668
678
|
v.remove(force=force)
|
669
679
|
removed.append(v.attrs["Name"])
|
670
680
|
except Exception as e:
|
671
|
-
|
672
|
-
|
681
|
+
self.logger.info(
|
682
|
+
f"Info: Failed to remove volume {v.attrs.get('Name', 'unknown')}: {e}"
|
683
|
+
)
|
684
|
+
continue
|
673
685
|
result = {
|
674
686
|
"volumes_removed": removed,
|
675
687
|
"space_reclaimed": "N/A (all volumes)",
|
676
688
|
}
|
677
689
|
else:
|
678
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}")
|
679
694
|
pruned = {
|
680
|
-
"space_reclaimed": self._format_size(
|
695
|
+
"space_reclaimed": self._format_size(
|
696
|
+
result.get("SpaceReclaimed", 0)
|
697
|
+
),
|
681
698
|
"volumes_removed": (
|
682
|
-
[v["Name"] for v in result
|
683
|
-
if result["VolumesRemoved"]
|
684
|
-
else []
|
699
|
+
[v["Name"] for v in result.get("VolumesDeleted", [])]
|
685
700
|
),
|
686
701
|
}
|
687
702
|
result = pruned
|
@@ -691,46 +706,81 @@ class DockerManager(ContainerManagerBase):
|
|
691
706
|
self.log_action("prune_volumes", params, error=e)
|
692
707
|
raise RuntimeError(f"Failed to prune volumes: {str(e)}")
|
693
708
|
|
694
|
-
def
|
695
|
-
params = {
|
696
|
-
try:
|
697
|
-
network = self.client.networks.get(network_id)
|
698
|
-
network.remove()
|
699
|
-
result = {"removed": network_id}
|
700
|
-
self.log_action("remove_network", params, result)
|
701
|
-
return result
|
702
|
-
except Exception as e:
|
703
|
-
self.log_action("remove_network", params, error=e)
|
704
|
-
raise RuntimeError(f"Failed to remove network: {str(e)}")
|
705
|
-
|
706
|
-
def prune_system(self, force: bool = False, all: bool = False) -> Dict:
|
707
|
-
params = {"force": force, "all": all}
|
709
|
+
def list_networks(self) -> List[Dict]:
|
710
|
+
params = {}
|
708
711
|
try:
|
709
|
-
|
710
|
-
result =
|
711
|
-
|
712
|
-
|
713
|
-
"
|
714
|
-
|
715
|
-
|
716
|
-
|
717
|
-
|
718
|
-
|
719
|
-
|
720
|
-
|
721
|
-
|
722
|
-
|
723
|
-
|
724
|
-
|
725
|
-
|
726
|
-
|
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
|
+
|
754
|
+
def remove_network(self, network_id: str) -> Dict:
|
755
|
+
params = {"network_id": network_id}
|
756
|
+
try:
|
757
|
+
network = self.client.networks.get(network_id)
|
758
|
+
network.remove()
|
759
|
+
result = {"removed": network_id}
|
760
|
+
self.log_action("remove_network", params, result)
|
761
|
+
return result
|
762
|
+
except Exception as e:
|
763
|
+
self.log_action("remove_network", params, error=e)
|
764
|
+
raise RuntimeError(f"Failed to remove network: {str(e)}")
|
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", [])]
|
727
777
|
),
|
728
778
|
}
|
729
|
-
self.log_action("
|
779
|
+
self.log_action("prune_networks", params, pruned)
|
730
780
|
return pruned
|
731
781
|
except Exception as e:
|
732
|
-
self.log_action("
|
733
|
-
raise RuntimeError(f"Failed to prune
|
782
|
+
self.log_action("prune_networks", params, error=e)
|
783
|
+
raise RuntimeError(f"Failed to prune networks: {str(e)}")
|
734
784
|
|
735
785
|
def compose_up(
|
736
786
|
self, compose_file: str, detach: bool = True, build: bool = False
|
@@ -957,17 +1007,14 @@ class DockerManager(ContainerManagerBase):
|
|
957
1007
|
class PodmanManager(ContainerManagerBase):
|
958
1008
|
def __init__(self, silent: bool = False, log_file: Optional[str] = None):
|
959
1009
|
super().__init__(silent, log_file)
|
960
|
-
|
961
1010
|
if PodmanClient is None:
|
962
1011
|
raise ImportError("Please install podman-py: pip install podman")
|
963
|
-
|
964
1012
|
base_url = self._autodetect_podman_url()
|
965
1013
|
if base_url is None:
|
966
1014
|
self.logger.error(
|
967
1015
|
"No valid Podman socket found after trying all known locations"
|
968
1016
|
)
|
969
1017
|
raise RuntimeError("Failed to connect to Podman: No valid socket found")
|
970
|
-
|
971
1018
|
try:
|
972
1019
|
self.client = PodmanClient(base_url=base_url)
|
973
1020
|
self.logger.info(f"Connected to Podman with base_url: {base_url}")
|
@@ -1002,7 +1049,6 @@ class PodmanManager(ContainerManagerBase):
|
|
1002
1049
|
"""Attempt to connect to Podman with the given base_url."""
|
1003
1050
|
try:
|
1004
1051
|
client = PodmanClient(base_url=base_url)
|
1005
|
-
# Test connection
|
1006
1052
|
client.version()
|
1007
1053
|
return client
|
1008
1054
|
except PodmanError as e:
|
@@ -1011,41 +1057,31 @@ class PodmanManager(ContainerManagerBase):
|
|
1011
1057
|
|
1012
1058
|
def _autodetect_podman_url(self) -> Optional[str]:
|
1013
1059
|
"""Autodetect the appropriate Podman socket URL based on platform."""
|
1014
|
-
# Check for environment variable override
|
1015
1060
|
base_url = os.environ.get("PODMAN_BASE_URL")
|
1016
1061
|
if base_url:
|
1017
1062
|
self.logger.info(f"Using PODMAN_BASE_URL from environment: {base_url}")
|
1018
1063
|
return base_url
|
1019
|
-
|
1020
1064
|
system = platform.system()
|
1021
1065
|
is_wsl = self._is_wsl()
|
1022
|
-
|
1023
|
-
# Define socket candidates based on platform
|
1024
1066
|
socket_candidates = []
|
1025
1067
|
if system == "Windows" and not is_wsl:
|
1026
|
-
# Windows with Podman machine
|
1027
1068
|
if self._is_podman_machine_running():
|
1028
1069
|
socket_candidates.append("npipe:////./pipe/docker_engine")
|
1029
|
-
# Fallback to WSL2 distro sockets if running in a mixed setup
|
1030
1070
|
socket_candidates.extend(
|
1031
1071
|
[
|
1032
|
-
"unix:///mnt/wsl/podman-sockets/podman-machine-default/podman-user.sock",
|
1033
|
-
"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",
|
1034
1074
|
]
|
1035
1075
|
)
|
1036
1076
|
elif system == "Linux" or is_wsl:
|
1037
|
-
# Linux or WSL2 distro: prioritize rootless, then rootful
|
1038
1077
|
uid = os.getuid()
|
1039
1078
|
socket_candidates.extend(
|
1040
1079
|
[
|
1041
|
-
f"unix:///run/user/{uid}/podman/podman.sock",
|
1042
|
-
"unix:///run/podman/podman.sock",
|
1080
|
+
f"unix:///run/user/{uid}/podman/podman.sock",
|
1081
|
+
"unix:///run/podman/podman.sock",
|
1043
1082
|
]
|
1044
1083
|
)
|
1045
|
-
|
1046
|
-
# Try each socket candidate
|
1047
1084
|
for url in socket_candidates:
|
1048
|
-
# For Unix sockets, check if the file exists (on Linux/WSL2)
|
1049
1085
|
if url.startswith("unix://") and (system == "Linux" or is_wsl):
|
1050
1086
|
socket_path = url.replace("unix://", "")
|
1051
1087
|
if not os.path.exists(socket_path):
|
@@ -1054,9 +1090,207 @@ class PodmanManager(ContainerManagerBase):
|
|
1054
1090
|
client = self._try_connect(url)
|
1055
1091
|
if client:
|
1056
1092
|
return url
|
1057
|
-
|
1058
1093
|
return None
|
1059
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
|
+
|
1060
1294
|
def list_images(self) -> List[Dict]:
|
1061
1295
|
params = {}
|
1062
1296
|
try:
|
@@ -1122,22 +1356,16 @@ class PodmanManager(ContainerManagerBase):
|
|
1122
1356
|
self.log_action("pull_image", params, error=e)
|
1123
1357
|
raise RuntimeError(f"Failed to pull image: {str(e)}")
|
1124
1358
|
|
1125
|
-
def
|
1126
|
-
params = {"
|
1359
|
+
def remove_image(self, image: str, force: bool = False) -> Dict:
|
1360
|
+
params = {"image": image, "force": force}
|
1127
1361
|
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
|
1362
|
+
self.client.images.remove(image, force=force)
|
1363
|
+
result = {"removed": image}
|
1364
|
+
self.log_action("remove_image", params, result)
|
1365
|
+
return result
|
1138
1366
|
except Exception as e:
|
1139
|
-
self.log_action("
|
1140
|
-
raise RuntimeError(f"Failed to
|
1367
|
+
self.log_action("remove_image", params, error=e)
|
1368
|
+
raise RuntimeError(f"Failed to remove image: {str(e)}")
|
1141
1369
|
|
1142
1370
|
def list_containers(self, all: bool = False) -> List[Dict]:
|
1143
1371
|
params = {"all": all}
|
@@ -1225,131 +1453,6 @@ class PodmanManager(ContainerManagerBase):
|
|
1225
1453
|
self.log_action("run_container", params, error=e)
|
1226
1454
|
raise RuntimeError(f"Failed to run container: {str(e)}")
|
1227
1455
|
|
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
1456
|
def stop_container(self, container_id: str, timeout: int = 10) -> Dict:
|
1354
1457
|
params = {"container_id": container_id, "timeout": timeout}
|
1355
1458
|
try:
|
@@ -1455,40 +1558,50 @@ class PodmanManager(ContainerManagerBase):
|
|
1455
1558
|
self.log_action("remove_volume", params, error=e)
|
1456
1559
|
raise RuntimeError(f"Failed to remove volume: {str(e)}")
|
1457
1560
|
|
1458
|
-
def
|
1459
|
-
params = {
|
1561
|
+
def list_networks(self) -> List[Dict]:
|
1562
|
+
params = {}
|
1460
1563
|
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
|
-
],
|
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,
|
1485
1578
|
}
|
1486
|
-
result
|
1487
|
-
self.log_action("
|
1579
|
+
result.append(simplified)
|
1580
|
+
self.log_action("list_networks", params, result)
|
1488
1581
|
return result
|
1489
1582
|
except Exception as e:
|
1490
|
-
self.log_action("
|
1491
|
-
raise RuntimeError(f"Failed to
|
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)}")
|
1492
1605
|
|
1493
1606
|
def remove_network(self, network_id: str) -> Dict:
|
1494
1607
|
params = {"network_id": network_id}
|
@@ -1502,26 +1615,6 @@ class PodmanManager(ContainerManagerBase):
|
|
1502
1615
|
self.log_action("remove_network", params, error=e)
|
1503
1616
|
raise RuntimeError(f"Failed to remove network: {str(e)}")
|
1504
1617
|
|
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
1618
|
def compose_up(
|
1526
1619
|
self, compose_file: str, detach: bool = True, build: bool = False
|
1527
1620
|
) -> str:
|
@@ -1608,21 +1701,24 @@ class PodmanManager(ContainerManagerBase):
|
|
1608
1701
|
raise NotImplementedError("Swarm not supported in Podman")
|
1609
1702
|
|
1610
1703
|
|
1704
|
+
def is_app_installed(app_name: str = "docker") -> bool:
|
1705
|
+
return shutil.which(app_name.lower()) is not None
|
1706
|
+
|
1707
|
+
|
1611
1708
|
def create_manager(
|
1612
1709
|
manager_type: Optional[str] = None, silent: bool = False, log_file: str = None
|
1613
1710
|
) -> ContainerManagerBase:
|
1614
1711
|
if manager_type is None:
|
1615
|
-
manager_type = os.environ.get("CONTAINER_MANAGER_TYPE")
|
1712
|
+
manager_type = os.environ.get("CONTAINER_MANAGER_TYPE", None)
|
1616
1713
|
if manager_type is None:
|
1617
|
-
|
1618
|
-
if PodmanClient is not None:
|
1714
|
+
if is_app_installed("podman"):
|
1619
1715
|
try:
|
1620
1716
|
test_client = PodmanClient()
|
1621
1717
|
test_client.close()
|
1622
1718
|
manager_type = "podman"
|
1623
1719
|
except Exception:
|
1624
1720
|
pass
|
1625
|
-
if
|
1721
|
+
if is_app_installed("docker"):
|
1626
1722
|
try:
|
1627
1723
|
test_client = docker.from_env()
|
1628
1724
|
test_client.close()
|
@@ -1721,7 +1817,7 @@ container_manager.py --manager docker --pull-image nginx --tag latest --list-con
|
|
1721
1817
|
)
|
1722
1818
|
|
1723
1819
|
|
1724
|
-
def container_manager(
|
1820
|
+
def container_manager():
|
1725
1821
|
parser = argparse.ArgumentParser(
|
1726
1822
|
description="Container Manager: A tool to manage containers with Docker, Podman, and Docker Swarm!"
|
1727
1823
|
)
|
@@ -1826,7 +1922,7 @@ def container_manager(argv):
|
|
1826
1922
|
parser.add_argument("--force", action="store_true", help="Force removal")
|
1827
1923
|
parser.add_argument("-h", "--help", action="store_true", help="Show help")
|
1828
1924
|
|
1829
|
-
args = parser.parse_args(
|
1925
|
+
args = parser.parse_args()
|
1830
1926
|
|
1831
1927
|
if args.help:
|
1832
1928
|
usage()
|
@@ -1980,7 +2076,7 @@ def container_manager(argv):
|
|
1980
2076
|
)
|
1981
2077
|
|
1982
2078
|
if prune_containers:
|
1983
|
-
print(json.dumps(manager.prune_containers(
|
2079
|
+
print(json.dumps(manager.prune_containers(), indent=2))
|
1984
2080
|
|
1985
2081
|
if get_container_logs:
|
1986
2082
|
if not container_logs_id:
|
@@ -2028,7 +2124,7 @@ def container_manager(argv):
|
|
2028
2124
|
print(json.dumps(manager.remove_network(remove_network_id), indent=2))
|
2029
2125
|
|
2030
2126
|
if prune_networks:
|
2031
|
-
print(json.dumps(manager.prune_networks(
|
2127
|
+
print(json.dumps(manager.prune_networks(), indent=2))
|
2032
2128
|
|
2033
2129
|
if prune_system:
|
2034
2130
|
print(json.dumps(manager.prune_system(force, prune_system_all), indent=2))
|
@@ -2100,4 +2196,4 @@ if __name__ == "__main__":
|
|
2100
2196
|
if len(sys.argv) < 2:
|
2101
2197
|
usage()
|
2102
2198
|
sys.exit(2)
|
2103
|
-
container_manager(
|
2199
|
+
container_manager()
|
@@ -39,6 +39,29 @@ def to_boolean(string):
|
|
39
39
|
raise ValueError(f"Cannot convert '{string}' to boolean")
|
40
40
|
|
41
41
|
|
42
|
+
def parse_image_string(image: str, default_tag: str = "latest") -> tuple[str, str]:
|
43
|
+
"""
|
44
|
+
Parse a container image string into image and tag components.
|
45
|
+
|
46
|
+
Args:
|
47
|
+
image: Input image string (e.g., 'registry.arpa/ubuntu/ubuntu:latest' or 'nginx')
|
48
|
+
default_tag: Fallback tag if none is specified (default: 'latest')
|
49
|
+
|
50
|
+
Returns:
|
51
|
+
Tuple of (image, tag) where image includes registry/repository, tag is the tag or default_tag
|
52
|
+
"""
|
53
|
+
# Split on the last ':' to separate image and tag
|
54
|
+
if ":" in image:
|
55
|
+
parts = image.rsplit(":", 1)
|
56
|
+
image_name, tag = parts[0], parts[1]
|
57
|
+
# Ensure tag is valid (not a port or malformed)
|
58
|
+
if "/" in tag or not tag:
|
59
|
+
# If tag contains '/' or is empty, assume no tag was provided
|
60
|
+
return image, default_tag
|
61
|
+
return image_name, tag
|
62
|
+
return image, default_tag
|
63
|
+
|
64
|
+
|
42
65
|
environment_silent = os.environ.get("SILENT", False)
|
43
66
|
environment_log_file = os.environ.get("LOG_FILE", None)
|
44
67
|
environment_container_manager_type = os.environ.get("CONTAINER_MANAGER_TYPE", None)
|
@@ -171,8 +194,13 @@ async def list_images(
|
|
171
194
|
tags={"container_management"},
|
172
195
|
)
|
173
196
|
async def pull_image(
|
174
|
-
image: str = Field(
|
175
|
-
|
197
|
+
image: str = Field(
|
198
|
+
description="Image name to pull (e.g., nginx, registry.arpa/ubuntu/ubuntu:latest)."
|
199
|
+
),
|
200
|
+
tag: str = Field(
|
201
|
+
description="Image tag (overridden if tag is included in image string)",
|
202
|
+
default="latest",
|
203
|
+
),
|
176
204
|
platform: Optional[str] = Field(
|
177
205
|
description="Platform (e.g., linux/amd64)", default=None
|
178
206
|
),
|
@@ -191,12 +219,14 @@ async def pull_image(
|
|
191
219
|
),
|
192
220
|
) -> Dict:
|
193
221
|
logger = logging.getLogger("ContainerManager")
|
222
|
+
# Parse image string to separate image and tag
|
223
|
+
parsed_image, parsed_tag = parse_image_string(image, tag)
|
194
224
|
logger.debug(
|
195
|
-
f"Pulling image {
|
225
|
+
f"Pulling image {parsed_image}:{parsed_tag} for {manager_type}, silent: {silent}, log_file: {log_file}"
|
196
226
|
)
|
197
227
|
try:
|
198
228
|
manager = create_manager(manager_type, silent, log_file)
|
199
|
-
return manager.pull_image(
|
229
|
+
return manager.pull_image(parsed_image, parsed_tag, platform)
|
200
230
|
except Exception as e:
|
201
231
|
logger.error(f"Failed to pull image: {str(e)}")
|
202
232
|
raise RuntimeError(f"Failed to pull image: {str(e)}")
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: container-manager-mcp
|
3
|
-
Version: 1.0.
|
3
|
+
Version: 1.0.4
|
4
4
|
Summary: Container Manager manage Docker, Docker Swarm, and Podman containers as an MCP Server
|
5
5
|
Author-email: Audel Rouhi <knucklessg1@gmail.com>
|
6
6
|
License: MIT
|
@@ -48,7 +48,7 @@ Dynamic: license-file
|
|
48
48
|

|
49
49
|

|
50
50
|
|
51
|
-
*Version: 1.0.
|
51
|
+
*Version: 1.0.4*
|
52
52
|
|
53
53
|
Container Manager MCP Server provides a robust interface to manage Docker and Podman containers, networks, volumes, and Docker Swarm services through a FastMCP server, enabling programmatic and remote container management.
|
54
54
|
|
@@ -0,0 +1,10 @@
|
|
1
|
+
container_manager_mcp/__init__.py,sha256=N3bhKd_oh5YmBBl9N1omfZgaXhJyP0vOzH4VKxs68_g,506
|
2
|
+
container_manager_mcp/__main__.py,sha256=zic5tX336HG8LfdzQQ0sDVx-tMSOsgOZCtaxHWgJ4Go,134
|
3
|
+
container_manager_mcp/container_manager.py,sha256=sHxM17dj7luzp1Ak2aQJ3JLqHd3POO3S2_W32HQwWog,89167
|
4
|
+
container_manager_mcp/container_manager_mcp.py,sha256=E3FOTFILyYYoJF3Erkpl5zapkiDi2pKlJlkZByc-8QE,47361
|
5
|
+
container_manager_mcp-1.0.4.dist-info/licenses/LICENSE,sha256=Z1xmcrPHBnGCETO_LLQJUeaSNBSnuptcDVTt4kaPUOE,1060
|
6
|
+
container_manager_mcp-1.0.4.dist-info/METADATA,sha256=XhTi2pEQeO1cfnrVgVViq_LiwGqLqUyhsogD-jXUFys,8238
|
7
|
+
container_manager_mcp-1.0.4.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
8
|
+
container_manager_mcp-1.0.4.dist-info/entry_points.txt,sha256=I23pXcCgAShlfYbENzs3kbw3l1lU9Gy7lODPfRqeeiA,156
|
9
|
+
container_manager_mcp-1.0.4.dist-info/top_level.txt,sha256=B7QQLOd9mBdu0lsPKqyu4T8-zUtbqKzQJbMbtAzoozU,22
|
10
|
+
container_manager_mcp-1.0.4.dist-info/RECORD,,
|
@@ -1,10 +0,0 @@
|
|
1
|
-
container_manager_mcp/__init__.py,sha256=N3bhKd_oh5YmBBl9N1omfZgaXhJyP0vOzH4VKxs68_g,506
|
2
|
-
container_manager_mcp/__main__.py,sha256=zic5tX336HG8LfdzQQ0sDVx-tMSOsgOZCtaxHWgJ4Go,134
|
3
|
-
container_manager_mcp/container_manager.py,sha256=1HN_sKQFGVtf35wk66Uez9MtHOv2B9LJ4tbTbEzCXEA,84152
|
4
|
-
container_manager_mcp/container_manager_mcp.py,sha256=M43YmLt_qoOfpnlK2VyyGX2vJ6gMBxepSqBauvMS4AA,46204
|
5
|
-
container_manager_mcp-1.0.3.dist-info/licenses/LICENSE,sha256=Z1xmcrPHBnGCETO_LLQJUeaSNBSnuptcDVTt4kaPUOE,1060
|
6
|
-
container_manager_mcp-1.0.3.dist-info/METADATA,sha256=RfIVbh9AeL0roB2yyqtXBJR2o8H7MqTpV82epVm0V1I,8238
|
7
|
-
container_manager_mcp-1.0.3.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
8
|
-
container_manager_mcp-1.0.3.dist-info/entry_points.txt,sha256=I23pXcCgAShlfYbENzs3kbw3l1lU9Gy7lODPfRqeeiA,156
|
9
|
-
container_manager_mcp-1.0.3.dist-info/top_level.txt,sha256=B7QQLOd9mBdu0lsPKqyu4T8-zUtbqKzQJbMbtAzoozU,22
|
10
|
-
container_manager_mcp-1.0.3.dist-info/RECORD,,
|
File without changes
|
{container_manager_mcp-1.0.3.dist-info → container_manager_mcp-1.0.4.dist-info}/entry_points.txt
RENAMED
File without changes
|
{container_manager_mcp-1.0.3.dist-info → container_manager_mcp-1.0.4.dist-info}/licenses/LICENSE
RENAMED
File without changes
|
{container_manager_mcp-1.0.3.dist-info → container_manager_mcp-1.0.4.dist-info}/top_level.txt
RENAMED
File without changes
|