container-manager-mcp 1.0.3__py3-none-any.whl → 1.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (29) hide show
  1. container_manager_mcp/__init__.py +23 -17
  2. container_manager_mcp/__main__.py +2 -2
  3. container_manager_mcp/container_manager.py +555 -441
  4. container_manager_mcp/container_manager_a2a.py +339 -0
  5. container_manager_mcp/container_manager_mcp.py +2055 -1323
  6. container_manager_mcp/mcp_config.json +7 -0
  7. container_manager_mcp/skills/container-manager-compose/SKILL.md +25 -0
  8. container_manager_mcp/skills/container-manager-containers/SKILL.md +28 -0
  9. container_manager_mcp/skills/container-manager-containers/troubleshoot.md +5 -0
  10. container_manager_mcp/skills/container-manager-images/SKILL.md +25 -0
  11. container_manager_mcp/skills/container-manager-info/SKILL.md +23 -0
  12. container_manager_mcp/skills/container-manager-logs/SKILL.md +22 -0
  13. container_manager_mcp/skills/container-manager-networks/SKILL.md +22 -0
  14. container_manager_mcp/skills/container-manager-swarm/SKILL.md +28 -0
  15. container_manager_mcp/skills/container-manager-swarm/orchestrate.md +4 -0
  16. container_manager_mcp/skills/container-manager-system/SKILL.md +19 -0
  17. container_manager_mcp/skills/container-manager-volumes/SKILL.md +23 -0
  18. container_manager_mcp/utils.py +31 -0
  19. container_manager_mcp-1.2.0.dist-info/METADATA +371 -0
  20. container_manager_mcp-1.2.0.dist-info/RECORD +26 -0
  21. container_manager_mcp-1.2.0.dist-info/entry_points.txt +4 -0
  22. {container_manager_mcp-1.0.3.dist-info → container_manager_mcp-1.2.0.dist-info}/top_level.txt +1 -0
  23. scripts/validate_a2a_agent.py +150 -0
  24. scripts/validate_agent.py +67 -0
  25. container_manager_mcp-1.0.3.dist-info/METADATA +0 -243
  26. container_manager_mcp-1.0.3.dist-info/RECORD +0 -10
  27. container_manager_mcp-1.0.3.dist-info/entry_points.txt +0 -3
  28. {container_manager_mcp-1.0.3.dist-info → container_manager_mcp-1.2.0.dist-info}/WHEEL +0 -0
  29. {container_manager_mcp-1.0.3.dist-info → container_manager_mcp-1.2.0.dist-info}/licenses/LICENSE +0 -0
@@ -8,10 +8,11 @@ from abc import ABC, abstractmethod
8
8
  from typing import List, Dict, Optional, Any
9
9
  import argparse
10
10
  import json
11
+ import shutil
11
12
  import subprocess
12
13
  from datetime import datetime
13
- import dateutil.parser
14
14
  import platform
15
+ import traceback
15
16
 
16
17
  try:
17
18
  import docker
@@ -40,7 +41,7 @@ class ContainerManagerBase(ABC):
40
41
  log_file = os.path.join(script_dir, "container_manager.log")
41
42
  logging.basicConfig(
42
43
  filename=log_file,
43
- level=logging.INFO,
44
+ level=logging.DEBUG, # Changed to DEBUG for more detailed logging
44
45
  format="%(asctime)s - %(levelname)s - %(message)s",
45
46
  )
46
47
  self.logger = logging.getLogger(__name__)
@@ -57,7 +58,10 @@ class ContainerManagerBase(ABC):
57
58
  if result:
58
59
  self.logger.info(f"Result: {result}")
59
60
  if error:
60
- self.logger.error(f"Error: {str(error)}")
61
+ self.logger.error(
62
+ f"Error in {action}: {type(error).__name__}: {str(error)}"
63
+ )
64
+ self.logger.error(f"Traceback: {traceback.format_exc()}")
61
65
 
62
66
  def _format_size(self, size_bytes: int) -> str:
63
67
  """Helper to format bytes to human-readable (e.g., 1.23GB)."""
@@ -70,17 +74,38 @@ class ContainerManagerBase(ABC):
70
74
  return f"{size_bytes:.2f}PB"
71
75
 
72
76
  def _parse_timestamp(self, timestamp: Any) -> str:
73
- """Parse timestamp (integer or string) to ISO 8601 string."""
77
+ """Parse timestamp (integer, float, or string) to ISO 8601 string."""
74
78
  if not timestamp:
75
79
  return "unknown"
80
+
81
+ # Handle numeric timestamps (Unix timestamps in seconds)
76
82
  if isinstance(timestamp, (int, float)):
77
- return datetime.fromtimestamp(timestamp).strftime("%Y-%m-%dT%H:%M:%S")
78
- if isinstance(timestamp, str):
79
83
  try:
80
- parsed = dateutil.parser.isoparse(timestamp)
81
- return parsed.strftime("%Y-%m-%dT%H:%M:%S")
82
- except ValueError:
84
+ return datetime.fromtimestamp(timestamp).strftime("%Y-%m-%dT%H:%M:%S")
85
+ except (ValueError, OSError):
83
86
  return "unknown"
87
+
88
+ # Handle string timestamps
89
+ if isinstance(timestamp, str):
90
+ # Common ISO 8601-like formats to try
91
+ formats = [
92
+ "%Y-%m-%dT%H:%M:%S", # 2023-10-05T14:30:00
93
+ "%Y-%m-%d %H:%M:%S", # 2023-10-05 14:30:00
94
+ "%Y-%m-%dT%H:%M:%S%z", # 2023-10-05T14:30:00+0000
95
+ "%Y-%m-%d %H:%M:%S%z", # 2023-10-05 14:30:00+0000
96
+ "%Y-%m-%dT%H:%M:%S.%f", # 2023-10-05T14:30:00.123456
97
+ "%Y-%m-%d %H:%M:%S.%f", # 2023-10-05 14:30:00.123456
98
+ "%Y-%m-%dT%H:%M:%S.%f%z", # 2023-10-05T14:30:00.123456+0000
99
+ "%Y-%m-%d", # 2023-10-05
100
+ ]
101
+ for fmt in formats:
102
+ try:
103
+ parsed = datetime.strptime(timestamp, fmt)
104
+ return parsed.strftime("%Y-%m-%dT%H:%M:%S")
105
+ except ValueError:
106
+ continue
107
+ return "unknown"
108
+
84
109
  return "unknown"
85
110
 
86
111
  @abstractmethod
@@ -135,7 +160,7 @@ class ContainerManagerBase(ABC):
135
160
  pass
136
161
 
137
162
  @abstractmethod
138
- def prune_containers(self, force: bool = False) -> Dict:
163
+ def prune_containers(self) -> Dict:
139
164
  pass
140
165
 
141
166
  @abstractmethod
@@ -245,6 +270,78 @@ class DockerManager(ContainerManagerBase):
245
270
  self.logger.error(f"Failed to connect to Docker daemon: {str(e)}")
246
271
  raise RuntimeError(f"Failed to connect to Docker: {str(e)}")
247
272
 
