container-manager-mcp 0.0.11__py3-none-any.whl → 1.0.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.
@@ -6,10 +6,12 @@ import logging
6
6
  import os
7
7
  from abc import ABC, abstractmethod
8
8
  from typing import List, Dict, Optional, Any
9
- import getopt
9
+ import argparse
10
10
  import json
11
11
  import subprocess
12
- from datetime import datetime # Added for consistent timestamp formatting
12
+ from datetime import datetime
13
+ import dateutil.parser
14
+ import platform
13
15
 
14
16
  try:
15
17
  import docker
@@ -27,6 +29,7 @@ except ImportError:
27
29
 
28
30
 
29
31
  class ContainerManagerBase(ABC):
32
+
30
33
  def __init__(self, silent: bool = False, log_file: str = None):
31
34
  self.silent = silent
32
35
  self.setup_logging(log_file)
@@ -66,6 +69,20 @@ class ContainerManagerBase(ABC):
66
69
  size_bytes /= 1024.0
67
70
  return f"{size_bytes:.2f}PB"
68
71
 
72
+ def _parse_timestamp(self, timestamp: Any) -> str:
73
+ """Parse timestamp (integer or string) to ISO 8601 string."""
74
+ if not timestamp:
75
+ return "unknown"
76
+ if isinstance(timestamp, (int, float)):
77
+ return datetime.fromtimestamp(timestamp).strftime("%Y-%m-%dT%H:%M:%S")
78
+ if isinstance(timestamp, str):
79
+ try:
80
+ parsed = dateutil.parser.isoparse(timestamp)
81
+ return parsed.strftime("%Y-%m-%dT%H:%M:%S")
82
+ except ValueError:
83
+ return "unknown"
84
+ return "unknown"
85
+
69
86
  @abstractmethod
70
87
  def get_version(self) -> Dict:
71
88
  pass
@@ -208,42 +225,6 @@ class DockerManager(ContainerManagerBase):
208
225
  self.logger.error(f"Failed to connect to Docker daemon: {str(e)}")
209
226
  raise RuntimeError(f"Failed to connect to Docker: {str(e)}")
210
227
 
211
- def get_version(self) -> Dict:
212
- params = {}
213
- try:
214
- version = self.client.version()
215
- result = {
216
- "version": version.get("Version", "unknown"),
217
- "api_version": version.get("ApiVersion", "unknown"),
218
- "os": version.get("Os", "unknown"),
219
- "arch": version.get("Arch", "unknown"),
220
- "build_time": version.get("BuildTime", "unknown"),
221
- }
222
- self.log_action("get_version", params, result)
223
- return result
224
- except Exception as e:
225
- self.log_action("get_version", params, error=e)
226
- raise RuntimeError(f"Failed to get version: {str(e)}")
227
-
228
- def get_info(self) -> Dict:
229
- params = {}
230
- try:
231
- info = self.client.info()
232
- result = {
233
- "containers_total": info.get("Containers", 0),
234
- "containers_running": info.get("ContainersRunning", 0),
235
- "images": info.get("Images", 0),
236
- "driver": info.get("Driver", "unknown"),
237
- "platform": f"{info.get('OperatingSystem', 'unknown')} {info.get('Architecture', 'unknown')}",
238
- "memory_total": self._format_size(info.get("MemTotal", 0)),
239
- "swap_total": self._format_size(info.get("SwapTotal", 0)),
240
- }
241
- self.log_action("get_info", params, result)
242
- return result
243
- except Exception as e:
244
- self.log_action("get_info", params, error=e)
245
- raise RuntimeError(f"Failed to get info: {str(e)}")
246
-
247
228
  def list_images(self) -> List[Dict]:
248
229
  params = {}
249
230
  try:
@@ -257,12 +238,8 @@ class DockerManager(ContainerManagerBase):
257
238
  repo_tag.rsplit(":", 1) if ":" in repo_tag else ("<none>", "<none>")
258
239
  )
259
240
 
260
- created = attrs.get("Created", 0)
261
- created_str = (
262
- datetime.fromtimestamp(created).strftime("%Y-%m-%dT%H:%M:%S")
263
- if created
264
- else "unknown"
265
- )
241
+ created = attrs.get("Created", None)
242
+ created_str = self._parse_timestamp(created)
266
243
 
267
244
  size_bytes = attrs.get("Size", 0)
268
245
  size_str = self._format_size(size_bytes) if size_bytes else "0B"
@@ -298,12 +275,8 @@ class DockerManager(ContainerManagerBase):
298
275
  repository, tag = (
299
276
  repo_tag.rsplit(":", 1) if ":" in repo_tag else (image, tag)
300
277
  )
301
- created = attrs.get("Created", 0)
302
- created_str = (
303
- datetime.fromtimestamp(created).strftime("%Y-%m-%dT%H:%M:%S")
304
- if created
305
- else "unknown"
306
- )
278
+ created = attrs.get("Created", None)
279
+ created_str = self._parse_timestamp(created)
307
280
  size_bytes = attrs.get("Size", 0)
308
281
  size_str = self._format_size(size_bytes) if size_bytes else "0B"