273
+ def prune_system(self, force: bool = False, all: bool = False) -> Dict:
274
+ params = {"force": force, "all": all}
275
+ try:
276
+ filters = {"until": None} if all else {}
277
+ result = self.client.system.prune(filters=filters, volumes=all)
278
+ if result is None:
279
+ result = {
280
+ "SpaceReclaimed": 0,
281
+ "ImagesDeleted": [],
282
+ "ContainersDeleted": [],
283
+ "VolumesDeleted": [],
284
+ "NetworksDeleted": [],
285
+ }
286
+ self.logger.debug(f"Raw prune_system result: {result}")
287
+ pruned = {
288
+ "space_reclaimed": self._format_size(result.get("SpaceReclaimed", 0)),
289
+ "images_removed": (
290
+ [img["Id"][7:19] for img in result.get("ImagesDeleted", [])]
291
+ ),
292
+ "containers_removed": (
293
+ [c["Id"][7:19] for c in result.get("ContainersDeleted", [])]
294
+ ),
295
+ "volumes_removed": (
296
+ [v["Name"] for v in result.get("VolumesDeleted", [])]
297
+ ),
298
+ "networks_removed": (
299
+ [n["Id"][7:19] for n in result.get("NetworksDeleted", [])]
300
+ ),
301
+ }
302
+ self.log_action("prune_system", params, pruned)
303
+ return pruned
304
+ except Exception as e:
305
+ self.log_action("prune_system", params, error=e)
306
+ raise RuntimeError(f"Failed to prune system: {str(e)}")
307
+
308
+ # Other DockerManager methods remain unchanged (omitted for brevity)
309
+ def get_version(self) -> Dict:
310
+ params = {}
311
+ try:
312
+ version = self.client.version()
313
+ result = {
314
+ "version": version.get("Version", "unknown"),
315
+ "api_version": version.get("ApiVersion", "unknown"),
316
+ "os": version.get("Os", "unknown"),
317
+ "arch": version.get("Arch", "unknown"),
318
+ "build_time": version.get("BuildTime", "unknown"),
319
+ }
320
+ self.log_action("get_version", params, result)
321
+ return result
322
+ except Exception as e:
323
+ self.log_action("get_version", params, error=e)
324
+ raise RuntimeError(f"Failed to get version: {str(e)}")
325
+
326
+ def get_info(self) -> Dict:
327
+ params = {}
328
+ try:
329
+ info = self.client.info()
330
+ result = {
331
+ "containers_total": info.get("Containers", 0),
332
+ "containers_running": info.get("ContainersRunning", 0),
333
+ "images": info.get("Images", 0),
334
+ "driver": info.get("Driver", "unknown"),
335
+ "platform": f"{info.get('OperatingSystem', 'unknown')} {info.get('Architecture', 'unknown')}",
336
+ "memory_total": self._format_size(info.get("MemTotal", 0)),
337
+ "swap_total": self._format_size(info.get("SwapTotal", 0)),
338
+ }
339
+ self.log_action("get_info", params, result)
340
+ return result
341
+ except Exception as e:
342
+ self.log_action("get_info", params, error=e)
343
+ raise RuntimeError(f"Failed to get info: {str(e)}")
344
+
248
345
  def list_images(self) -> List[Dict]:
249
346
  params = {}
250
347
  try:
@@ -314,21 +411,55 @@ class DockerManager(ContainerManagerBase):
314
411
  self.log_action("pull_image", params, error=e)
315
412
  raise RuntimeError(f"Failed to pull image: {str(e)}")
316
413
 
414
+ def remove_image(self, image: str, force: bool = False) -> Dict:
415
+ params = {"image": image, "force": force}
416
+ try:
417
+ self.client.images.remove(image, force=force)
418
+ result = {"removed": image}
419
+ self.log_action("remove_image", params, result)
420
+ return result
421
+ except Exception as e:
422
+ self.log_action("remove_image", params, error=e)
423
+ raise RuntimeError(f"Failed to remove image: {str(e)}")
424
+
317
425
  def prune_images(self, force: bool = False, all: bool = False) -> Dict:
318
426
  params = {"force": force, "all": all}
319
427
  try:
320
- filters = {"dangling": {"true": True}} if not all else {}
321
- result = self.client.images.prune(filters=filters)
322
- pruned = {
323
- "space_reclaimed": self._format_size(result["SpaceReclaimed"]),
324
- "images_removed": (
325
- [img["Id"][7:19] for img in result["ImagesRemoved"]]
326
- if result["ImagesRemoved"]
327
- else []
328
- ),
329
- }
330
- self.log_action("prune_images", params, pruned)
331
- return pruned
428
+ if all:
429
+ # Manually remove all unused images
430
+ images = self.client.images.list(all=True)
431
+ removed = []
432
+ for img in images:
433
+ try:
434
+ for tag in img.attrs.get("RepoTags", []):
435
+ self.client.images.remove(tag, force=force)
436
+ removed.append(img.attrs["Id"][7:19])
437
+ except Exception as e:
438
+ self.logger.info(
439
+ f"Info: Failed to remove image {img.attrs.get('Id', 'unknown')}: {e}"
440
+ )
441
+ continue
442
+ result = {
443
+ "images_removed": removed,
444
+ "space_reclaimed": "N/A (all images)",
445
+ }
446
+ else:
447
+ filters = {"dangling": True} if not all else {}
448
+ result = self.client.images.prune(filters=filters)
449
+ if result is None:
450
+ result = {"SpaceReclaimed": 0, "ImagesDeleted": []}
451
+ self.logger.debug(f"Raw prune_images result: {result}")
452
+ pruned = {
453
+ "space_reclaimed": self._format_size(
454
+ result.get("SpaceReclaimed", 0)
455
+ ),
456
+ "images_removed": (
457
+ [img["Id"][7:19] for img in result.get("ImagesDeleted", [])]
458
+ ),
459
+ }
460
+ result = pruned
461
+ self.log_action("prune_images", params, result)
462
+ return result
332
463
  except Exception as e:
333
464
  self.log_action("prune_images", params, error=e)
334
465
  raise RuntimeError(f"Failed to prune images: {str(e)}")
@@ -386,7 +517,7 @@ class DockerManager(ContainerManagerBase):
386
517
  }
387
518
  try:
388
519
  container = self.client.containers.run(
389
- image,
520
+ image=image,
390
521
  command=command,
391
522
  name=name,
392
523
  detach=detach,
@@ -399,14 +530,17 @@ class DockerManager(ContainerManagerBase):
399
530
  self.log_action("run_container", params, result)
400
531
  return result
401
532
  attrs = container.attrs
402
- ports = attrs.get("NetworkSettings", {}).get("Ports", {})
403
533
  port_mappings = []
404
- for container_port, host_ports in ports.items():
405
- if host_ports:
406
- for hp in host_ports:
407
- port_mappings.append(
408
- f"{hp.get('HostIp', '0.0.0.0')}:{hp.get('HostPort')}->{container_port}"
409
- )
534
+ if ports: # Check if ports is not None
535
+ network_settings = attrs.get("NetworkSettings", {})
536
+ container_ports = network_settings.get("Ports", {})
537
+ if container_ports: # Check if Ports dictionary is not empty
538
+ for container_port, host_ports in container_ports.items():
539
+ if host_ports: # Check if host_ports is not None or empty
540
+ for hp in host_ports:
541
+ port_mappings.append(
542
+ f"{hp.get('HostIp', '0.0.0.0')}:{hp.get('HostPort')}->{container_port}"
543
+ )
410
544
  created = attrs.get("Created", None)
411
545
  created_str = self._parse_timestamp(created)
412
546
  result = {
@@ -423,134 +557,6 @@ class DockerManager(ContainerManagerBase):
423
557
  self.log_action("run_container", params, error=e)
424
558
  raise RuntimeError(f"Failed to run container: {str(e)}")
425
559
 
426
- def prune_containers(self, force: bool = False) -> Dict:
427
- params = {"force": force}
428
- try:
429
- result = self.client.containers.prune()
430
- pruned = {
431
- "space_reclaimed": self._format_size(result["SpaceReclaimed"]),
432
- "containers_removed": (
433
- [c["Id"][7:19] for c in result["ContainersRemoved"]]
434
- if result["ContainersRemoved"]
435
- else []
436
- ),
437
- }
438
- self.log_action("prune_containers", params, pruned)
439
- return pruned
440
- except Exception as e:
441
- self.log_action("prune_containers", params, error=e)
442
- raise RuntimeError(f"Failed to prune containers: {str(e)}")
443
-
444
- def list_networks(self) -> List[Dict]:
445
- params = {}
446
- try:
447
- networks = self.client.networks.list()
448
- result = []
449
- for net in networks:
450
- attrs = net.attrs
451
- containers = len(attrs.get("Containers", {}))
452
- created = attrs.get("Created", None)
453
- created_str = self._parse_timestamp(created)
454
- simplified = {
455
- "id": attrs.get("Id", "unknown")[7:19],
456
- "name": attrs.get("Name", "unknown"),
457
- "driver": attrs.get("Driver", "unknown"),
458
- "scope": attrs.get("Scope", "unknown"),
459
- "containers": containers,
460
- "created": created_str,
461
- }
462
- result.append(simplified)
463
- self.log_action("list_networks", params, result)
464
- return result
465
- except Exception as e:
466
- self.log_action("list_networks", params, error=e)
467
- raise RuntimeError(f"Failed to list networks: {str(e)}")
468
-
469
- def create_network(self, name: str, driver: str = "bridge") -> Dict:
470
- params = {"name": name, "driver": driver}
471
- try:
472
- network = self.client.networks.create(name, driver=driver)
473
- attrs = network.attrs
474
- created = attrs.get("Created", None)
475
- created_str = self._parse_timestamp(created)
476
- result = {
477
- "id": attrs.get("Id", "unknown")[7:19],
478
- "name": attrs.get("Name", name),
479
- "driver": attrs.get("Driver", driver),
480
- "scope": attrs.get("Scope", "unknown"),
481
- "created": created_str,
482
- }
483
- self.log_action("create_network", params, result)
484
- return result
485
- except Exception as e:
486
- self.log_action("create_network", params, error=e)
487
- raise RuntimeError(f"Failed to create network: {str(e)}")
488
-
489
- def prune_networks(self) -> Dict:
490
- params = {}
491
- try:
492
- result = self.client.networks.prune()
493
- pruned = {
494
- "space_reclaimed": self._format_size(result["SpaceReclaimed"]),
495
- "networks_removed": (
496
- [n["Id"][7:19] for n in result["NetworksRemoved"]]
497
- if result["NetworksRemoved"]
498
- else []
499
- ),
500
- }
501
- self.log_action("prune_networks", params, pruned)
502
- return pruned
503
- except Exception as e:
504
- self.log_action("prune_networks", params, error=e)
505
- raise RuntimeError(f"Failed to prune networks: {str(e)}")
506
-
507
- def get_version(self) -> Dict:
508
- params = {}
509
- try:
510
- version = self.client.version()
511
- result = {
512
- "version": version.get("Version", "unknown"),
513
- "api_version": version.get("ApiVersion", "unknown"),
514
- "os": version.get("Os", "unknown"),
515
- "arch": version.get("Arch", "unknown"),
516
- "build_time": version.get("BuildTime", "unknown"),
517
- }
518
- self.log_action("get_version", params, result)
519
- return result
520
- except Exception as e:
521
- self.log_action("get_version", params, error=e)
522
- raise RuntimeError(f"Failed to get version: {str(e)}")
523
-
524
- def get_info(self) -> Dict:
525
- params = {}
526
- try:
527
- info = self.client.info()
528
- result = {
529
- "containers_total": info.get("Containers", 0),
530
- "containers_running": info.get("ContainersRunning", 0),
531
- "images": info.get("Images", 0),
532
- "driver": info.get("Driver", "unknown"),
533
- "platform": f"{info.get('OperatingSystem', 'unknown')} {info.get('Architecture', 'unknown')}",
534
- "memory_total": self._format_size(info.get("MemTotal", 0)),
535
- "swap_total": self._format_size(info.get("SwapTotal", 0)),
536
- }
537
- self.log_action("get_info", params, result)
538
- return result
539
- except Exception as e:
540
- self.log_action("get_info", params, error=e)
541
- raise RuntimeError(f"Failed to get info: {str(e)}")
542
-
543
- def remove_image(self, image: str, force: bool = False) -> Dict:
544
- params = {"image": image, "force": force}
545
- try:
546
- self.client.images.remove(image, force=force)
547
- result = {"removed": image}
548
- self.log_action("remove_image", params, result)
549
- return result
550
- except Exception as e:
551
- self.log_action("remove_image", params, error=e)
552
- raise RuntimeError(f"Failed to remove image: {str(e)}")
553
-
554
560
  def stop_container(self, container_id: str, timeout: int = 10) -> Dict:
555
561
  params = {"container_id": container_id, "timeout": timeout}
556
562
  try:
@@ -575,6 +581,34 @@ class DockerManager(ContainerManagerBase):
575
581
  self.log_action("remove_container", params, error=e)
576
582
  raise RuntimeError(f"Failed to remove container: {str(e)}")
577
583
 
584
+ def prune_containers(self) -> Dict:
585
+ params = {}
586
+ try:
587
+ result = self.client.containers.prune()
588
+ self.logger.debug(f"Raw prune_containers result: {result}")
589
+ if result is None:
590
+ result = {"SpaceReclaimed": 0, "ContainersDeleted": []}
591
+ pruned = {
592
+ "space_reclaimed": self._format_size(result.get("SpaceReclaimed", 0)),
593
+ "containers_removed": (
594
+ [c["Id"][7:19] for c in result.get("ContainersDeleted", [])]
595
+ ),
596
+ }
597
+ self.log_action("prune_containers", params, pruned)
598
+ return pruned
599
+ except TypeError as e:
600
+ self.logger.error(f"TypeError in prune_containers: {str(e)}")
601
+ self.logger.error(f"Traceback: {traceback.format_exc()}")
602
+ self.log_action("prune_containers", params, error=e)
603
+ raise RuntimeError(f"Failed to prune containers: {str(e)}")
604
+ except Exception as e:
605
+ self.logger.error(
606
+ f"Unexpected exception in prune_containers: {type(e).__name__}: {str(e)}"
607
+ )
608
+ self.logger.error(f"Traceback: {traceback.format_exc()}")
609
+ self.log_action("prune_containers", params, error=e)
610
+ raise RuntimeError(f"Failed to prune containers: {str(e)}")
611
+
578
612
  def get_container_logs(self, container_id: str, tail: str = "all") -> str:
579
613
  params = {"container_id": container_id, "tail": tail}
580
614
  try:
@@ -660,7 +694,6 @@ class DockerManager(ContainerManagerBase):
660
694
  params = {"force": force, "all": all}
661
695
  try:
662
696
  if all:
663
- # Remove all volumes (equivalent to --all, but docker doesn't have --all for prune; we list and remove)
664
697
  volumes = self.client.volumes.list(all=True)
665
698
  removed = []
666
699
  for v in volumes:
@@ -668,20 +701,25 @@ class DockerManager(ContainerManagerBase):
668
701
  v.remove(force=force)
669
702
  removed.append(v.attrs["Name"])
670
703
  except Exception as e:
671
- logging.info(f"Info: {e}")
672
- pass
704
+ self.logger.info(
705
+ f"Info: Failed to remove volume {v.attrs.get('Name', 'unknown')}: {e}"
706
+ )
707
+ continue
673
708
  result = {
674
709
  "volumes_removed": removed,
675
710
  "space_reclaimed": "N/A (all volumes)",
676
711
  }
677
712
  else:
678
713
  result = self.client.volumes.prune()
714
+ if result is None:
715
+ result = {"SpaceReclaimed": 0, "VolumesDeleted": []}
716
+ self.logger.debug(f"Raw prune_volumes result: {result}")
679
717
  pruned = {
680
- "space_reclaimed": self._format_size(result["SpaceReclaimed"]),
718
+ "space_reclaimed": self._format_size(
719
+ result.get("SpaceReclaimed", 0)
720
+ ),
681
721
  "volumes_removed": (
682
- [v["Name"] for v in result["VolumesRemoved"]]
683
- if result["VolumesRemoved"]
684
- else []
722
+ [v["Name"] for v in result.get("VolumesDeleted", [])]
685
723
  ),
686
724
  }
687
725
  result = pruned
@@ -691,46 +729,81 @@ class DockerManager(ContainerManagerBase):
691
729
  self.log_action("prune_volumes", params, error=e)
692
730
  raise RuntimeError(f"Failed to prune volumes: {str(e)}")
693
731
 
694
- def remove_network(self, network_id: str) -> Dict:
695
- params = {"network_id": network_id}
732
+ def list_networks(self) -> List[Dict]:
733
+ params = {}
696
734
  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)}")