309
282
  result = {
@@ -321,17 +294,6 @@ class DockerManager(ContainerManagerBase):
321
294
  self.log_action("pull_image", params, error=e)
322
295
  raise RuntimeError(f"Failed to pull image: {str(e)}")
323
296
 
324
- def remove_image(self, image: str, force: bool = False) -> Dict:
325
- params = {"image": image, "force": force}
326
- try:
327
- self.client.images.remove(image, force=force)
328
- result = {"removed": image}
329
- self.log_action("remove_image", params, result)
330
- return result
331
- except Exception as e:
332
- self.log_action("remove_image", params, error=e)
333
- raise RuntimeError(f"Failed to remove image: {str(e)}")
334
-
335
297
  def list_containers(self, all: bool = False) -> List[Dict]:
336
298
  params = {"all": all}
337
299
  try:
@@ -347,12 +309,8 @@ class DockerManager(ContainerManagerBase):
347
309
  port_mappings.append(
348
310
  f"{hp.get('HostIp', '0.0.0.0')}:{hp.get('HostPort')}->{container_port}"
349
311
  )
350
- created = attrs.get("Created", 0)
351
- created_str = (
352
- datetime.fromtimestamp(created).strftime("%Y-%m-%dT%H:%M:%S")
353
- if created
354
- else "unknown"
355
- )
312
+ created = attrs.get("Created", None)
313
+ created_str = self._parse_timestamp(created)
356
314
  simplified = {
357
315
  "id": attrs.get("Id", "unknown")[7:19],
358
316
  "image": attrs.get("Config", {}).get("Image", "unknown"),
@@ -410,12 +368,8 @@ class DockerManager(ContainerManagerBase):
410
368
  port_mappings.append(
411
369
  f"{hp.get('HostIp', '0.0.0.0')}:{hp.get('HostPort')}->{container_port}"
412
370
  )
413
- created = attrs.get("Created", 0)
414
- created_str = (
415
- datetime.fromtimestamp(created).strftime("%Y-%m-%dT%H:%M:%S")
416
- if created
417
- else "unknown"
418
- )
371
+ created = attrs.get("Created", None)
372
+ created_str = self._parse_timestamp(created)
419
373
  result = {
420
374
  "id": attrs.get("Id", "unknown")[7:19],
421
375
  "image": attrs.get("Config", {}).get("Image", image),
@@ -430,6 +384,98 @@ class DockerManager(ContainerManagerBase):
430
384
  self.log_action("run_container", params, error=e)
431
385
  raise RuntimeError(f"Failed to run container: {str(e)}")
432
386
 
387
+ def list_networks(self) -> List[Dict]:
388
+ params = {}
389
+ try:
390
+ networks = self.client.networks.list()
391
+ result = []
392
+ for net in networks:
393
+ attrs = net.attrs
394
+ containers = len(attrs.get("Containers", {}))
395
+ created = attrs.get("Created", None)
396
+ created_str = self._parse_timestamp(created)
397
+ simplified = {
398
+ "id": attrs.get("Id", "unknown")[7:19],
399
+ "name": attrs.get("Name", "unknown"),
400
+ "driver": attrs.get("Driver", "unknown"),
401
+ "scope": attrs.get("Scope", "unknown"),
402
+ "containers": containers,
403
+ "created": created_str,
404
+ }
405
+ result.append(simplified)
406
+ self.log_action("list_networks", params, result)
407
+ return result
408
+ except Exception as e:
409
+ self.log_action("list_networks", params, error=e)
410
+ raise RuntimeError(f"Failed to list networks: {str(e)}")
411
+
412
+ def create_network(self, name: str, driver: str = "bridge") -> Dict:
413
+ params = {"name": name, "driver": driver}
414
+ try:
415
+ network = self.client.networks.create(name, driver=driver)
416
+ attrs = network.attrs
417
+ created = attrs.get("Created", None)
418
+ created_str = self._parse_timestamp(created)
419
+ result = {
420
+ "id": attrs.get("Id", "unknown")[7:19],
421
+ "name": attrs.get("Name", name),
422
+ "driver": attrs.get("Driver", driver),
423
+ "scope": attrs.get("Scope", "unknown"),
424
+ "created": created_str,
425
+ }
426
+ self.log_action("create_network", params, result)
427
+ return result
428
+ except Exception as e:
429
+ self.log_action("create_network", params, error=e)
430
+ raise RuntimeError(f"Failed to create network: {str(e)}")
431
+
432
+ def get_version(self) -> Dict:
433
+ params = {}
434
+ try:
435
+ version = self.client.version()
436
+ result = {
437
+ "version": version.get("Version", "unknown"),
438
+ "api_version": version.get("ApiVersion", "unknown"),
439
+ "os": version.get("Os", "unknown"),
440
+ "arch": version.get("Arch", "unknown"),
441
+ "build_time": version.get("BuildTime", "unknown"),
442
+ }
443
+ self.log_action("get_version", params, result)
444
+ return result
445
+ except Exception as e:
446
+ self.log_action("get_version", params, error=e)
447
+ raise RuntimeError(f"Failed to get version: {str(e)}")
448
+
449
+ def get_info(self) -> Dict:
450
+ params = {}
451
+ try:
452
+ info = self.client.info()
453
+ result = {
454
+ "containers_total": info.get("Containers", 0),
455
+ "containers_running": info.get("ContainersRunning", 0),
456
+ "images": info.get("Images", 0),
457
+ "driver": info.get("Driver", "unknown"),
458
+ "platform": f"{info.get('OperatingSystem', 'unknown')} {info.get('Architecture', 'unknown')}",
459
+ "memory_total": self._format_size(info.get("MemTotal", 0)),
460
+ "swap_total": self._format_size(info.get("SwapTotal", 0)),
461
+ }
462
+ self.log_action("get_info", params, result)
463
+ return result
464
+ except Exception as e:
465
+ self.log_action("get_info", params, error=e)
466
+ raise RuntimeError(f"Failed to get info: {str(e)}")
467
+
468
+ def remove_image(self, image: str, force: bool = False) -> Dict:
469
+ params = {"image": image, "force": force}
470
+ try:
471
+ self.client.images.remove(image, force=force)
472
+ result = {"removed": image}
473
+ self.log_action("remove_image", params, result)
474
+ return result
475
+ except Exception as e:
476
+ self.log_action("remove_image", params, error=e)
477
+ raise RuntimeError(f"Failed to remove image: {str(e)}")
478
+
433
479
  def stop_container(self, container_id: str, timeout: int = 10) -> Dict:
434
480
  params = {"container_id": container_id, "timeout": timeout}
435
481
  try:
@@ -535,65 +581,6 @@ class DockerManager(ContainerManagerBase):
535
581
  self.log_action("remove_volume", params, error=e)
536
582
  raise RuntimeError(f"Failed to remove volume: {str(e)}")
537
583
 
538
- def list_networks(self) -> List[Dict]:
539
- params = {}
540
- try:
541
- networks = self.client.networks.list()
542
- result = []
543
- for net in networks:
544
- attrs = net.attrs
545
- containers = len(attrs.get("Containers", {}))
546
- created = attrs.get("Created", "unknown")
547
- if isinstance(created, str):
548
- created_str = created
549
- else:
550
- created_str = (
551
- datetime.fromtimestamp(created).strftime("%Y-%m-%dT%H:%M:%S")
552
- if created
553
- else "unknown"
554
- )
555
- simplified = {
556
- "id": attrs.get("Id", "unknown")[7:19],
557
- "name": attrs.get("Name", "unknown"),
558
- "driver": attrs.get("Driver", "unknown"),
559
- "scope": attrs.get("Scope", "unknown"),
560
- "containers": containers,
561
- "created": created_str,
562
- }
563
- result.append(simplified)
564
- self.log_action("list_networks", params, result)
565
- return result
566
- except Exception as e:
567
- self.log_action("list_networks", params, error=e)
568
- raise RuntimeError(f"Failed to list networks: {str(e)}")
569
-
570
- def create_network(self, name: str, driver: str = "bridge") -> Dict:
571
- params = {"name": name, "driver": driver}
572
- try:
573
- network = self.client.networks.create(name, driver=driver)
574
- attrs = network.attrs
575
- created = attrs.get("Created", "unknown")
576
- if isinstance(created, str):
577
- created_str = created
578
- else:
579
- created_str = (
580
- datetime.fromtimestamp(created).strftime("%Y-%m-%dT%H:%M:%S")
581
- if created
582
- else "unknown"
583
- )
584
- result = {
585
- "id": attrs.get("Id", "unknown")[7:19],
586
- "name": attrs.get("Name", name),
587
- "driver": attrs.get("Driver", driver),
588
- "scope": attrs.get("Scope", "unknown"),
589
- "created": created_str,
590
- }
591
- self.log_action("create_network", params, result)
592
- return result
593
- except Exception as e:
594
- self.log_action("create_network", params, error=e)
595
- raise RuntimeError(f"Failed to create network: {str(e)}")
596
-
597
584
  def remove_network(self, network_id: str) -> Dict:
598
585
  params = {"network_id": network_id}
599
586
  try:
@@ -829,52 +816,107 @@ class DockerManager(ContainerManagerBase):
829
816
 
830
817
 
831
818
  class PodmanManager(ContainerManagerBase):
832
- def __init__(self, silent: bool = False, log_file: str = None):
819
+ def __init__(self, silent: bool = False, log_file: Optional[str] = None):
833
820
  super().__init__(silent, log_file)
821
+
834
822
  if PodmanClient is None:
835
823
  raise ImportError("Please install podman-py: pip install podman")
824
+
825
+ base_url = self._autodetect_podman_url()
826
+ if base_url is None:
827
+ self.logger.error(
828
+ "No valid Podman socket found after trying all known locations"
829
+ )
830
+ raise RuntimeError("Failed to connect to Podman: No valid socket found")
831
+
836
832
  try:
837
- self.client = PodmanClient()
833
+ self.client = PodmanClient(base_url=base_url)
834
+ self.logger.info(f"Connected to Podman with base_url: {base_url}")
838
835
  except PodmanError as e:
839
- self.logger.error(f"Failed to connect to Podman daemon: {str(e)}")
840
- raise RuntimeError(f"Failed to connect to Podman: {str(e)}")
836
+ self.logger.error(
837
+ f"Failed to connect to Podman daemon with {base_url}: {str(e)}"
838
+ )
839
+ raise RuntimeError(f"Failed to connect to Podman with {base_url}: {str(e)}")
841
840
 
842
- def get_version(self) -> Dict:
843
- params = {}
841
+ def _is_wsl(self) -> bool:
842
+ """Check if running inside WSL2."""
844
843
  try:
845
- version = self.client.version()
846
- result = {
847
- "version": version.get("Version", "unknown"),
848
- "api_version": version.get("APIVersion", "unknown"),
849
- "os": version.get("Os", "unknown"),
850
- "arch": version.get("Arch", "unknown"),
851
- "build_time": version.get("BuildTime", "unknown"),
852
- }
853
- self.log_action("get_version", params, result)
854
- return result
855
- except Exception as e:
856
- self.log_action("get_version", params, error=e)
857
- raise RuntimeError(f"Failed to get version: {str(e)}")
844
+ with open("/proc/version", "r") as f:
845
+ return "WSL" in f.read()
846
+ except FileNotFoundError:
847
+ return "WSL_DISTRO_NAME" in os.environ
858
848
 
859
- def get_info(self) -> Dict:
860
- params = {}
849
+ def _is_podman_machine_running(self) -> bool:
850
+ """Check if Podman machine is running (for Windows/WSL2)."""
861
851
  try:
862
- info = self.client.info()
863
- host = info.get("host", {})
864
- result = {
865
- "containers_total": info.get("store", {}).get("containers", 0),
866
- "containers_running": host.get("runningContainers", 0),
867
- "images": info.get("store", {}).get("images", 0),
868
- "driver": host.get("graphDriverName", "unknown"),
869
- "platform": f"{host.get('os', 'unknown')} {host.get('arch', 'unknown')}",
870
- "memory_total": self._format_size(host.get("memTotal", 0)),
871
- "swap_total": self._format_size(host.get("swapTotal", 0)),
872
- }
873
- self.log_action("get_info", params, result)
874
- return result
875
- except Exception as e:
876
- self.log_action("get_info", params, error=e)
877
- raise RuntimeError(f"Failed to get info: {str(e)}")
852
+ result = subprocess.run(
853
+ ["podman", "machine", "list", "--format", "{{.Running}}"],
854
+ capture_output=True,
855
+ text=True,
856
+ check=False,
857
+ )
858
+ return "true" in result.stdout.lower()
859
+ except (subprocess.SubprocessError, FileNotFoundError):
860
+ return False
861
+
862
+ def _try_connect(self, base_url: str) -> Optional[PodmanClient]:
863
+ """Attempt to connect to Podman with the given base_url."""
864
+ try:
865
+ client = PodmanClient(base_url=base_url)
866
+ # Test connection
867
+ client.version()
868
+ return client
869
+ except PodmanError as e:
870
+ self.logger.debug(f"Connection failed for {base_url}: {str(e)}")
871
+ return None
872
+
873
+ def _autodetect_podman_url(self) -> Optional[str]:
874
+ """Autodetect the appropriate Podman socket URL based on platform."""
875
+ # Check for environment variable override
876
+ base_url = os.environ.get("PODMAN_BASE_URL")
877
+ if base_url:
878
+ self.logger.info(f"Using PODMAN_BASE_URL from environment: {base_url}")
879
+ return base_url
880
+
881
+ system = platform.system()
882
+ is_wsl = self._is_wsl()
883
+
884
+ # Define socket candidates based on platform
885
+ socket_candidates = []
886
+ if system == "Windows" and not is_wsl:
887
+ # Windows with Podman machine
888
+ if self._is_podman_machine_running():
889
+ socket_candidates.append("npipe:////./pipe/docker_engine")
890
+ # Fallback to WSL2 distro sockets if running in a mixed setup
891
+ socket_candidates.extend(
892
+ [
893
+ "unix:///mnt/wsl/podman-sockets/podman-machine-default/podman-user.sock", # Rootless
894
+ "unix:///mnt/wsl/podman-sockets/podman-machine-default/podman-root.sock", # Rootful
895
+ ]
896
+ )
897
+ elif system == "Linux" or is_wsl:
898
+ # Linux or WSL2 distro: prioritize rootless, then rootful
899
+ uid = os.getuid()
900
+ socket_candidates.extend(
901
+ [
902
+ f"unix:///run/user/{uid}/podman/podman.sock", # Rootless
903
+ "unix:///run/podman/podman.sock", # Rootful
904
+ ]
905
+ )
906
+
907
+ # Try each socket candidate
908
+ for url in socket_candidates:
909
+ # For Unix sockets, check if the file exists (on Linux/WSL2)
910
+ if url.startswith("unix://") and (system == "Linux" or is_wsl):
911
+ socket_path = url.replace("unix://", "")
912
+ if not os.path.exists(socket_path):
913
+ self.logger.debug(f"Socket {socket_path} does not exist")
914
+ continue
915
+ client = self._try_connect(url)
916
+ if client:
917
+ return url
918
+
919
+ return None
878
920
 
879
921
  def list_images(self) -> List[Dict]:
880
922
  params = {}
@@ -888,12 +930,8 @@ class PodmanManager(ContainerManagerBase):
888
930
  repository, tag = (
889
931
  repo_tag.rsplit(":", 1) if ":" in repo_tag else ("<none>", "<none>")
890
932
  )
891
- created = attrs.get("Created", 0)
892
- created_str = (
893
- datetime.fromtimestamp(created).strftime("%Y-%m-%dT%H:%M:%S")
894
- if created
895
- else "unknown"
896
- )
933
+ created = attrs.get("Created", None)
934
+ created_str = self._parse_timestamp(created)
897
935
  size_bytes = attrs.get("Size", 0)
898
936
  size_str = self._format_size(size_bytes) if size_bytes else "0B"
899
937
  simplified = {
@@ -926,12 +964,8 @@ class PodmanManager(ContainerManagerBase):
926
964
  repository, tag = (
927
965
  repo_tag.rsplit(":", 1) if ":" in repo_tag else (image, tag)
928
966
  )
929
- created = attrs.get("Created", 0)
930
- created_str = (
931
- datetime.fromtimestamp(created).strftime("%Y-%m-%dT%H:%M:%S")
932
- if created
933
- else "unknown"
934
- )
967
+ created = attrs.get("Created", None)
968
+ created_str = self._parse_timestamp(created)
935
969
  size_bytes = attrs.get("Size", 0)
936
970
  size_str = self._format_size(size_bytes) if size_bytes else "0B"
937
971
  result = {
@@ -949,17 +983,6 @@ class PodmanManager(ContainerManagerBase):
949
983
  self.log_action("pull_image", params, error=e)
950
984
  raise RuntimeError(f"Failed to pull image: {str(e)}")
951
985
 
952
- def remove_image(self, image: str, force: bool = False) -> Dict:
953
- params = {"image": image, "force": force}
954
- try:
955
- self.client.images.remove(image, force=force)
956
- result = {"removed": image}
957
- self.log_action("remove_image", params, result)
958
- return result
959
- except Exception as e:
960
- self.log_action("remove_image", params, error=e)
961
- raise RuntimeError(f"Failed to remove image: {str(e)}")
962
-
963
986
  def list_containers(self, all: bool = False) -> List[Dict]:
964
987
  params = {"all": all}
965
988
  try:
@@ -973,12 +996,8 @@ class PodmanManager(ContainerManagerBase):
973
996
  for p in ports
974
997
  if p.get("host_port")
975
998
  ]
976
- created = attrs.get("Created", 0)
977
- created_str = (
978
- datetime.fromtimestamp(created).strftime("%Y-%m-%dT%H:%M:%S")
979
- if created
980
- else "unknown"
981
- )
999
+ created = attrs.get("Created", None)
1000
+ created_str = self._parse_timestamp(created)
982
1001
  simplified = {
983
1002
  "id": attrs.get("Id", "unknown")[7:19],
984
1003
  "image": attrs.get("Image", "unknown"),
@@ -1034,12 +1053,8 @@ class PodmanManager(ContainerManagerBase):
1034
1053
  for p in ports
1035
1054
  if p.get("host_port")
1036
1055
  ]
1037
- created = attrs.get("Created", 0)
1038
- created_str = (
1039
- datetime.fromtimestamp(created).strftime("%Y-%m-%dT%H:%M:%S")
1040
- if created
1041
- else "unknown"
1042
- )
1056
+ created = attrs.get("Created", None)
1057
+ created_str = self._parse_timestamp(created)
1043
1058
  result = {
1044
1059
  "id": attrs.get("Id", "unknown")[7:19],
1045
1060
  "image": attrs.get("Image", image),
@@ -1054,6 +1069,99 @@ class PodmanManager(ContainerManagerBase):
1054
1069
  self.log_action("run_container", params, error=e)
1055
1070
  raise RuntimeError(f"Failed to run container: {str(e)}")
1056
1071
 
1072
+ def list_networks(self) -> List[Dict]:
1073
+ params = {}
1074
+ try:
1075
+ networks = self.client.networks.list()
1076
+ result = []
1077
+ for net in networks:
1078
+ attrs = net.attrs
1079
+ containers = len(attrs.get("Containers", {}))
1080
+ created = attrs.get("Created", None)
1081
+ created_str = self._parse_timestamp(created)
1082
+ simplified = {
1083
+ "id": attrs.get("Id", "unknown")[7:19],
1084
+ "name": attrs.get("Name", "unknown"),
1085
+ "driver": attrs.get("Driver", "unknown"),
1086
+ "scope": attrs.get("Scope", "unknown"),
1087
+ "containers": containers,
1088
+ "created": created_str,
1089
+ }
1090
+ result.append(simplified)
1091
+ self.log_action("list_networks", params, result)
1092
+ return result
1093
+ except Exception as e:
1094
+ self.log_action("list_networks", params, error=e)
1095
+ raise RuntimeError(f"Failed to list networks: {str(e)}")
1096
+
1097
+ def create_network(self, name: str, driver: str = "bridge") -> Dict:
1098
+ params = {"name": name, "driver": driver}
1099
+ try:
1100
+ network = self.client.networks.create(name, driver=driver)
1101
+ attrs = network.attrs
1102
+ created = attrs.get("Created", None)
1103
+ created_str = self._parse_timestamp(created)
1104
+ result = {
1105
+ "id": attrs.get("Id", "unknown")[7:19],
1106
+ "name": attrs.get("Name", name),
1107
+ "driver": attrs.get("Driver", driver),
1108
+ "scope": attrs.get("Scope", "unknown"),
1109
+ "created": created_str,
1110
+ }
1111
+ self.log_action("create_network", params, result)
1112
+ return result
1113
+ except Exception as e:
1114
+ self.log_action("create_network", params, error=e)
1115
+ raise RuntimeError(f"Failed to create network: {str(e)}")
1116
+
1117
+ def get_version(self) -> Dict:
1118
+ params = {}
1119
+ try:
1120
+ version = self.client.version()
1121
+ result = {
1122
+ "version": version.get("Version", "unknown"),
1123
+ "api_version": version.get("APIVersion", "unknown"),
1124
+ "os": version.get("Os", "unknown"),
1125
+ "arch": version.get("Arch", "unknown"),
1126
+ "build_time": version.get("BuildTime", "unknown"),
1127
+ }
1128
+ self.log_action("get_version", params, result)
1129
+ return result
1130
+ except Exception as e:
1131
+ self.log_action("get_version", params, error=e)
1132
+ raise RuntimeError(f"Failed to get version: {str(e)}")
1133
+
1134
+ def get_info(self) -> Dict:
1135
+ params = {}
1136
+ try:
1137
+ info = self.client.info()
1138
+ host = info.get("host", {})
1139
+ result = {
1140
+ "containers_total": info.get("store", {}).get("containers", 0),
1141
+ "containers_running": host.get("runningContainers", 0),
1142
+ "images": info.get("store", {}).get("images", 0),
1143
+ "driver": host.get("graphDriverName", "unknown"),
1144
+ "platform": f"{host.get('os', 'unknown')} {host.get('arch', 'unknown')}",
1145
+ "memory_total": self._format_size(host.get("memTotal", 0)),
1146
+ "swap_total": self._format_size(host.get("swapTotal", 0)),
1147
+ }
1148
+ self.log_action("get_info", params, result)
1149
+ return result
1150
+ except Exception as e:
1151
+ self.log_action("get_info", params, error=e)
1152
+ raise RuntimeError(f"Failed to get info: {str(e)}")
1153
+
1154
+ def remove_image(self, image: str, force: bool = False) -> Dict:
1155
+ params = {"image": image, "force": force}
1156
+ try:
1157
+ self.client.images.remove(image, force=force)
1158
+ result = {"removed": image}
1159
+ self.log_action("remove_image", params, result)
1160
+ return result
1161
+ except Exception as e:
1162
+ self.log_action("remove_image", params, error=e)
1163
+ raise RuntimeError(f"Failed to remove image: {str(e)}")
1164
+
1057
1165
  def stop_container(self, container_id: str, timeout: int = 10) -> Dict:
1058
1166
  params = {"container_id": container_id, "timeout": timeout}
1059
1167
  try:
@@ -1159,65 +1267,6 @@ class PodmanManager(ContainerManagerBase):
1159
1267
  self.log_action("remove_volume", params, error=e)
1160
1268
  raise RuntimeError(f"Failed to remove volume: {str(e)}")
1161
1269
 
1162
- def list_networks(self) -> List[Dict]:
1163
- params = {}
1164
- try:
1165
- networks = self.client.networks.list()
1166
- result = []
1167
- for net in networks:
1168
- attrs = net.attrs
1169
- containers = len(attrs.get("Containers", {}))
1170
- created = attrs.get("Created", "unknown")
1171
- if isinstance(created, str):
1172
- created_str = created
1173
- else:
1174
- created_str = (
1175
- datetime.fromtimestamp(created).strftime("%Y-%m-%dT%H:%M:%S")
1176
- if created
1177
- else "unknown"
1178
- )
1179
- simplified = {
1180
- "id": attrs.get("Id", "unknown")[7:19],
1181
- "name": attrs.get("Name", "unknown"),
1182
- "driver": attrs.get("Driver", "unknown"),
1183
- "scope": attrs.get("Scope", "unknown"),
1184
- "containers": containers,
1185
- "created": created_str,
1186
- }
1187
- result.append(simplified)
1188
- self.log_action("list_networks", params, result)
1189
- return result
1190
- except Exception as e:
1191
- self.log_action("list_networks", params, error=e)
1192
- raise RuntimeError(f"Failed to list networks: {str(e)}")
1193
-
1194
- def create_network(self, name: str, driver: str = "bridge") -> Dict:
1195
- params = {"name": name, "driver": driver}
1196
- try:
1197
- network = self.client.networks.create(name, driver=driver)
1198
- attrs = network.attrs
1199
- created = attrs.get("Created", "unknown")
1200
- if isinstance(created, str):
1201
- created_str = created
1202
- else:
1203
- created_str = (
1204
- datetime.fromtimestamp(created).strftime("%Y-%m-%dT%H:%M:%S")
1205
- if created
1206
- else "unknown"
1207
- )
1208
- result = {
1209
- "id": attrs.get("Id", "unknown")[7:19],
1210
- "name": attrs.get("Name", name),
1211
- "driver": attrs.get("Driver", driver),
1212
- "scope": attrs.get("Scope", "unknown"),
1213
- "created": created_str,
1214
- }
1215
- self.log_action("create_network", params, result)
1216
- return result
1217
- except Exception as e:
1218
- self.log_action("create_network", params, error=e)
1219
- raise RuntimeError(f"Failed to create network: {str(e)}")
1220
-
1221
1270
  def remove_network(self, network_id: str) -> Dict:
1222
1271
  params = {"network_id": network_id}
1223
1272
  try:
@@ -1316,13 +1365,32 @@ class PodmanManager(ContainerManagerBase):
1316
1365
  raise NotImplementedError("Swarm not supported in Podman")
1317
1366
 
1318
1367
 
1319
- # The rest of the file (create_manager, usage, container_manager) remains unchanged
1320
-
1321
-
1322
1368
  def create_manager(
1323
- manager_type: str, silent: bool = False, log_file: str = None
1369
+ manager_type: Optional[str] = None, silent: bool = False, log_file: str = None
1324
1370
  ) -> ContainerManagerBase:
1325
- if manager_type.lower() == "docker" or manager_type.lower() == "swarm":
1371
+ if manager_type is None:
1372
+ manager_type = os.environ.get("CONTAINER_MANAGER_TYPE")
1373
+ if manager_type is None:
1374
+ # Autodetect
1375
+ if PodmanClient is not None:
1376
+ try:
1377
+ test_client = PodmanClient()
1378
+ test_client.close()
1379
+ manager_type = "podman"
1380
+ except Exception:
1381
+ pass
1382
+ if manager_type is None and docker is not None:
1383
+ try:
1384
+ test_client = docker.from_env()
1385
+ test_client.close()
1386
+ manager_type = "docker"
1387
+ except Exception:
1388
+ pass
1389
+ if manager_type is None:
1390
+ raise ValueError(
1391
+ "No supported container manager detected. Set CONTAINER_MANAGER_TYPE or install Docker/Podman."
1392
+ )
1393
+ if manager_type.lower() in ["docker", "swarm"]:
1326
1394
  return DockerManager(silent=silent, log_file=log_file)
1327
1395
  elif manager_type.lower() == "podman":
1328
1396
  return PodmanManager(silent=silent, log_file=log_file)
@@ -1338,7 +1406,7 @@ Container Manager: A tool to manage containers with Docker, Podman, and Docker S
1338
1406
  Usage:
1339
1407
  -h | --help [ See usage for script ]
1340
1408
  -s | --silent [ Suppress output ]
1341
- -m | --manager <type> [ docker, podman, swarm; default: docker ]
1409
+ -m | --manager <type> [ docker, podman, swarm; default: auto-detect ]
1342
1410
  --log-file <path> [ Log to specified file (default: container_manager.log in script dir) ]
1343
1411
 
1344
1412
  Actions:
@@ -1403,262 +1471,178 @@ container_manager.py --manager docker --pull-image nginx --tag latest --list-con
1403
1471
 
1404
1472
 
1405
1473
  def container_manager(argv):
1406
- get_version = False
1407
- get_info = False
1408
- list_images = False
1409
- pull_image = False
1410
- pull_image_str = None
1411
- tag = "latest"
1412
- platform = None
1413
- remove_image = False
1414
- remove_image_str = None
1415
- force = False
1416
- list_containers = False
1417
- all_containers = False
1418
- run_container = False
1419
- run_image = None
1420
- name = None
1421
- command = None
1422
- detach = False
1423
- ports_str = None
1424
- volumes_str = None
1425
- environment_str = None
1426
- stop_container = False
1427
- stop_container_id = None
1428
- timeout = 10
1429
- remove_container = False
1430
- remove_container_id = None
1431
- get_container_logs = False
1432
- container_logs_id = None
1433
- tail = "all"
1434
- exec_in_container = False
1435
- exec_container_id = None
1436
- exec_command = None
1437
- exec_detach = False
1438
- list_volumes = False
1439
- create_volume = False
1440
- create_volume_name = None
1441
- remove_volume = False
1442
- remove_volume_name = None
1443
- list_networks = False
1444
- create_network = False
1445
- create_network_name = None
1446
- driver = "bridge"
1447
- remove_network = False
1448
- remove_network_id = None
1449
- compose_up = False
1450
- compose_up_file = None
1451
- compose_build = False
1452
- compose_detach = True
1453
- compose_down = False
1454
- compose_down_file = None
1455
- compose_ps = False
1456
- compose_ps_file = None
1457
- compose_logs = False
1458
- compose_logs_file = None
1459
- compose_service = None
1460
- init_swarm = False
1461
- advertise_addr = None
1462
- leave_swarm = False
1463
- list_nodes = False
1464
- list_services = False
1465
- create_service = False
1466
- create_service_name = None
1467
- service_image = None
1468
- replicas = 1
1469
- mounts_str = None
1470
- remove_service = False
1471
- remove_service_id = None
1472
- manager_type = "docker"
1473
- silent = False
1474
- log_file = None
1475
-
1476
- try:
1477
- opts, _ = getopt.getopt(
1478
- argv,
1479
- "hsm:",
1480
- [
1481
- "help",
1482
- "silent",
1483
- "manager=",
1484
- "log-file=",
1485
- "get-version",
1486
- "get-info",
1487
- "list-images",
1488
- "pull-image=",
1489
- "tag=",
1490
- "platform=",
1491
- "remove-image=",
1492
- "force",
1493
- "list-containers",
1494
- "all",
1495
- "run-container=",
1496
- "name=",
1497
- "command=",
1498
- "detach",
1499
- "ports=",
1500
- "volumes=",
1501
- "environment=",
1502
- "stop-container=",
1503
- "timeout=",
1504
- "remove-container=",
1505
- "get-container-logs=",
1506
- "tail=",
1507
- "exec-in-container=",
1508
- "exec-command=",
1509
- "exec-detach",
1510
- "list-volumes",
1511
- "create-volume=",
1512
- "remove-volume=",
1513
- "list-networks",
1514
- "create-network=",
1515
- "driver=",
1516
- "remove-network=",
1517
- "compose-up=",
1518
- "build",
1519
- "compose-down=",
1520
- "compose-ps=",
1521
- "compose-logs=",
1522
- "service=",
1523
- "init-swarm",
1524
- "advertise-addr=",
1525
- "leave-swarm",
1526
- "list-nodes",
1527
- "list-services",
1528
- "create-service=",
1529
- "image=",
1530
- "replicas=",
1531
- "mounts=",
1532
- "remove-service=",
1533
- ],
1534
- )
1535
- except getopt.GetoptError:
1536
- usage()
1537
- sys.exit(2)
1474
+ parser = argparse.ArgumentParser(
1475
+ description="Container Manager: A tool to manage containers with Docker, Podman, and Docker Swarm!"
1476
+ )
1477
+ parser.add_argument("-s", "--silent", action="store_true", help="Suppress output")
1478
+ parser.add_argument(
1479
+ "-m",
1480
+ "--manager",
1481
+ type=str,
1482
+ default=None,
1483
+ help="Container manager type: docker, podman, swarm (default: auto-detect)",
1484
+ )
1485
+ parser.add_argument("--log-file", type=str, default=None, help="Path to log file")
1486
+ parser.add_argument("--get-version", action="store_true", help="Get version info")
1487
+ parser.add_argument("--get-info", action="store_true", help="Get system info")
1488
+ parser.add_argument("--list-images", action="store_true", help="List images")
1489
+ parser.add_argument("--pull-image", type=str, default=None, help="Image to pull")
1490
+ parser.add_argument("--tag", type=str, default="latest", help="Image tag")
1491
+ parser.add_argument("--platform", type=str, default=None, help="Platform")
1492
+ parser.add_argument(
1493
+ "--remove-image", type=str, default=None, help="Image to remove"
1494
+ )
1495
+ parser.add_argument("--force", action="store_true", help="Force removal")
1496
+ parser.add_argument(
1497
+ "--list-containers", action="store_true", help="List containers"
1498
+ )
1499
+ parser.add_argument("--all", action="store_true", help="Show all containers")
1500
+ parser.add_argument("--run-container", type=str, default=None, help="Image to run")
1501
+ parser.add_argument("--name", type=str, default=None, help="Container name")
1502
+ parser.add_argument("--command", type=str, default=None, help="Command to run")
1503
+ parser.add_argument("--detach", action="store_true", help="Detach mode")
1504
+ parser.add_argument("--ports", type=str, default=None, help="Port mappings")
1505
+ parser.add_argument("--volumes", type=str, default=None, help="Volume mappings")
1506
+ parser.add_argument(
1507
+ "--environment", type=str, default=None, help="Environment vars"
1508
+ )
1509
+ parser.add_argument(
1510
+ "--stop-container", type=str, default=None, help="Container to stop"
1511
+ )
1512
+ parser.add_argument("--timeout", type=int, default=10, help="Timeout in seconds")
1513
+ parser.add_argument(
1514
+ "--remove-container", type=str, default=None, help="Container to remove"
1515
+ )
1516
+ parser.add_argument(
1517
+ "--get-container-logs", type=str, default=None, help="Container logs"
1518
+ )
1519
+ parser.add_argument("--tail", type=str, default="all", help="Tail lines")
1520
+ parser.add_argument(
1521
+ "--exec-in-container", type=str, default=None, help="Container to exec"
1522
+ )
1523
+ parser.add_argument("--exec-command", type=str, default=None, help="Exec command")
1524
+ parser.add_argument("--exec-detach", action="store_true", help="Detach exec")
1525
+ parser.add_argument("--list-volumes", action="store_true", help="List volumes")
1526
+ parser.add_argument(
1527
+ "--create-volume", type=str, default=None, help="Volume to create"
1528
+ )
1529
+ parser.add_argument(
1530
+ "--remove-volume", type=str, default=None, help="Volume to remove"
1531
+ )
1532
+ parser.add_argument("--list-networks", action="store_true", help="List networks")
1533
+ parser.add_argument(
1534
+ "--create-network", type=str, default=None, help="Network to create"
1535
+ )
1536
+ parser.add_argument("--driver", type=str, default="bridge", help="Network driver")
1537
+ parser.add_argument(
1538
+ "--remove-network", type=str, default=None, help="Network to remove"
1539
+ )
1540
+ parser.add_argument("--compose-up", type=str, default=None, help="Compose file up")
1541
+ parser.add_argument("--build", action="store_true", help="Build images")
1542
+ parser.add_argument(
1543
+ "--compose-detach", action="store_true", default=True, help="Detach compose"
1544
+ )
1545
+ parser.add_argument(
1546
+ "--compose-down", type=str, default=None, help="Compose file down"
1547
+ )
1548
+ parser.add_argument("--compose-ps", type=str, default=None, help="Compose ps")
1549
+ parser.add_argument("--compose-logs", type=str, default=None, help="Compose logs")
1550
+ parser.add_argument("--service", type=str, default=None, help="Specific service")
1551
+ parser.add_argument("--init-swarm", action="store_true", help="Init swarm")
1552
+ parser.add_argument(
1553
+ "--advertise-addr", type=str, default=None, help="Advertise address"
1554
+ )
1555
+ parser.add_argument("--leave-swarm", action="store_true", help="Leave swarm")
1556
+ parser.add_argument("--list-nodes", action="store_true", help="List swarm nodes")
1557
+ parser.add_argument(
1558
+ "--list-services", action="store_true", help="List swarm services"
1559
+ )
1560
+ parser.add_argument(
1561
+ "--create-service", type=str, default=None, help="Service to create"
1562
+ )
1563
+ parser.add_argument("--image", type=str, default=None, help="Service image")
1564
+ parser.add_argument("--replicas", type=int, default=1, help="Replicas")
1565
+ parser.add_argument("--mounts", type=str, default=None, help="Mounts")
1566
+ parser.add_argument(
1567
+ "--remove-service", type=str, default=None, help="Service to remove"
1568
+ )
1569
+ parser.add_argument("-h", "--help", action="store_true", help="Show help")
1570
+
1571
+ args = parser.parse_args(argv)
1538
1572
 
1539
- for opt, arg in opts:
1540
- if opt in ("-h", "--help"):
1541
- usage()
1542
- sys.exit()
1543
- elif opt in ("-s", "--silent"):
1544
- silent = True
1545
- elif opt in ("-m", "--manager"):
1546
- manager_type = arg
1547
- elif opt == "--log-file":
1548
- log_file = arg
1549
- elif opt == "--get-version":
1550
- get_version = True
1551
- elif opt == "--get-info":
1552
- get_info = True
1553
- elif opt == "--list-images":
1554
- list_images = True
1555
- elif opt == "--pull-image":
1556
- pull_image = True
1557
- pull_image_str = arg
1558
- elif opt == "--tag":
1559
- tag = arg
1560
- elif opt == "--platform":
1561
- platform = arg
1562
- elif opt == "--remove-image":
1563
- remove_image = True
1564
- remove_image_str = arg
1565
- elif opt == "--force":
1566
- force = True
1567
- elif opt == "--list-containers":
1568
- list_containers = True
1569
- elif opt == "--all":
1570
- all_containers = True
1571
- elif opt == "--run-container":
1572
- run_container = True
1573
- run_image = arg
1574
- elif opt == "--name":
1575
- name = arg
1576
- elif opt == "--command":
1577
- command = arg
1578
- elif opt == "--detach":
1579
- detach = True
1580
- elif opt == "--ports":
1581
- ports_str = arg
1582
- elif opt == "--volumes":
1583
- volumes_str = arg
1584
- elif opt == "--environment":
1585
- environment_str = arg
1586
- elif opt == "--stop-container":
1587
- stop_container = True
1588
- stop_container_id = arg
1589
- elif opt == "--timeout":
1590
- timeout = int(arg)
1591
- elif opt == "--remove-container":
1592
- remove_container = True
1593
- remove_container_id = arg
1594
- elif opt == "--get-container-logs":
1595
- get_container_logs = True
1596
- container_logs_id = arg
1597
- elif opt == "--tail":
1598
- tail = arg
1599
- elif opt == "--exec-in-container":
1600
- exec_in_container = True
1601
- exec_container_id = arg
1602
- elif opt == "--exec-command":
1603
- exec_command = arg
1604
- elif opt == "--exec-detach":
1605
- exec_detach = True
1606
- elif opt == "--list-volumes":
1607
- list_volumes = True
1608
- elif opt == "--create-volume":
1609
- create_volume = True
1610
- create_volume_name = arg
1611
- elif opt == "--remove-volume":
1612
- remove_volume = True
1613
- remove_volume_name = arg
1614
- elif opt == "--list-networks":
1615
- list_networks = True
1616
- elif opt == "--create-network":
1617
- create_network = True
1618
- create_network_name = arg
1619
- elif opt == "--driver":
1620
- driver = arg
1621
- elif opt == "--remove-network":
1622
- remove_network = True
1623
- remove_network_id = arg
1624
- elif opt == "--compose-up":
1625
- compose_up = True
1626
- compose_up_file = arg
1627
- elif opt == "--build":
1628
- compose_build = True
1629
- elif opt == "--compose-down":
1630
- compose_down = True
1631
- compose_down_file = arg
1632
- elif opt == "--compose-ps":
1633
- compose_ps = True
1634
- compose_ps_file = arg
1635
- elif opt == "--compose-logs":
1636
- compose_logs = True
1637
- compose_logs_file = arg
1638
- elif opt == "--service":
1639
- compose_service = arg
1640
- elif opt == "--init-swarm":
1641
- init_swarm = True
1642
- elif opt == "--advertise-addr":
1643
- advertise_addr = arg
1644
- elif opt == "--leave-swarm":
1645
- leave_swarm = True
1646
- elif opt == "--list-nodes":
1647
- list_nodes = True
1648
- elif opt == "--list-services":
1649
- list_services = True
1650
- elif opt == "--create-service":
1651
- create_service = True
1652
- create_service_name = arg
1653
- elif opt == "--image":
1654
- service_image = arg
1655
- elif opt == "--replicas":
1656
- replicas = int(arg)
1657
- elif opt == "--mounts":
1658
- mounts_str = arg
1659
- elif opt == "--remove-service":
1660
- remove_service = True
1661
- remove_service_id = arg
1573
+ if args.help:
1574
+ usage()
1575
+ sys.exit(0)
1576
+
1577
+ get_version = args.get_version
1578
+ get_info = args.get_info
1579
+ list_images = args.list_images
1580
+ pull_image = args.pull_image is not None
1581
+ pull_image_str = args.pull_image
1582
+ tag = args.tag
1583
+ platform = args.platform
1584
+ remove_image = args.remove_image is not None
1585
+ remove_image_str = args.remove_image
1586
+ force = args.force
1587
+ list_containers = args.list_containers
1588
+ all_containers = args.all
1589
+ run_container = args.run_container is not None
1590
+ run_image = args.run_container
1591
+ name = args.name
1592
+ command = args.command
1593
+ detach = args.detach
1594
+ ports_str = args.ports
1595
+ volumes_str = args.volumes
1596
+ environment_str = args.environment
1597
+ stop_container = args.stop_container is not None
1598
+ stop_container_id = args.stop_container
1599
+ timeout = args.timeout
1600
+ remove_container = args.remove_container is not None
1601
+ remove_container_id = args.remove_container
1602
+ get_container_logs = args.get_container_logs is not None
1603
+ container_logs_id = args.get_container_logs
1604
+ tail = args.tail
1605
+ exec_in_container = args.exec_in_container is not None
1606
+ exec_container_id = args.exec_in_container
1607
+ exec_command = args.exec_command
1608
+ exec_detach = args.exec_detach
1609
+ list_volumes = args.list_volumes
1610
+ create_volume = args.create_volume is not None
1611
+ create_volume_name = args.create_volume
1612
+ remove_volume = args.remove_volume is not None
1613
+ remove_volume_name = args.remove_volume
1614
+ list_networks = args.list_networks
1615
+ create_network = args.create_network is not None
1616
+ create_network_name = args.create_network
1617
+ driver = args.driver
1618
+ remove_network = args.remove_network is not None
1619
+ remove_network_id = args.remove_network
1620
+ compose_up = args.compose_up is not None
1621
+ compose_up_file = args.compose_up
1622
+ compose_build = args.build
1623
+ compose_detach = args.compose_detach
1624
+ compose_down = args.compose_down is not None
1625
+ compose_down_file = args.compose_down
1626
+ compose_ps = args.compose_ps is not None
1627
+ compose_ps_file = args.compose_ps
1628
+ compose_logs = args.compose_logs is not None
1629
+ compose_logs_file = args.compose_logs
1630
+ compose_service = args.service
1631
+ init_swarm = args.init_swarm
1632
+ advertise_addr = args.advertise_addr
1633
+ leave_swarm = args.leave_swarm
1634
+ list_nodes = args.list_nodes
1635
+ list_services = args.list_services
1636
+ create_service = args.create_service is not None
1637
+ create_service_name = args.create_service
1638
+ service_image = args.image
1639
+ replicas = args.replicas
1640
+ mounts_str = args.mounts
1641
+ remove_service = args.remove_service is not None
1642
+ remove_service_id = args.remove_service
1643
+ manager_type = args.manager
1644
+ silent = args.silent
1645
+ log_file = args.log_file
1662
1646
 
1663
1647
  manager = create_manager(manager_type, silent, log_file)
1664
1648