735
+ networks = self.client.networks.list()
736
+ result = []
737
+ for net in networks:
738
+ attrs = net.attrs
739
+ containers = len(attrs.get("Containers", {}))
740
+ created = attrs.get("Created", None)
741
+ created_str = self._parse_timestamp(created)
742
+ simplified = {
743
+ "id": attrs.get("Id", "unknown")[7:19],
744
+ "name": attrs.get("Name", "unknown"),
745
+ "driver": attrs.get("Driver", "unknown"),
746
+ "scope": attrs.get("Scope", "unknown"),
747
+ "containers": containers,
748
+ "created": created_str,
749
+ }
750
+ result.append(simplified)
751
+ self.log_action("list_networks", params, result)
752
+ return result
753
+ except Exception as e:
754
+ self.log_action("list_networks", params, error=e)
755
+ raise RuntimeError(f"Failed to list networks: {str(e)}")
705
756
 
706
- def prune_system(self, force: bool = False, all: bool = False) -> Dict:
707
- params = {"force": force, "all": all}
757
+ def create_network(self, name: str, driver: str = "bridge") -> Dict:
758
+ params = {"name": name, "driver": driver}
708
759
  try:
709
- filters = {"until": None} if all else {}
710
- result = self.client.system.prune(filters=filters, space=True)
760
+ network = self.client.networks.create(name, driver=driver)
761
+ attrs = network.attrs
762
+ created = attrs.get("Created", None)
763
+ created_str = self._parse_timestamp(created)
764
+ result = {
765
+ "id": attrs.get("Id", "unknown")[7:19],
766
+ "name": attrs.get("Name", name),
767
+ "driver": attrs.get("Driver", driver),
768
+ "scope": attrs.get("Scope", "unknown"),
769
+ "created": created_str,
770
+ }
771
+ self.log_action("create_network", params, result)
772
+ return result
773
+ except Exception as e:
774
+ self.log_action("create_network", params, error=e)
775
+ raise RuntimeError(f"Failed to create network: {str(e)}")
776
+
777
+ def remove_network(self, network_id: str) -> Dict:
778
+ params = {"network_id": network_id}
779
+ try:
780
+ network = self.client.networks.get(network_id)
781
+ network.remove()
782
+ result = {"removed": network_id}
783
+ self.log_action("remove_network", params, result)
784
+ return result
785
+ except Exception as e:
786
+ self.log_action("remove_network", params, error=e)
787
+ raise RuntimeError(f"Failed to remove network: {str(e)}")
788
+
789
+ def prune_networks(self) -> Dict:
790
+ params = {}
791
+ try:
792
+ result = self.client.networks.prune()
793
+ if result is None:
794
+ result = {"SpaceReclaimed": 0, "NetworksDeleted": []}
795
+ self.logger.debug(f"Raw prune_networks result: {result}")
711
796
  pruned = {
712
- "space_reclaimed": self._format_size(result["SpaceReclaimed"]),
713
- "images_removed": (
714
- [img["Id"][7:19] for img in result["ImagesDeleted"]]
715
- if "ImagesDeleted" in result
716
- else []
717
- ),
718
- "containers_removed": (
719
- [c["Id"][7:19] for c in result["ContainersDeleted"]]
720
- if "ContainersDeleted" in result
721
- else []
722
- ),
723
- "volumes_removed": (
724
- [v["Name"] for v in result["VolumesDeleted"]]
725
- if "VolumesDeleted" in result
726
- else []
797
+ "space_reclaimed": self._format_size(result.get("SpaceReclaimed", 0)),
798
+ "networks_removed": (
799
+ [n["Id"][7:19] for n in result.get("NetworksDeleted", [])]
727
800
  ),
728
801
  }
729
- self.log_action("prune_system", params, pruned)
802
+ self.log_action("prune_networks", params, pruned)
730
803
  return pruned
731
804
  except Exception as e:
732
- self.log_action("prune_system", params, error=e)
733
- raise RuntimeError(f"Failed to prune system: {str(e)}")
805
+ self.log_action("prune_networks", params, error=e)
806
+ raise RuntimeError(f"Failed to prune networks: {str(e)}")
734
807
 
735
808
  def compose_up(
736
809
  self, compose_file: str, detach: bool = True, build: bool = False
@@ -957,17 +1030,14 @@ class DockerManager(ContainerManagerBase):
957
1030
  class PodmanManager(ContainerManagerBase):
958
1031
  def __init__(self, silent: bool = False, log_file: Optional[str] = None):
959
1032
  super().__init__(silent, log_file)
960
-
961
1033
  if PodmanClient is None:
962
1034
  raise ImportError("Please install podman-py: pip install podman")
963
-
964
1035
  base_url = self._autodetect_podman_url()
965
1036
  if base_url is None:
966
1037
  self.logger.error(
967
1038
  "No valid Podman socket found after trying all known locations"
968
1039
  )
969
1040
  raise RuntimeError("Failed to connect to Podman: No valid socket found")
970
-
971
1041
  try:
972
1042
  self.client = PodmanClient(base_url=base_url)
973
1043
  self.logger.info(f"Connected to Podman with base_url: {base_url}")
@@ -1002,7 +1072,6 @@ class PodmanManager(ContainerManagerBase):
1002
1072
  """Attempt to connect to Podman with the given base_url."""
1003
1073
  try:
1004
1074
  client = PodmanClient(base_url=base_url)
1005
- # Test connection
1006
1075
  client.version()
1007
1076
  return client
1008
1077
  except PodmanError as e:
@@ -1011,52 +1080,242 @@ class PodmanManager(ContainerManagerBase):
1011
1080
 
1012
1081
  def _autodetect_podman_url(self) -> Optional[str]:
1013
1082
  """Autodetect the appropriate Podman socket URL based on platform."""
1014
- # Check for environment variable override
1015
- base_url = os.environ.get("PODMAN_BASE_URL")
1083
+ base_url = os.environ.get("CONTAINER_MANAGER_PODMAN_BASE_URL")
1016
1084
  if base_url:
1017
- self.logger.info(f"Using PODMAN_BASE_URL from environment: {base_url}")
1085
+ self.logger.info(
1086
+ f"Using CONTAINER_MANAGER_PODMAN_BASE_URL from environment: {base_url}"
1087
+ )
1018
1088
  return base_url
1019
-
1020
1089
  system = platform.system()
1021
1090
  is_wsl = self._is_wsl()
1022
-
1023
- # Define socket candidates based on platform
1024
1091
  socket_candidates = []
1025
1092
  if system == "Windows" and not is_wsl:
1026
- # Windows with Podman machine
1027
- if self._is_podman_machine_running():
1028
- socket_candidates.append("npipe:////./pipe/docker_engine")
1029
- # Fallback to WSL2 distro sockets if running in a mixed setup
1093
+ if not self._is_podman_machine_running():
1094
+ raise RuntimeError("Podman Machine is not running on Windows system")
1030
1095
  socket_candidates.extend(
1031
1096
  [
1032
- "unix:///mnt/wsl/podman-sockets/podman-machine-default/podman-user.sock", # Rootless
1033
- "unix:///mnt/wsl/podman-sockets/podman-machine-default/podman-root.sock", # Rootful
1097
+ "tcp://127.0.0.1:8080",
1098
+ "unix:///run/podman/podman.sock",
1099
+ "npipe:////./pipe/docker_engine",
1100
+ "unix:///mnt/wsl/podman-sockets/podman-machine-default/podman-user.sock",
1101
+ "unix:///mnt/wsl/podman-sockets/podman-machine-default/podman-root.sock",
1034
1102
  ]
1035
1103
  )
1036
1104
  elif system == "Linux" or is_wsl:
1037
- # Linux or WSL2 distro: prioritize rootless, then rootful
1038
1105
  uid = os.getuid()
1039
1106
  socket_candidates.extend(
1040
1107
  [
1041
- f"unix:///run/user/{uid}/podman/podman.sock", # Rootless
1042
- "unix:///run/podman/podman.sock", # Rootful
1108
+ f"unix:///run/user/{uid}/podman/podman.sock",
1109
+ "unix:///run/podman/podman.sock",
1110
+ "unix:///mnt/wsl/podman-sockets/podman-machine-default/podman-user.sock",
1111
+ "unix:///mnt/wsl/podman-sockets/podman-machine-default/podman-root.sock",
1043
1112
  ]
1044
1113
  )
1045
-
1046
- # Try each socket candidate
1047
1114
  for url in socket_candidates:
1048
- # For Unix sockets, check if the file exists (on Linux/WSL2)
1049
- if url.startswith("unix://") and (system == "Linux" or is_wsl):
1050
- socket_path = url.replace("unix://", "")
1051
- if not os.path.exists(socket_path):
1052
- self.logger.debug(f"Socket {socket_path} does not exist")
1053
- continue
1054
1115
  client = self._try_connect(url)
1055
1116
  if client:
1056
1117
  return url
1057
-
1058
1118
  return None
1059
1119
 
1120
+ def prune_images(self, force: bool = False, all: bool = False) -> Dict:
1121
+ params = {"force": force, "all": all}
1122
+ try:
1123
+ if all:
1124
+ # Manually remove all unused images
1125
+ images = self.client.images.list(all=True)
1126
+ removed = []
1127
+ for img in images:
1128
+ try:
1129
+ for tag in img.attrs.get("Names", []):
1130
+ self.client.images.remove(tag, force=force)
1131
+ removed.append(img.attrs["Id"][7:19])
1132
+ except Exception as e:
1133
+ self.logger.info(
1134
+ f"Info: Failed to remove image {img.attrs.get('Id', 'unknown')}: {e}"
1135
+ )
1136
+ continue
1137
+ result = {
1138
+ "images_removed": removed,
1139
+ "space_reclaimed": "N/A (all images)",
1140
+ }
1141
+ else:
1142
+ filters = {"dangling": True} if not all else {}
1143
+ result = self.client.images.prune(filters=filters)
1144
+ if result is None:
1145
+ result = {"SpaceReclaimed": 0, "ImagesRemoved": []}
1146
+ self.logger.debug(f"Raw prune_images result: {result}")
1147
+ pruned = {
1148
+ "space_reclaimed": self._format_size(
1149
+ result.get("SpaceReclaimed", 0)
1150
+ ),
1151
+ "images_removed": (
1152
+ [img["Id"][7:19] for img in result.get("ImagesRemoved", [])]
1153
+ or [img["Id"][7:19] for img in result.get("ImagesDeleted", [])]
1154
+ ),
1155
+ }
1156
+ result = pruned
1157
+ self.log_action("prune_images", params, result)
1158
+ return result
1159
+ except Exception as e:
1160
+ self.log_action("prune_images", params, error=e)
1161
+ raise RuntimeError(f"Failed to prune images: {str(e)}")
1162
+
1163
+ def prune_containers(self) -> Dict:
1164
+ params = {}
1165
+ try:
1166
+ result = self.client.containers.prune()
1167
+ self.logger.debug(f"Raw prune_containers result: {result}")
1168
+ if result is None:
1169
+ result = {"SpaceReclaimed": 0, "ContainersDeleted": []}
1170
+ pruned = {
1171
+ "space_reclaimed": self._format_size(result.get("SpaceReclaimed", 0)),
1172
+ "containers_removed": (
1173
+ [c["Id"][7:19] for c in result.get("ContainersDeleted", [])]
1174
+ or [c["Id"][7:19] for c in result.get("ContainersRemoved", [])]
1175
+ ),
1176
+ }
1177
+ self.log_action("prune_containers", params, pruned)
1178
+ return pruned
1179
+ except PodmanError as e:
1180
+ self.logger.error(f"PodmanError in prune_containers: {str(e)}")
1181
+ self.logger.error(f"Traceback: {traceback.format_exc()}")
1182
+ self.log_action("prune_containers", params, error=e)
1183
+ raise RuntimeError(f"Failed to prune containers: {str(e)}")
1184
+ except Exception as e:
1185
+ self.logger.error(
1186
+ f"Unexpected exception in prune_containers: {type(e).__name__}: {str(e)}"
1187
+ )
1188
+ self.logger.error(f"Traceback: {traceback.format_exc()}")
1189
+ self.log_action("prune_containers", params, error=e)
1190
+ raise RuntimeError(f"Failed to prune containers: {str(e)}")
1191
+
1192
+ def prune_volumes(self, force: bool = False, all: bool = False) -> Dict:
1193
+ params = {"force": force, "all": all}
1194
+ try:
1195
+ if all:
1196
+ volumes = self.client.volumes.list(all=True)
1197
+ removed = []
1198
+ for v in volumes:
1199
+ try:
1200
+ v.remove(force=force)
1201
+ removed.append(v.attrs["Name"])
1202
+ except Exception as e:
1203
+ self.logger.info(
1204
+ f"Info: Failed to remove volume {v.attrs.get('Name', 'unknown')}: {e}"
1205
+ )
1206
+ continue
1207
+ result = {
1208
+ "volumes_removed": removed,
1209
+ "space_reclaimed": "N/A (all volumes)",
1210
+ }
1211
+ else:
1212
+ result = self.client.volumes.prune()
1213
+ if result is None:
1214
+ result = {"SpaceReclaimed": 0, "VolumesRemoved": []}
1215
+ self.logger.debug(f"Raw prune_volumes result: {result}")
1216
+ pruned = {
1217
+ "space_reclaimed": self._format_size(
1218
+ result.get("SpaceReclaimed", 0)
1219
+ ),
1220
+ "volumes_removed": (
1221
+ [v["Name"] for v in result.get("VolumesRemoved", [])]
1222
+ or [v["Name"] for v in result.get("VolumesDeleted", [])]
1223
+ ),
1224
+ }
1225
+ result = pruned
1226
+ self.log_action("prune_volumes", params, result)
1227
+ return result
1228
+ except Exception as e:
1229
+ self.log_action("prune_volumes", params, error=e)
1230
+ raise RuntimeError(f"Failed to prune volumes: {str(e)}")
1231
+
1232
+ def prune_networks(self) -> Dict:
1233
+ params = {}
1234
+ try:
1235
+ result = self.client.networks.prune()
1236
+ if result is None:
1237
+ result = {"SpaceReclaimed": 0, "NetworksRemoved": []}
1238
+ self.logger.debug(f"Raw prune_networks result: {result}")
1239
+ pruned = {
1240
+ "space_reclaimed": self._format_size(result.get("SpaceReclaimed", 0)),
1241
+ "networks_removed": (
1242
+ [n["Id"][7:19] for n in result.get("NetworksRemoved", [])]
1243
+ or [n["Id"][7:19] for n in result.get("NetworksDeleted", [])]
1244
+ ),
1245
+ }
1246
+ self.log_action("prune_networks", params, pruned)
1247
+ return pruned
1248
+ except Exception as e:
1249
+ self.log_action("prune_networks", params, error=e)
1250
+ raise RuntimeError(f"Failed to prune networks: {str(e)}")
1251
+
1252
+ def prune_system(self, force: bool = False, all: bool = False) -> Dict:
1253
+ params = {"force": force, "all": all}
1254
+ try:
1255
+ cmd = (
1256
+ ["podman", "system", "prune", "--force"]
1257
+ if force
1258
+ else ["podman", "system", "prune"]
1259
+ )
1260
+ if all:
1261
+ cmd.append("--all")
1262
+ if all: # Include volumes if all=True
1263
+ cmd.append("--volumes")
1264
+ result = subprocess.run(cmd, capture_output=True, text=True)
1265
+ if result.returncode != 0:
1266
+ raise RuntimeError(result.stderr)
1267
+ self.logger.debug(f"Raw prune_system result: {result.stdout}")
1268
+ pruned = {
1269
+ "output": result.stdout.strip(),
1270
+ "space_reclaimed": "Check output",
1271
+ "images_removed": [], # Podman CLI doesn't provide detailed breakdown
1272
+ "containers_removed": [],
1273
+ "volumes_removed": [],
1274
+ "networks_removed": [],
1275
+ }
1276
+ self.log_action("prune_system", params, pruned)
1277
+ return pruned
1278
+ except Exception as e:
1279
+ self.log_action("prune_system", params, error=e)
1280
+ raise RuntimeError(f"Failed to prune system: {str(e)}")
1281
+
1282
+ def get_version(self) -> Dict:
1283
+ params = {}
1284
+ try:
1285
+ version = self.client.version()
1286
+ result = {
1287
+ "version": version.get("Version", "unknown"),
1288
+ "api_version": version.get("APIVersion", "unknown"),
1289
+ "os": version.get("Os", "unknown"),
1290
+ "arch": version.get("Arch", "unknown"),
1291
+ "build_time": version.get("BuildTime", "unknown"),
1292
+ }
1293
+ self.log_action("get_version", params, result)
1294
+ return result
1295
+ except Exception as e:
1296
+ self.log_action("get_version", params, error=e)
1297
+ raise RuntimeError(f"Failed to get version: {str(e)}")
1298
+
1299
+ def get_info(self) -> Dict:
1300
+ params = {}
1301
+ try:
1302
+ info = self.client.info()
1303
+ host = info.get("host", {})
1304
+ result = {
1305
+ "containers_total": info.get("store", {}).get("containers", 0),
1306
+ "containers_running": host.get("runningContainers", 0),
1307
+ "images": info.get("store", {}).get("images", 0),
1308
+ "driver": host.get("graphDriverName", "unknown"),
1309
+ "platform": f"{host.get('os', 'unknown')} {host.get('arch', 'unknown')}",
1310
+ "memory_total": self._format_size(host.get("memTotal", 0)),
1311
+ "swap_total": self._format_size(host.get("swapTotal", 0)),
1312
+ }
1313
+ self.log_action("get_info", params, result)
1314
+ return result
1315
+ except Exception as e:
1316
+ self.log_action("get_info", params, error=e)
1317
+ raise RuntimeError(f"Failed to get info: {str(e)}")
1318
+
1060
1319
  def list_images(self) -> List[Dict]:
1061
1320
  params = {}
1062
1321
  try:
@@ -1122,22 +1381,16 @@ class PodmanManager(ContainerManagerBase):
1122
1381
  self.log_action("pull_image", params, error=e)
1123
1382
  raise RuntimeError(f"Failed to pull image: {str(e)}")
1124
1383
 
1125
- def prune_images(self, force: bool = False, all: bool = False) -> Dict:
1126
- params = {"force": force, "all": all}
1384
+ def remove_image(self, image: str, force: bool = False) -> Dict:
1385
+ params = {"image": image, "force": force}
1127
1386
  try:
1128
- filters = {"dangling": True} if not all else {}
1129
- result = self.client.images.prune(filters=filters)
1130
- pruned = {
1131
- "space_reclaimed": self._format_size(result.get("SpaceReclaimed", 0)),
1132
- "images_removed": [
1133
- img["Id"][7:19] for img in result.get("ImagesRemoved", [])
1134
- ],
1135
- }
1136
- self.log_action("prune_images", params, pruned)
1137
- return pruned
1387
+ self.client.images.remove(image, force=force)
1388
+ result = {"removed": image}
1389
+ self.log_action("remove_image", params, result)
1390
+ return result
1138
1391
  except Exception as e:
1139
- self.log_action("prune_images", params, error=e)
1140
- raise RuntimeError(f"Failed to prune images: {str(e)}")
1392
+ self.log_action("remove_image", params, error=e)
1393
+ raise RuntimeError(f"Failed to remove image: {str(e)}")
1141
1394
 
1142
1395
  def list_containers(self, all: bool = False) -> List[Dict]:
1143
1396
  params = {"all": all}
@@ -1203,12 +1456,15 @@ class PodmanManager(ContainerManagerBase):
1203
1456
  self.log_action("run_container", params, result)
1204
1457
  return result
1205
1458
  attrs = container.attrs
1206
- ports = attrs.get("Ports", [])
1207
- port_mappings = [
1208
- f"{p.get('host_ip', '0.0.0.0')}:{p.get('host_port')}->{p.get('container_port')}/{p.get('protocol', 'tcp')}"
1209
- for p in ports
1210
- if p.get("host_port")
1211
- ]
1459
+ port_mappings = []
1460
+ if ports: # Check if ports is not None
1461
+ container_ports = attrs.get("Ports", [])
1462
+ if container_ports: # Check if Ports list is not empty
1463
+ port_mappings = [
1464
+ f"{p.get('host_ip', '0.0.0.0')}:{p.get('host_port')}->{p.get('container_port')}/{p.get('protocol', 'tcp')}"
1465
+ for p in container_ports
1466
+ if p.get("host_port")
1467
+ ]
1212
1468
  created = attrs.get("Created", None)
1213
1469
  created_str = self._parse_timestamp(created)
1214
1470
  result = {
@@ -1225,131 +1481,6 @@ class PodmanManager(ContainerManagerBase):
1225
1481
  self.log_action("run_container", params, error=e)
1226
1482
  raise RuntimeError(f"Failed to run container: {str(e)}")
1227
1483
 
1228
- def prune_containers(self, force: bool = False) -> Dict:
1229
- params = {"force": force}
1230
- try:
1231
- result = self.client.containers.prune()
1232
- pruned = {
1233
- "space_reclaimed": self._format_size(result.get("SpaceReclaimed", 0)),
1234
- "containers_removed": [
1235
- c["Id"][7:19] for c in result.get("ContainersRemoved", [])
1236
- ],
1237
- }
1238
- self.log_action("prune_containers", params, pruned)
1239
- return pruned
1240
- except Exception as e:
1241
- self.log_action("prune_containers", params, error=e)
1242
- raise RuntimeError(f"Failed to prune containers: {str(e)}")
1243
-
1244
- def list_networks(self) -> List[Dict]:
1245
- params = {}
1246
- try:
1247
- networks = self.client.networks.list()
1248
- result = []
1249
- for net in networks:
1250
- attrs = net.attrs
1251
- containers = len(attrs.get("Containers", {}))
1252
- created = attrs.get("Created", None)
1253
- created_str = self._parse_timestamp(created)
1254
- simplified = {
1255
- "id": attrs.get("Id", "unknown")[7:19],
1256
- "name": attrs.get("Name", "unknown"),
1257
- "driver": attrs.get("Driver", "unknown"),
1258
- "scope": attrs.get("Scope", "unknown"),
1259
- "containers": containers,
1260
- "created": created_str,
1261
- }
1262
- result.append(simplified)
1263
- self.log_action("list_networks", params, result)
1264
- return result
1265
- except Exception as e:
1266
- self.log_action("list_networks", params, error=e)
1267
- raise RuntimeError(f"Failed to list networks: {str(e)}")
1268
-
1269
- def create_network(self, name: str, driver: str = "bridge") -> Dict:
1270
- params = {"name": name, "driver": driver}
1271
- try:
1272
- network = self.client.networks.create(name, driver=driver)
1273
- attrs = network.attrs
1274
- created = attrs.get("Created", None)
1275
- created_str = self._parse_timestamp(created)
1276
- result = {
1277
- "id": attrs.get("Id", "unknown")[7:19],
1278
- "name": attrs.get("Name", name),
1279
- "driver": attrs.get("Driver", driver),
1280
- "scope": attrs.get("Scope", "unknown"),
1281
- "created": created_str,
1282
- }
1283
- self.log_action("create_network", params, result)
1284
- return result
1285
- except Exception as e:
1286
- self.log_action("create_network", params, error=e)
1287
- raise RuntimeError(f"Failed to create network: {str(e)}")
1288
-
1289
- def prune_networks(self) -> Dict:
1290
- params = {}
1291
- try:
1292
- result = self.client.networks.prune()
1293
- pruned = {
1294
- "space_reclaimed": self._format_size(result.get("SpaceReclaimed", 0)),
1295
- "networks_removed": [
1296
- n["Id"][7:19] for n in result.get("NetworksRemoved", [])
1297
- ],
1298
- }
1299
- self.log_action("prune_networks", params, pruned)
1300
- return pruned
1301
- except Exception as e:
1302
- self.log_action("prune_networks", params, error=e)
1303
- raise RuntimeError(f"Failed to prune networks: {str(e)}")
1304
-
1305
- def get_version(self) -> Dict:
1306
- params = {}
1307
- try:
1308
- version = self.client.version()
1309
- result = {
1310
- "version": version.get("Version", "unknown"),
1311
- "api_version": version.get("APIVersion", "unknown"),
1312
- "os": version.get("Os", "unknown"),
1313
- "arch": version.get("Arch", "unknown"),
1314
- "build_time": version.get("BuildTime", "unknown"),
1315
- }
1316
- self.log_action("get_version", params, result)
1317
- return result
1318
- except Exception as e:
1319
- self.log_action("get_version", params, error=e)
1320
- raise RuntimeError(f"Failed to get version: {str(e)}")
1321
-
1322
- def get_info(self) -> Dict:
1323
- params = {}
1324
- try:
1325
- info = self.client.info()
1326
- host = info.get("host", {})
1327
- result = {
1328
- "containers_total": info.get("store", {}).get("containers", 0),
1329
- "containers_running": host.get("runningContainers", 0),
1330
- "images": info.get("store", {}).get("images", 0),
1331
- "driver": host.get("graphDriverName", "unknown"),
1332
- "platform": f"{host.get('os', 'unknown')} {host.get('arch', 'unknown')}",
1333
- "memory_total": self._format_size(host.get("memTotal", 0)),
1334
- "swap_total": self._format_size(host.get("swapTotal", 0)),
1335
- }
1336
- self.log_action("get_info", params, result)
1337
- return result
1338
- except Exception as e:
1339
- self.log_action("get_info", params, error=e)
1340
- raise RuntimeError(f"Failed to get info: {str(e)}")
1341
-
1342
- def remove_image(self, image: str, force: bool = False) -> Dict:
1343
- params = {"image": image, "force": force}
1344
- try:
1345
- self.client.images.remove(image, force=force)
1346
- result = {"removed": image}
1347
- self.log_action("remove_image", params, result)
1348
- return result
1349
- except Exception as e:
1350
- self.log_action("remove_image", params, error=e)
1351
- raise RuntimeError(f"Failed to remove image: {str(e)}")
1352
-
1353
1484
  def stop_container(self, container_id: str, timeout: int = 10) -> Dict:
1354
1485
  params = {"container_id": container_id, "timeout": timeout}
1355
1486
  try:
@@ -1455,40 +1586,50 @@ class PodmanManager(ContainerManagerBase):
1455
1586
  self.log_action("remove_volume", params, error=e)
1456
1587
  raise RuntimeError(f"Failed to remove volume: {str(e)}")
1457
1588
 
1458
- def prune_volumes(self, force: bool = False, all: bool = False) -> Dict:
1459
- params = {"force": force, "all": all}
1589
+ def list_networks(self) -> List[Dict]:
1590
+ params = {}
1460
1591
  try:
1461
- if all:
1462
- # Remove all volumes
1463
- volumes = self.client.volumes.list(all=True)
1464
- removed = []
1465
- for v in volumes:
1466
- try:
1467
- v.remove(force=force)
1468
- removed.append(v.attrs["Name"])
1469
- except Exception as e:
1470
- logging.info(f"Info: {e}")
1471
- pass
1472
- result = {
1473
- "volumes_removed": removed,
1474
- "space_reclaimed": "N/A (all volumes)",
1475
- }
1476
- else:
1477
- result = self.client.volumes.prune()
1478
- pruned = {
1479
- "space_reclaimed": self._format_size(
1480
- result.get("SpaceReclaimed", 0)
1481
- ),
1482
- "volumes_removed": [
1483
- v["Name"] for v in result.get("VolumesRemoved", [])
1484
- ],
1592
+ networks = self.client.networks.list()
1593
+ result = []
1594
+ for net in networks:
1595
+ attrs = net.attrs
1596
+ containers = len(attrs.get("Containers", {}))
1597
+ created = attrs.get("Created", None)
1598
+ created_str = self._parse_timestamp(created)
1599
+ simplified = {
1600
+ "id": attrs.get("Id", "unknown")[7:19],
1601
+ "name": attrs.get("Name", "unknown"),
1602
+ "driver": attrs.get("Driver", "unknown"),
1603
+ "scope": attrs.get("Scope", "unknown"),
1604
+ "containers": containers,
1605
+ "created": created_str,
1485
1606
  }
1486
- result = pruned
1487
- self.log_action("prune_volumes", params, result)
1607
+ result.append(simplified)
1608
+ self.log_action("list_networks", params, result)
1488
1609
  return result
1489
1610
  except Exception as e:
1490
- self.log_action("prune_volumes", params, error=e)
1491
- raise RuntimeError(f"Failed to prune volumes: {str(e)}")
1611
+ self.log_action("list_networks", params, error=e)
1612
+ raise RuntimeError(f"Failed to list networks: {str(e)}")
1613
+
1614
+ def create_network(self, name: str, driver: str = "bridge") -> Dict:
1615
+ params = {"name": name, "driver": driver}
1616
+ try:
1617
+ network = self.client.networks.create(name, driver=driver)
1618
+ attrs = network.attrs
1619
+ created = attrs.get("Created", None)
1620
+ created_str = self._parse_timestamp(created)
1621
+ result = {
1622
+ "id": attrs.get("Id", "unknown")[7:19],
1623
+ "name": attrs.get("Name", name),
1624
+ "driver": attrs.get("Driver", driver),
1625
+ "scope": attrs.get("Scope", "unknown"),
1626
+ "created": created_str,
1627
+ }
1628
+ self.log_action("create_network", params, result)
1629
+ return result
1630
+ except Exception as e:
1631
+ self.log_action("create_network", params, error=e)
1632
+ raise RuntimeError(f"Failed to create network: {str(e)}")
1492
1633
 
1493
1634
  def remove_network(self, network_id: str) -> Dict:
1494
1635
  params = {"network_id": network_id}
@@ -1502,26 +1643,6 @@ class PodmanManager(ContainerManagerBase):
1502
1643
  self.log_action("remove_network", params, error=e)
1503
1644
  raise RuntimeError(f"Failed to remove network: {str(e)}")
1504
1645
 
1505
- def prune_system(self, force: bool = False, all: bool = False) -> Dict:
1506
- params = {"force": force, "all": all}
1507
- try:
1508
- # Podman system prune uses CLI, as podman-py may not have direct support
1509
- cmd = ["podman", "system", "prune"]
1510
- if all:
1511
- cmd.append("--all")
1512
- result = subprocess.run(cmd, capture_output=True, text=True)
1513
- if result.returncode != 0:
1514
- raise RuntimeError(result.stderr)
1515
- pruned = {
1516
- "output": result.stdout.strip(),
1517
- "space_reclaimed": "Check output",
1518
- }
1519
- self.log_action("prune_system", params, pruned)
1520
- return pruned
1521
- except Exception as e:
1522
- self.log_action("prune_system", params, error=e)
1523
- raise RuntimeError(f"Failed to prune system: {str(e)}")
1524
-
1525
1646
  def compose_up(
1526
1647
  self, compose_file: str, detach: bool = True, build: bool = False
1527
1648
  ) -> str:
@@ -1608,27 +1729,20 @@ class PodmanManager(ContainerManagerBase):
1608
1729
  raise NotImplementedError("Swarm not supported in Podman")
1609
1730
 
1610
1731
 
1732
+ def is_app_installed(app_name: str = "docker") -> bool:
1733
+ return shutil.which(app_name.lower()) is not None
1734
+
1735
+
1611
1736
  def create_manager(
1612
1737
  manager_type: Optional[str] = None, silent: bool = False, log_file: str = None
1613
1738
  ) -> ContainerManagerBase:
1614
1739
  if manager_type is None:
1615
- manager_type = os.environ.get("CONTAINER_MANAGER_TYPE")
1740
+ manager_type = os.environ.get("CONTAINER_MANAGER_TYPE", None)
1616
1741
  if manager_type is None:
1617
- # Autodetect
1618
- if PodmanClient is not None:
1619
- try:
1620
- test_client = PodmanClient()
1621
- test_client.close()
1622
- manager_type = "podman"
1623
- except Exception:
1624
- pass
1625
- if manager_type is None and docker is not None:
1626
- try:
1627
- test_client = docker.from_env()
1628
- test_client.close()
1629
- manager_type = "docker"
1630
- except Exception:
1631
- pass
1742
+ if is_app_installed("podman"):
1743
+ manager_type = "podman"
1744
+ if is_app_installed("docker"):
1745
+ manager_type = "docker"
1632
1746
  if manager_type is None:
1633
1747
  raise ValueError(
1634
1748
  "No supported container manager detected. Set CONTAINER_MANAGER_TYPE or install Docker/Podman."
@@ -1721,7 +1835,7 @@ container_manager.py --manager docker --pull-image nginx --tag latest --list-con
1721
1835
  )
1722
1836
 
1723
1837
 
1724
- def container_manager(argv):
1838
+ def container_manager():
1725
1839
  parser = argparse.ArgumentParser(
1726
1840
  description="Container Manager: A tool to manage containers with Docker, Podman, and Docker Swarm!"
1727
1841
  )
@@ -1826,7 +1940,7 @@ def container_manager(argv):
1826
1940
  parser.add_argument("--force", action="store_true", help="Force removal")
1827
1941
  parser.add_argument("-h", "--help", action="store_true", help="Show help")
1828
1942
 
1829
- args = parser.parse_args(argv)
1943
+ args = parser.parse_args()
1830
1944
 
1831
1945
  if args.help:
1832
1946
  usage()
@@ -1980,7 +2094,7 @@ def container_manager(argv):
1980
2094
  )
1981
2095
 
1982
2096
  if prune_containers:
1983
- print(json.dumps(manager.prune_containers(force), indent=2))
2097
+ print(json.dumps(manager.prune_containers(), indent=2))
1984
2098
 
1985
2099
  if get_container_logs:
1986
2100
  if not container_logs_id:
@@ -2028,7 +2142,7 @@ def container_manager(argv):
2028
2142
  print(json.dumps(manager.remove_network(remove_network_id), indent=2))
2029
2143
 
2030
2144
  if prune_networks:
2031
- print(json.dumps(manager.prune_networks(force), indent=2))
2145
+ print(json.dumps(manager.prune_networks(), indent=2))
2032
2146
 
2033
2147
  if prune_system:
2034
2148
  print(json.dumps(manager.prune_system(force, prune_system_all), indent=2))
@@ -2100,4 +2214,4 @@ if __name__ == "__main__":
2100
2214
  if len(sys.argv) < 2:
2101
2215
  usage()
2102
2216
  sys.exit(2)
2103
- container_manager(sys.argv[1:])
2217
+ container_manager()