container-manager-mcp 0.0.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- container_manager_mcp/__init__.py +24 -0
- container_manager_mcp/container_manager.py +1381 -0
- container_manager_mcp/container_manager_mcp.py +1134 -0
- container_manager_mcp-0.0.2.dist-info/METADATA +248 -0
- container_manager_mcp-0.0.2.dist-info/RECORD +9 -0
- container_manager_mcp-0.0.2.dist-info/WHEEL +5 -0
- container_manager_mcp-0.0.2.dist-info/entry_points.txt +3 -0
- container_manager_mcp-0.0.2.dist-info/licenses/LICENSE +20 -0
- container_manager_mcp-0.0.2.dist-info/top_level.txt +1 -0
@@ -0,0 +1,1381 @@
|
|
1
|
+
#!/usr/bin/env python
|
2
|
+
# coding: utf-8
|
3
|
+
|
4
|
+
import sys
|
5
|
+
import logging
|
6
|
+
import os
|
7
|
+
from abc import ABC, abstractmethod
|
8
|
+
from typing import List, Dict, Optional, Any
|
9
|
+
import getopt
|
10
|
+
import json
|
11
|
+
import subprocess
|
12
|
+
|
13
|
+
try:
|
14
|
+
import docker
|
15
|
+
from docker.errors import DockerException
|
16
|
+
except ImportError:
|
17
|
+
docker = None
|
18
|
+
DockerException = Exception
|
19
|
+
|
20
|
+
try:
|
21
|
+
from podman import PodmanClient
|
22
|
+
from podman.errors import PodmanError
|
23
|
+
except ImportError:
|
24
|
+
PodmanClient = None
|
25
|
+
PodmanError = Exception
|
26
|
+
|
27
|
+
|
28
|
+
class ContainerManagerBase(ABC):
|
29
|
+
def __init__(self, silent: bool = False, log_file: str = None):
|
30
|
+
self.silent = silent
|
31
|
+
self.setup_logging(log_file)
|
32
|
+
|
33
|
+
def setup_logging(self, log_file: str):
|
34
|
+
if not log_file:
|
35
|
+
script_dir = os.path.dirname(os.path.abspath(__file__))
|
36
|
+
log_file = os.path.join(script_dir, "container_manager.log")
|
37
|
+
logging.basicConfig(
|
38
|
+
filename=log_file,
|
39
|
+
level=logging.INFO,
|
40
|
+
format="%(asctime)s - %(levelname)s - %(message)s",
|
41
|
+
)
|
42
|
+
self.logger = logging.getLogger(__name__)
|
43
|
+
self.logger.info(f"Logging initialized to {log_file}")
|
44
|
+
|
45
|
+
def log_action(
|
46
|
+
self,
|
47
|
+
action: str,
|
48
|
+
params: Dict = None,
|
49
|
+
result: Any = None,
|
50
|
+
error: Exception = None,
|
51
|
+
):
|
52
|
+
self.logger.info(f"Performing action: {action} with params: {params}")
|
53
|
+
if result:
|
54
|
+
self.logger.info(f"Result: {result}")
|
55
|
+
if error:
|
56
|
+
self.logger.error(f"Error: {str(error)}")
|
57
|
+
|
58
|
+
@abstractmethod
|
59
|
+
def get_version(self) -> Dict:
|
60
|
+
pass
|
61
|
+
|
62
|
+
@abstractmethod
|
63
|
+
def get_info(self) -> Dict:
|
64
|
+
pass
|
65
|
+
|
66
|
+
@abstractmethod
|
67
|
+
def list_images(self) -> List[Dict]:
|
68
|
+
pass
|
69
|
+
|
70
|
+
@abstractmethod
|
71
|
+
def pull_image(
|
72
|
+
self, image: str, tag: str = "latest", platform: Optional[str] = None
|
73
|
+
) -> Dict:
|
74
|
+
pass
|
75
|
+
|
76
|
+
@abstractmethod
|
77
|
+
def remove_image(self, image: str, force: bool = False) -> Dict:
|
78
|
+
pass
|
79
|
+
|
80
|
+
@abstractmethod
|
81
|
+
def list_containers(self, all: bool = False) -> List[Dict]:
|
82
|
+
pass
|
83
|
+
|
84
|
+
@abstractmethod
|
85
|
+
def run_container(
|
86
|
+
self,
|
87
|
+
image: str,
|
88
|
+
name: Optional[str] = None,
|
89
|
+
command: Optional[str] = None,
|
90
|
+
detach: bool = False,
|
91
|
+
ports: Optional[Dict[str, str]] = None,
|
92
|
+
volumes: Optional[Dict[str, Dict]] = None,
|
93
|
+
environment: Optional[Dict[str, str]] = None,
|
94
|
+
) -> Dict:
|
95
|
+
pass
|
96
|
+
|
97
|
+
@abstractmethod
|
98
|
+
def stop_container(self, container_id: str, timeout: int = 10) -> Dict:
|
99
|
+
pass
|
100
|
+
|
101
|
+
@abstractmethod
|
102
|
+
def remove_container(self, container_id: str, force: bool = False) -> Dict:
|
103
|
+
pass
|
104
|
+
|
105
|
+
@abstractmethod
|
106
|
+
def get_container_logs(self, container_id: str, tail: str = "all") -> str:
|
107
|
+
pass
|
108
|
+
|
109
|
+
@abstractmethod
|
110
|
+
def exec_in_container(
|
111
|
+
self, container_id: str, command: List[str], detach: bool = False
|
112
|
+
) -> Dict:
|
113
|
+
pass
|
114
|
+
|
115
|
+
@abstractmethod
|
116
|
+
def list_volumes(self) -> Dict:
|
117
|
+
pass
|
118
|
+
|
119
|
+
@abstractmethod
|
120
|
+
def create_volume(self, name: str) -> Dict:
|
121
|
+
pass
|
122
|
+
|
123
|
+
@abstractmethod
|
124
|
+
def remove_volume(self, name: str, force: bool = False) -> Dict:
|
125
|
+
pass
|
126
|
+
|
127
|
+
@abstractmethod
|
128
|
+
def list_networks(self) -> List[Dict]:
|
129
|
+
pass
|
130
|
+
|
131
|
+
@abstractmethod
|
132
|
+
def create_network(self, name: str, driver: str = "bridge") -> Dict:
|
133
|
+
pass
|
134
|
+
|
135
|
+
@abstractmethod
|
136
|
+
def remove_network(self, network_id: str) -> Dict:
|
137
|
+
pass
|
138
|
+
|
139
|
+
# Compose methods
|
140
|
+
@abstractmethod
|
141
|
+
def compose_up(
|
142
|
+
self, compose_file: str, detach: bool = True, build: bool = False
|
143
|
+
) -> str:
|
144
|
+
pass
|
145
|
+
|
146
|
+
@abstractmethod
|
147
|
+
def compose_down(self, compose_file: str) -> str:
|
148
|
+
pass
|
149
|
+
|
150
|
+
@abstractmethod
|
151
|
+
def compose_ps(self, compose_file: str) -> str:
|
152
|
+
pass
|
153
|
+
|
154
|
+
@abstractmethod
|
155
|
+
def compose_logs(self, compose_file: str, service: Optional[str] = None) -> str:
|
156
|
+
pass
|
157
|
+
|
158
|
+
# Swarm methods (to be implemented only in DockerManager)
|
159
|
+
def init_swarm(self, advertise_addr: Optional[str] = None) -> Dict:
|
160
|
+
raise NotImplementedError("Swarm not supported")
|
161
|
+
|
162
|
+
def leave_swarm(self, force: bool = False) -> Dict:
|
163
|
+
raise NotImplementedError("Swarm not supported")
|
164
|
+
|
165
|
+
def list_nodes(self) -> List[Dict]:
|
166
|
+
raise NotImplementedError("Swarm not supported")
|
167
|
+
|
168
|
+
def list_services(self) -> List[Dict]:
|
169
|
+
raise NotImplementedError("Swarm not supported")
|
170
|
+
|
171
|
+
def create_service(
|
172
|
+
self,
|
173
|
+
name: str,
|
174
|
+
image: str,
|
175
|
+
replicas: int = 1,
|
176
|
+
ports: Optional[Dict[str, str]] = None,
|
177
|
+
mounts: Optional[List[str]] = None,
|
178
|
+
) -> Dict:
|
179
|
+
raise NotImplementedError("Swarm not supported")
|
180
|
+
|
181
|
+
def remove_service(self, service_id: str) -> Dict:
|
182
|
+
raise NotImplementedError("Swarm not supported")
|
183
|
+
|
184
|
+
|
185
|
+
class DockerManager(ContainerManagerBase):
|
186
|
+
def __init__(self, silent: bool = False, log_file: str = None):
|
187
|
+
super().__init__(silent, log_file)
|
188
|
+
if docker is None:
|
189
|
+
raise ImportError("Please install docker-py: pip install docker")
|
190
|
+
try:
|
191
|
+
self.client = docker.from_env()
|
192
|
+
except DockerException as e:
|
193
|
+
self.logger.error(f"Failed to connect to Docker daemon: {str(e)}")
|
194
|
+
raise RuntimeError(f"Failed to connect to Docker: {str(e)}")
|
195
|
+
|
196
|
+
def get_version(self) -> Dict:
|
197
|
+
params = {}
|
198
|
+
try:
|
199
|
+
result = self.client.version()
|
200
|
+
self.log_action("get_version", params, result)
|
201
|
+
return result
|
202
|
+
except Exception as e:
|
203
|
+
self.log_action("get_version", params, error=e)
|
204
|
+
raise RuntimeError(f"Failed to get version: {str(e)}")
|
205
|
+
|
206
|
+
def get_info(self) -> Dict:
|
207
|
+
params = {}
|
208
|
+
try:
|
209
|
+
result = self.client.info()
|
210
|
+
self.log_action("get_info", params, result)
|
211
|
+
return result
|
212
|
+
except Exception as e:
|
213
|
+
self.log_action("get_info", params, error=e)
|
214
|
+
raise RuntimeError(f"Failed to get info: {str(e)}")
|
215
|
+
|
216
|
+
def list_images(self) -> List[Dict]:
|
217
|
+
params = {}
|
218
|
+
try:
|
219
|
+
images = self.client.images.list()
|
220
|
+
result = [img.attrs for img in images]
|
221
|
+
self.log_action("list_images", params, result)
|
222
|
+
return result
|
223
|
+
except Exception as e:
|
224
|
+
self.log_action("list_images", params, error=e)
|
225
|
+
raise RuntimeError(f"Failed to list images: {str(e)}")
|
226
|
+
|
227
|
+
def pull_image(
|
228
|
+
self, image: str, tag: str = "latest", platform: Optional[str] = None
|
229
|
+
) -> Dict:
|
230
|
+
params = {"image": image, "tag": tag, "platform": platform}
|
231
|
+
try:
|
232
|
+
img = self.client.images.pull(f"{image}:{tag}", platform=platform)
|
233
|
+
result = img.attrs
|
234
|
+
self.log_action("pull_image", params, result)
|
235
|
+
return result
|
236
|
+
except Exception as e:
|
237
|
+
self.log_action("pull_image", params, error=e)
|
238
|
+
raise RuntimeError(f"Failed to pull image: {str(e)}")
|
239
|
+
|
240
|
+
def remove_image(self, image: str, force: bool = False) -> Dict:
|
241
|
+
params = {"image": image, "force": force}
|
242
|
+
try:
|
243
|
+
self.client.images.remove(image, force=force)
|
244
|
+
result = {"removed": image}
|
245
|
+
self.log_action("remove_image", params, result)
|
246
|
+
return result
|
247
|
+
except Exception as e:
|
248
|
+
self.log_action("remove_image", params, error=e)
|
249
|
+
raise RuntimeError(f"Failed to remove image: {str(e)}")
|
250
|
+
|
251
|
+
def list_containers(self, all: bool = False) -> List[Dict]:
|
252
|
+
params = {"all": all}
|
253
|
+
try:
|
254
|
+
containers = self.client.containers.list(all=all)
|
255
|
+
result = [c.attrs for c in containers]
|
256
|
+
self.log_action("list_containers", params, result)
|
257
|
+
return result
|
258
|
+
except Exception as e:
|
259
|
+
self.log_action("list_containers", params, error=e)
|
260
|
+
raise RuntimeError(f"Failed to list containers: {str(e)}")
|
261
|
+
|
262
|
+
def run_container(
|
263
|
+
self,
|
264
|
+
image: str,
|
265
|
+
name: Optional[str] = None,
|
266
|
+
command: Optional[str] = None,
|
267
|
+
detach: bool = False,
|
268
|
+
ports: Optional[Dict[str, str]] = None,
|
269
|
+
volumes: Optional[Dict[str, Dict]] = None,
|
270
|
+
environment: Optional[Dict[str, str]] = None,
|
271
|
+
) -> Dict:
|
272
|
+
params = {
|
273
|
+
"image": image,
|
274
|
+
"name": name,
|
275
|
+
"command": command,
|
276
|
+
"detach": detach,
|
277
|
+
"ports": ports,
|
278
|
+
"volumes": volumes,
|
279
|
+
"environment": environment,
|
280
|
+
}
|
281
|
+
try:
|
282
|
+
container = self.client.containers.run(
|
283
|
+
image,
|
284
|
+
command=command,
|
285
|
+
name=name,
|
286
|
+
detach=detach,
|
287
|
+
ports=ports,
|
288
|
+
volumes=volumes,
|
289
|
+
environment=environment,
|
290
|
+
)
|
291
|
+
result = (
|
292
|
+
container.attrs if detach else {"output": container.decode("utf-8")}
|
293
|
+
)
|
294
|
+
self.log_action("run_container", params, result)
|
295
|
+
return result
|
296
|
+
except Exception as e:
|
297
|
+
self.log_action("run_container", params, error=e)
|
298
|
+
raise RuntimeError(f"Failed to run container: {str(e)}")
|
299
|
+
|
300
|
+
def stop_container(self, container_id: str, timeout: int = 10) -> Dict:
|
301
|
+
params = {"container_id": container_id, "timeout": timeout}
|
302
|
+
try:
|
303
|
+
container = self.client.containers.get(container_id)
|
304
|
+
container.stop(timeout=timeout)
|
305
|
+
result = {"stopped": container_id}
|
306
|
+
self.log_action("stop_container", params, result)
|
307
|
+
return result
|
308
|
+
except Exception as e:
|
309
|
+
self.log_action("stop_container", params, error=e)
|
310
|
+
raise RuntimeError(f"Failed to stop container: {str(e)}")
|
311
|
+
|
312
|
+
def remove_container(self, container_id: str, force: bool = False) -> Dict:
|
313
|
+
params = {"container_id": container_id, "force": force}
|
314
|
+
try:
|
315
|
+
container = self.client.containers.get(container_id)
|
316
|
+
container.remove(force=force)
|
317
|
+
result = {"removed": container_id}
|
318
|
+
self.log_action("remove_container", params, result)
|
319
|
+
return result
|
320
|
+
except Exception as e:
|
321
|
+
self.log_action("remove_container", params, error=e)
|
322
|
+
raise RuntimeError(f"Failed to remove container: {str(e)}")
|
323
|
+
|
324
|
+
def get_container_logs(self, container_id: str, tail: str = "all") -> str:
|
325
|
+
params = {"container_id": container_id, "tail": tail}
|
326
|
+
try:
|
327
|
+
container = self.client.containers.get(container_id)
|
328
|
+
logs = container.logs(tail=tail).decode("utf-8")
|
329
|
+
self.log_action("get_container_logs", params, logs)
|
330
|
+
return logs
|
331
|
+
except Exception as e:
|
332
|
+
self.log_action("get_container_logs", params, error=e)
|
333
|
+
raise RuntimeError(f"Failed to get container logs: {str(e)}")
|
334
|
+
|
335
|
+
def exec_in_container(
|
336
|
+
self, container_id: str, command: List[str], detach: bool = False
|
337
|
+
) -> Dict:
|
338
|
+
params = {"container_id": container_id, "command": command, "detach": detach}
|
339
|
+
try:
|
340
|
+
container = self.client.containers.get(container_id)
|
341
|
+
exit_code, output = container.exec_run(command, detach=detach)
|
342
|
+
result = {
|
343
|
+
"exit_code": exit_code,
|
344
|
+
"output": output.decode("utf-8") if output else None,
|
345
|
+
}
|
346
|
+
self.log_action("exec_in_container", params, result)
|
347
|
+
return result
|
348
|
+
except Exception as e:
|
349
|
+
self.log_action("exec_in_container", params, error=e)
|
350
|
+
raise RuntimeError(f"Failed to exec in container: {str(e)}")
|
351
|
+
|
352
|
+
def list_volumes(self) -> Dict:
|
353
|
+
params = {}
|
354
|
+
try:
|
355
|
+
volumes = self.client.volumes.list()
|
356
|
+
result = {"volumes": [v.attrs for v in volumes]}
|
357
|
+
self.log_action("list_volumes", params, result)
|
358
|
+
return result
|
359
|
+
except Exception as e:
|
360
|
+
self.log_action("list_volumes", params, error=e)
|
361
|
+
raise RuntimeError(f"Failed to list volumes: {str(e)}")
|
362
|
+
|
363
|
+
def create_volume(self, name: str) -> Dict:
|
364
|
+
params = {"name": name}
|
365
|
+
try:
|
366
|
+
volume = self.client.volumes.create(name=name)
|
367
|
+
result = volume.attrs
|
368
|
+
self.log_action("create_volume", params, result)
|
369
|
+
return result
|
370
|
+
except Exception as e:
|
371
|
+
self.log_action("create_volume", params, error=e)
|
372
|
+
raise RuntimeError(f"Failed to create volume: {str(e)}")
|
373
|
+
|
374
|
+
def remove_volume(self, name: str, force: bool = False) -> Dict:
|
375
|
+
params = {"name": name, "force": force}
|
376
|
+
try:
|
377
|
+
volume = self.client.volumes.get(name)
|
378
|
+
volume.remove(force=force)
|
379
|
+
result = {"removed": name}
|
380
|
+
self.log_action("remove_volume", params, result)
|
381
|
+
return result
|
382
|
+
except Exception as e:
|
383
|
+
self.log_action("remove_volume", params, error=e)
|
384
|
+
raise RuntimeError(f"Failed to remove volume: {str(e)}")
|
385
|
+
|
386
|
+
def list_networks(self) -> List[Dict]:
|
387
|
+
params = {}
|
388
|
+
try:
|
389
|
+
networks = self.client.networks.list()
|
390
|
+
result = [net.attrs for net in networks]
|
391
|
+
self.log_action("list_networks", params, result)
|
392
|
+
return result
|
393
|
+
except Exception as e:
|
394
|
+
self.log_action("list_networks", params, error=e)
|
395
|
+
raise RuntimeError(f"Failed to list networks: {str(e)}")
|
396
|
+
|
397
|
+
def create_network(self, name: str, driver: str = "bridge") -> Dict:
|
398
|
+
params = {"name": name, "driver": driver}
|
399
|
+
try:
|
400
|
+
network = self.client.networks.create(name, driver=driver)
|
401
|
+
result = network.attrs
|
402
|
+
self.log_action("create_network", params, result)
|
403
|
+
return result
|
404
|
+
except Exception as e:
|
405
|
+
self.log_action("create_network", params, error=e)
|
406
|
+
raise RuntimeError(f"Failed to create network: {str(e)}")
|
407
|
+
|
408
|
+
def remove_network(self, network_id: str) -> Dict:
|
409
|
+
params = {"network_id": network_id}
|
410
|
+
try:
|
411
|
+
network = self.client.networks.get(network_id)
|
412
|
+
network.remove()
|
413
|
+
result = {"removed": network_id}
|
414
|
+
self.log_action("remove_network", params, result)
|
415
|
+
return result
|
416
|
+
except Exception as e:
|
417
|
+
self.log_action("remove_network", params, error=e)
|
418
|
+
raise RuntimeError(f"Failed to remove network: {str(e)}")
|
419
|
+
|
420
|
+
def compose_up(
|
421
|
+
self, compose_file: str, detach: bool = True, build: bool = False
|
422
|
+
) -> str:
|
423
|
+
params = {"compose_file": compose_file, "detach": detach, "build": build}
|
424
|
+
try:
|
425
|
+
cmd = ["docker", "compose", "-f", compose_file, "up"]
|
426
|
+
if build:
|
427
|
+
cmd.append("--build")
|
428
|
+
if detach:
|
429
|
+
cmd.append("-d")
|
430
|
+
result = subprocess.run(cmd, capture_output=True, text=True)
|
431
|
+
if result.returncode != 0:
|
432
|
+
raise RuntimeError(result.stderr)
|
433
|
+
self.log_action("compose_up", params, result.stdout)
|
434
|
+
return result.stdout
|
435
|
+
except Exception as e:
|
436
|
+
self.log_action("compose_up", params, error=e)
|
437
|
+
raise RuntimeError(f"Failed to compose up: {str(e)}")
|
438
|
+
|
439
|
+
def compose_down(self, compose_file: str) -> str:
|
440
|
+
params = {"compose_file": compose_file}
|
441
|
+
try:
|
442
|
+
cmd = ["docker", "compose", "-f", compose_file, "down"]
|
443
|
+
result = subprocess.run(cmd, capture_output=True, text=True)
|
444
|
+
if result.returncode != 0:
|
445
|
+
raise RuntimeError(result.stderr)
|
446
|
+
self.log_action("compose_down", params, result.stdout)
|
447
|
+
return result.stdout
|
448
|
+
except Exception as e:
|
449
|
+
self.log_action("compose_down", params, error=e)
|
450
|
+
raise RuntimeError(f"Failed to compose down: {str(e)}")
|
451
|
+
|
452
|
+
def compose_ps(self, compose_file: str) -> str:
|
453
|
+
params = {"compose_file": compose_file}
|
454
|
+
try:
|
455
|
+
cmd = ["docker", "compose", "-f", compose_file, "ps"]
|
456
|
+
result = subprocess.run(cmd, capture_output=True, text=True)
|
457
|
+
if result.returncode != 0:
|
458
|
+
raise RuntimeError(result.stderr)
|
459
|
+
self.log_action("compose_ps", params, result.stdout)
|
460
|
+
return result.stdout
|
461
|
+
except Exception as e:
|
462
|
+
self.log_action("compose_ps", params, error=e)
|
463
|
+
raise RuntimeError(f"Failed to compose ps: {str(e)}")
|
464
|
+
|
465
|
+
def compose_logs(self, compose_file: str, service: Optional[str] = None) -> str:
|
466
|
+
params = {"compose_file": compose_file, "service": service}
|
467
|
+
try:
|
468
|
+
cmd = ["docker", "compose", "-f", compose_file, "logs"]
|
469
|
+
if service:
|
470
|
+
cmd.append(service)
|
471
|
+
result = subprocess.run(cmd, capture_output=True, text=True)
|
472
|
+
if result.returncode != 0:
|
473
|
+
raise RuntimeError(result.stderr)
|
474
|
+
self.log_action("compose_logs", params, result.stdout)
|
475
|
+
return result.stdout
|
476
|
+
except Exception as e:
|
477
|
+
self.log_action("compose_logs", params, error=e)
|
478
|
+
raise RuntimeError(f"Failed to compose logs: {str(e)}")
|
479
|
+
|
480
|
+
def init_swarm(self, advertise_addr: Optional[str] = None) -> Dict:
|
481
|
+
params = {"advertise_addr": advertise_addr}
|
482
|
+
try:
|
483
|
+
swarm_id = self.client.swarm.init(advertise_addr=advertise_addr)
|
484
|
+
result = {"swarm_id": swarm_id}
|
485
|
+
self.log_action("init_swarm", params, result)
|
486
|
+
return result
|
487
|
+
except Exception as e:
|
488
|
+
self.log_action("init_swarm", params, error=e)
|
489
|
+
raise RuntimeError(f"Failed to init swarm: {str(e)}")
|
490
|
+
|
491
|
+
def leave_swarm(self, force: bool = False) -> Dict:
|
492
|
+
params = {"force": force}
|
493
|
+
try:
|
494
|
+
self.client.swarm.leave(force=force)
|
495
|
+
result = {"left": True}
|
496
|
+
self.log_action("leave_swarm", params, result)
|
497
|
+
return result
|
498
|
+
except Exception as e:
|
499
|
+
self.log_action("leave_swarm", params, error=e)
|
500
|
+
raise RuntimeError(f"Failed to leave swarm: {str(e)}")
|
501
|
+
|
502
|
+
def list_nodes(self) -> List[Dict]:
|
503
|
+
params = {}
|
504
|
+
try:
|
505
|
+
nodes = self.client.nodes.list()
|
506
|
+
result = [node.attrs for node in nodes]
|
507
|
+
self.log_action("list_nodes", params, result)
|
508
|
+
return result
|
509
|
+
except Exception as e:
|
510
|
+
self.log_action("list_nodes", params, error=e)
|
511
|
+
raise RuntimeError(f"Failed to list nodes: {str(e)}")
|
512
|
+
|
513
|
+
def list_services(self) -> List[Dict]:
|
514
|
+
params = {}
|
515
|
+
try:
|
516
|
+
services = self.client.services.list()
|
517
|
+
result = [service.attrs for service in services]
|
518
|
+
self.log_action("list_services", params, result)
|
519
|
+
return result
|
520
|
+
except Exception as e:
|
521
|
+
self.log_action("list_services", params, error=e)
|
522
|
+
raise RuntimeError(f"Failed to list services: {str(e)}")
|
523
|
+
|
524
|
+
def create_service(
|
525
|
+
self,
|
526
|
+
name: str,
|
527
|
+
image: str,
|
528
|
+
replicas: int = 1,
|
529
|
+
ports: Optional[Dict[str, str]] = None,
|
530
|
+
mounts: Optional[List[str]] = None,
|
531
|
+
) -> Dict:
|
532
|
+
params = {
|
533
|
+
"name": name,
|
534
|
+
"image": image,
|
535
|
+
"replicas": replicas,
|
536
|
+
"ports": ports,
|
537
|
+
"mounts": mounts,
|
538
|
+
}
|
539
|
+
try:
|
540
|
+
mode = {"mode": "replicated", "replicas": replicas}
|
541
|
+
target_ports = [docker.types.EndpointSpec(ports=ports)] if ports else None
|
542
|
+
service = self.client.services.create(
|
543
|
+
image,
|
544
|
+
name=name,
|
545
|
+
mode=mode,
|
546
|
+
mounts=mounts,
|
547
|
+
endpoint_spec=target_ports[0] if target_ports else None,
|
548
|
+
)
|
549
|
+
result = service.attrs
|
550
|
+
self.log_action("create_service", params, result)
|
551
|
+
return result
|
552
|
+
except Exception as e:
|
553
|
+
self.log_action("create_service", params, error=e)
|
554
|
+
raise RuntimeError(f"Failed to create service: {str(e)}")
|
555
|
+
|
556
|
+
def remove_service(self, service_id: str) -> Dict:
|
557
|
+
params = {"service_id": service_id}
|
558
|
+
try:
|
559
|
+
service = self.client.services.get(service_id)
|
560
|
+
service.remove()
|
561
|
+
result = {"removed": service_id}
|
562
|
+
self.log_action("remove_service", params, result)
|
563
|
+
return result
|
564
|
+
except Exception as e:
|
565
|
+
self.log_action("remove_service", params, error=e)
|
566
|
+
raise RuntimeError(f"Failed to remove service: {str(e)}")
|
567
|
+
|
568
|
+
|
569
|
+
class PodmanManager(ContainerManagerBase):
|
570
|
+
def __init__(self, silent: bool = False, log_file: str = None):
|
571
|
+
super().__init__(silent, log_file)
|
572
|
+
if PodmanClient is None:
|
573
|
+
raise ImportError("Please install podman-py: pip install podman")
|
574
|
+
try:
|
575
|
+
self.client = PodmanClient()
|
576
|
+
except PodmanError as e:
|
577
|
+
self.logger.error(f"Failed to connect to Podman daemon: {str(e)}")
|
578
|
+
raise RuntimeError(f"Failed to connect to Podman: {str(e)}")
|
579
|
+
|
580
|
+
def get_version(self) -> Dict:
|
581
|
+
params = {}
|
582
|
+
try:
|
583
|
+
result = self.client.version()
|
584
|
+
self.log_action("get_version", params, result)
|
585
|
+
return result
|
586
|
+
except Exception as e:
|
587
|
+
self.log_action("get_version", params, error=e)
|
588
|
+
raise RuntimeError(f"Failed to get version: {str(e)}")
|
589
|
+
|
590
|
+
def get_info(self) -> Dict:
|
591
|
+
params = {}
|
592
|
+
try:
|
593
|
+
result = self.client.info()
|
594
|
+
self.log_action("get_info", params, result)
|
595
|
+
return result
|
596
|
+
except Exception as e:
|
597
|
+
self.log_action("get_info", params, error=e)
|
598
|
+
raise RuntimeError(f"Failed to get info: {str(e)}")
|
599
|
+
|
600
|
+
def list_images(self) -> List[Dict]:
|
601
|
+
params = {}
|
602
|
+
try:
|
603
|
+
images = self.client.images.list()
|
604
|
+
result = [img.attrs for img in images]
|
605
|
+
self.log_action("list_images", params, result)
|
606
|
+
return result
|
607
|
+
except Exception as e:
|
608
|
+
self.log_action("list_images", params, error=e)
|
609
|
+
raise RuntimeError(f"Failed to list images: {str(e)}")
|
610
|
+
|
611
|
+
def pull_image(
|
612
|
+
self, image: str, tag: str = "latest", platform: Optional[str] = None
|
613
|
+
) -> Dict:
|
614
|
+
params = {"image": image, "tag": tag, "platform": platform}
|
615
|
+
try:
|
616
|
+
img = self.client.images.pull(f"{image}:{tag}", platform=platform)
|
617
|
+
result = img[0].attrs if isinstance(img, list) else img.attrs
|
618
|
+
self.log_action("pull_image", params, result)
|
619
|
+
return result
|
620
|
+
except Exception as e:
|
621
|
+
self.log_action("pull_image", params, error=e)
|
622
|
+
raise RuntimeError(f"Failed to pull image: {str(e)}")
|
623
|
+
|
624
|
+
def remove_image(self, image: str, force: bool = False) -> Dict:
|
625
|
+
params = {"image": image, "force": force}
|
626
|
+
try:
|
627
|
+
self.client.images.remove(image, force=force)
|
628
|
+
result = {"removed": image}
|
629
|
+
self.log_action("remove_image", params, result)
|
630
|
+
return result
|
631
|
+
except Exception as e:
|
632
|
+
self.log_action("remove_image", params, error=e)
|
633
|
+
raise RuntimeError(f"Failed to remove image: {str(e)}")
|
634
|
+
|
635
|
+
def list_containers(self, all: bool = False) -> List[Dict]:
|
636
|
+
params = {"all": all}
|
637
|
+
try:
|
638
|
+
containers = self.client.containers.list(all=all)
|
639
|
+
result = [c.attrs for c in containers]
|
640
|
+
self.log_action("list_containers", params, result)
|
641
|
+
return result
|
642
|
+
except Exception as e:
|
643
|
+
self.log_action("list_containers", params, error=e)
|
644
|
+
raise RuntimeError(f"Failed to list containers: {str(e)}")
|
645
|
+
|
646
|
+
def run_container(
|
647
|
+
self,
|
648
|
+
image: str,
|
649
|
+
name: Optional[str] = None,
|
650
|
+
command: Optional[str] = None,
|
651
|
+
detach: bool = False,
|
652
|
+
ports: Optional[Dict[str, str]] = None,
|
653
|
+
volumes: Optional[Dict[str, Dict]] = None,
|
654
|
+
environment: Optional[Dict[str, str]] = None,
|
655
|
+
) -> Dict:
|
656
|
+
params = {
|
657
|
+
"image": image,
|
658
|
+
"name": name,
|
659
|
+
"command": command,
|
660
|
+
"detach": detach,
|
661
|
+
"ports": ports,
|
662
|
+
"volumes": volumes,
|
663
|
+
"environment": environment,
|
664
|
+
}
|
665
|
+
try:
|
666
|
+
container = self.client.containers.run(
|
667
|
+
image,
|
668
|
+
command=command,
|
669
|
+
name=name,
|
670
|
+
detach=detach,
|
671
|
+
ports=ports,
|
672
|
+
volumes=volumes,
|
673
|
+
environment=environment,
|
674
|
+
)
|
675
|
+
result = (
|
676
|
+
container.attrs if detach else {"output": container.decode("utf-8")}
|
677
|
+
)
|
678
|
+
self.log_action("run_container", params, result)
|
679
|
+
return result
|
680
|
+
except Exception as e:
|
681
|
+
self.log_action("run_container", params, error=e)
|
682
|
+
raise RuntimeError(f"Failed to run container: {str(e)}")
|
683
|
+
|
684
|
+
def stop_container(self, container_id: str, timeout: int = 10) -> Dict:
|
685
|
+
params = {"container_id": container_id, "timeout": timeout}
|
686
|
+
try:
|
687
|
+
container = self.client.containers.get(container_id)
|
688
|
+
container.stop(timeout=timeout)
|
689
|
+
result = {"stopped": container_id}
|
690
|
+
self.log_action("stop_container", params, result)
|
691
|
+
return result
|
692
|
+
except Exception as e:
|
693
|
+
self.log_action("stop_container", params, error=e)
|
694
|
+
raise RuntimeError(f"Failed to stop container: {str(e)}")
|
695
|
+
|
696
|
+
def remove_container(self, container_id: str, force: bool = False) -> Dict:
|
697
|
+
params = {"container_id": container_id, "force": force}
|
698
|
+
try:
|
699
|
+
container = self.client.containers.get(container_id)
|
700
|
+
container.remove(force=force)
|
701
|
+
result = {"removed": container_id}
|
702
|
+
self.log_action("remove_container", params, result)
|
703
|
+
return result
|
704
|
+
except Exception as e:
|
705
|
+
self.log_action("remove_container", params, error=e)
|
706
|
+
raise RuntimeError(f"Failed to remove container: {str(e)}")
|
707
|
+
|
708
|
+
def get_container_logs(self, container_id: str, tail: str = "all") -> str:
|
709
|
+
params = {"container_id": container_id, "tail": tail}
|
710
|
+
try:
|
711
|
+
container = self.client.containers.get(container_id)
|
712
|
+
logs = container.logs(tail=tail).decode("utf-8")
|
713
|
+
self.log_action("get_container_logs", params, logs)
|
714
|
+
return logs
|
715
|
+
except Exception as e:
|
716
|
+
self.log_action("get_container_logs", params, error=e)
|
717
|
+
raise RuntimeError(f"Failed to get container logs: {str(e)}")
|
718
|
+
|
719
|
+
def exec_in_container(
|
720
|
+
self, container_id: str, command: List[str], detach: bool = False
|
721
|
+
) -> Dict:
|
722
|
+
params = {"container_id": container_id, "command": command, "detach": detach}
|
723
|
+
try:
|
724
|
+
container = self.client.containers.get(container_id)
|
725
|
+
exit_code, output = container.exec_run(command, detach=detach)
|
726
|
+
result = {
|
727
|
+
"exit_code": exit_code,
|
728
|
+
"output": output.decode("utf-8") if output else None,
|
729
|
+
}
|
730
|
+
self.log_action("exec_in_container", params, result)
|
731
|
+
return result
|
732
|
+
except Exception as e:
|
733
|
+
self.log_action("exec_in_container", params, error=e)
|
734
|
+
raise RuntimeError(f"Failed to exec in container: {str(e)}")
|
735
|
+
|
736
|
+
def list_volumes(self) -> Dict:
|
737
|
+
params = {}
|
738
|
+
try:
|
739
|
+
volumes = self.client.volumes.list()
|
740
|
+
result = {"volumes": [v.attrs for v in volumes]}
|
741
|
+
self.log_action("list_volumes", params, result)
|
742
|
+
return result
|
743
|
+
except Exception as e:
|
744
|
+
self.log_action("list_volumes", params, error=e)
|
745
|
+
raise RuntimeError(f"Failed to list volumes: {str(e)}")
|
746
|
+
|
747
|
+
def create_volume(self, name: str) -> Dict:
|
748
|
+
params = {"name": name}
|
749
|
+
try:
|
750
|
+
volume = self.client.volumes.create(name=name)
|
751
|
+
result = volume.attrs
|
752
|
+
self.log_action("create_volume", params, result)
|
753
|
+
return result
|
754
|
+
except Exception as e:
|
755
|
+
self.log_action("create_volume", params, error=e)
|
756
|
+
raise RuntimeError(f"Failed to create volume: {str(e)}")
|
757
|
+
|
758
|
+
def remove_volume(self, name: str, force: bool = False) -> Dict:
|
759
|
+
params = {"name": name, "force": force}
|
760
|
+
try:
|
761
|
+
volume = self.client.volumes.get(name)
|
762
|
+
volume.remove(force=force)
|
763
|
+
result = {"removed": name}
|
764
|
+
self.log_action("remove_volume", params, result)
|
765
|
+
return result
|
766
|
+
except Exception as e:
|
767
|
+
self.log_action("remove_volume", params, error=e)
|
768
|
+
raise RuntimeError(f"Failed to remove volume: {str(e)}")
|
769
|
+
|
770
|
+
def list_networks(self) -> List[Dict]:
|
771
|
+
params = {}
|
772
|
+
try:
|
773
|
+
networks = self.client.networks.list()
|
774
|
+
result = [net.attrs for net in networks]
|
775
|
+
self.log_action("list_networks", params, result)
|
776
|
+
return result
|
777
|
+
except Exception as e:
|
778
|
+
self.log_action("list_networks", params, error=e)
|
779
|
+
raise RuntimeError(f"Failed to list networks: {str(e)}")
|
780
|
+
|
781
|
+
def create_network(self, name: str, driver: str = "bridge") -> Dict:
|
782
|
+
params = {"name": name, "driver": driver}
|
783
|
+
try:
|
784
|
+
network = self.client.networks.create(name, driver=driver)
|
785
|
+
result = network.attrs
|
786
|
+
self.log_action("create_network", params, result)
|
787
|
+
return result
|
788
|
+
except Exception as e:
|
789
|
+
self.log_action("create_network", params, error=e)
|
790
|
+
raise RuntimeError(f"Failed to create network: {str(e)}")
|
791
|
+
|
792
|
+
def remove_network(self, network_id: str) -> Dict:
|
793
|
+
params = {"network_id": network_id}
|
794
|
+
try:
|
795
|
+
network = self.client.networks.get(network_id)
|
796
|
+
network.remove()
|
797
|
+
result = {"removed": network_id}
|
798
|
+
self.log_action("remove_network", params, result)
|
799
|
+
return result
|
800
|
+
except Exception as e:
|
801
|
+
self.log_action("remove_network", params, error=e)
|
802
|
+
raise RuntimeError(f"Failed to remove network: {str(e)}")
|
803
|
+
|
804
|
+
def compose_up(
|
805
|
+
self, compose_file: str, detach: bool = True, build: bool = False
|
806
|
+
) -> str:
|
807
|
+
params = {"compose_file": compose_file, "detach": detach, "build": build}
|
808
|
+
try:
|
809
|
+
cmd = ["podman-compose", "-f", compose_file, "up"]
|
810
|
+
if build:
|
811
|
+
cmd.append("--build")
|
812
|
+
if detach:
|
813
|
+
cmd.append("-d")
|
814
|
+
result = subprocess.run(cmd, capture_output=True, text=True)
|
815
|
+
if result.returncode != 0:
|
816
|
+
raise RuntimeError(result.stderr)
|
817
|
+
self.log_action("compose_up", params, result.stdout)
|
818
|
+
return result.stdout
|
819
|
+
except Exception as e:
|
820
|
+
self.log_action("compose_up", params, error=e)
|
821
|
+
raise RuntimeError(f"Failed to compose up: {str(e)}")
|
822
|
+
|
823
|
+
def compose_down(self, compose_file: str) -> str:
|
824
|
+
params = {"compose_file": compose_file}
|
825
|
+
try:
|
826
|
+
cmd = ["podman-compose", "-f", compose_file, "down"]
|
827
|
+
result = subprocess.run(cmd, capture_output=True, text=True)
|
828
|
+
if result.returncode != 0:
|
829
|
+
raise RuntimeError(result.stderr)
|
830
|
+
self.log_action("compose_down", params, result.stdout)
|
831
|
+
return result.stdout
|
832
|
+
except Exception as e:
|
833
|
+
self.log_action("compose_down", params, error=e)
|
834
|
+
raise RuntimeError(f"Failed to compose down: {str(e)}")
|
835
|
+
|
836
|
+
def compose_ps(self, compose_file: str) -> str:
|
837
|
+
params = {"compose_file": compose_file}
|
838
|
+
try:
|
839
|
+
cmd = ["podman-compose", "-f", compose_file, "ps"]
|
840
|
+
result = subprocess.run(cmd, capture_output=True, text=True)
|
841
|
+
if result.returncode != 0:
|
842
|
+
raise RuntimeError(result.stderr)
|
843
|
+
self.log_action("compose_ps", params, result.stdout)
|
844
|
+
return result.stdout
|
845
|
+
except Exception as e:
|
846
|
+
self.log_action("compose_ps", params, error=e)
|
847
|
+
raise RuntimeError(f"Failed to compose ps: {str(e)}")
|
848
|
+
|
849
|
+
def compose_logs(self, compose_file: str, service: Optional[str] = None) -> str:
|
850
|
+
params = {"compose_file": compose_file, "service": service}
|
851
|
+
try:
|
852
|
+
cmd = ["podman-compose", "-f", compose_file, "logs"]
|
853
|
+
if service:
|
854
|
+
cmd.append(service)
|
855
|
+
result = subprocess.run(cmd, capture_output=True, text=True)
|
856
|
+
if result.returncode != 0:
|
857
|
+
raise RuntimeError(result.stderr)
|
858
|
+
self.log_action("compose_logs", params, result.stdout)
|
859
|
+
return result.stdout
|
860
|
+
except Exception as e:
|
861
|
+
self.log_action("compose_logs", params, error=e)
|
862
|
+
raise RuntimeError(f"Failed to compose logs: {str(e)}")
|
863
|
+
|
864
|
+
|
865
|
+
def create_manager(
|
866
|
+
manager_type: str, silent: bool = False, log_file: str = None
|
867
|
+
) -> ContainerManagerBase:
|
868
|
+
if manager_type.lower() == "docker" or manager_type.lower() == "swarm":
|
869
|
+
return DockerManager(silent=silent, log_file=log_file)
|
870
|
+
elif manager_type.lower() == "podman":
|
871
|
+
return PodmanManager(silent=silent, log_file=log_file)
|
872
|
+
else:
|
873
|
+
raise ValueError(f"Unsupported container manager type: {manager_type}")
|
874
|
+
|
875
|
+
|
876
|
+
def usage():
|
877
|
+
print(
|
878
|
+
"""
|
879
|
+
Container Manager: A tool to manage containers with Docker, Podman, and Docker Swarm!
|
880
|
+
|
881
|
+
Usage:
|
882
|
+
-h | --help [ See usage for script ]
|
883
|
+
-s | --silent [ Suppress output ]
|
884
|
+
-m | --manager <type> [ docker, podman, swarm; default: docker ]
|
885
|
+
--log-file <path> [ Log to specified file (default: container_manager.log in script dir) ]
|
886
|
+
|
887
|
+
Actions:
|
888
|
+
--get-version [ Get version info ]
|
889
|
+
--get-info [ Get system info ]
|
890
|
+
--list-images [ List images ]
|
891
|
+
--pull-image <image> [ Pull image, e.g., nginx ]
|
892
|
+
--tag <tag> [ Tag, default: latest ]
|
893
|
+
--platform <plat> [ Platform, e.g., linux/amd64 ]
|
894
|
+
--remove-image <image> [ Remove image ]
|
895
|
+
--force [ Force removal (global for remove actions) ]
|
896
|
+
--list-containers [ List containers ]
|
897
|
+
--all [ Show all containers ]
|
898
|
+
--run-container <image> [ Run container ]
|
899
|
+
--name <name> [ Container name ]
|
900
|
+
--command <cmd> [ Command to run ]
|
901
|
+
--detach [ Detach mode ]
|
902
|
+
--ports <ports> [ Ports, comma-separated host:container, e.g., 8080:80,8443:443 ]
|
903
|
+
--volumes <vols> [ Volumes, comma-separated host:container:mode, mode default rw ]
|
904
|
+
--environment <env> [ Env vars, comma-separated KEY=val ]
|
905
|
+
--stop-container <id> [ Stop container ]
|
906
|
+
--timeout <sec> [ Timeout, default 10 ]
|
907
|
+
--remove-container <id>[ Remove container ]
|
908
|
+
--force [ Force ]
|
909
|
+
--get-container-logs <id> [ Get logs ]
|
910
|
+
--tail <tail> [ Tail lines, default all ]
|
911
|
+
--exec-in-container <id> [ Exec command ]
|
912
|
+
--exec-command <cmd> [ Command, space-separated "ls -l /" ]
|
913
|
+
--exec-detach [ Detach exec ]
|
914
|
+
--list-volumes [ List volumes ]
|
915
|
+
--create-volume <name> [ Create volume ]
|
916
|
+
--remove-volume <name> [ Remove volume ]
|
917
|
+
--force [ Force ]
|
918
|
+
--list-networks [ List networks ]
|
919
|
+
--create-network <name>[ Create network ]
|
920
|
+
--driver <driver> [ Driver, default bridge ]
|
921
|
+
--remove-network <id> [ Remove network ]
|
922
|
+
--compose-up <file> [ Compose up ]
|
923
|
+
--build [ Build images ]
|
924
|
+
--detach [ Detach mode, default true ]
|
925
|
+
--compose-down <file> [ Compose down ]
|
926
|
+
--compose-ps <file> [ Compose ps ]
|
927
|
+
--compose-logs <file> [ Compose logs ]
|
928
|
+
--service <service> [ Specific service ]
|
929
|
+
--init-swarm [ Init swarm ]
|
930
|
+
--advertise-addr <addr> [ Advertise address ]
|
931
|
+
--leave-swarm [ Leave swarm ]
|
932
|
+
--force [ Force ]
|
933
|
+
--list-nodes [ List swarm nodes ]
|
934
|
+
--list-services [ List swarm services ]
|
935
|
+
--create-service <name>[ Create service ]
|
936
|
+
--image <image> [ Image for service ]
|
937
|
+
--replicas <n> [ Replicas, default 1 ]
|
938
|
+
--ports <ports> [ Ports, same as run-container ]
|
939
|
+
--mounts <mounts> [ Mounts, comma-separated source:target:mode ]
|
940
|
+
--remove-service <id> [ Remove service ]
|
941
|
+
|
942
|
+
Example:
|
943
|
+
container_manager.py --manager docker --pull-image nginx --tag latest --list-containers --all --log-file /path/to/log.log
|
944
|
+
"""
|
945
|
+
)
|
946
|
+
|
947
|
+
|
948
|
+
def main(argv):
|
949
|
+
get_version = False
|
950
|
+
get_info = False
|
951
|
+
list_images = False
|
952
|
+
pull_image = False
|
953
|
+
pull_image_str = None
|
954
|
+
tag = "latest"
|
955
|
+
platform = None
|
956
|
+
remove_image = False
|
957
|
+
remove_image_str = None
|
958
|
+
force = False
|
959
|
+
list_containers = False
|
960
|
+
all_containers = False
|
961
|
+
run_container = False
|
962
|
+
run_image = None
|
963
|
+
name = None
|
964
|
+
command = None
|
965
|
+
detach = False
|
966
|
+
ports_str = None
|
967
|
+
volumes_str = None
|
968
|
+
environment_str = None
|
969
|
+
stop_container = False
|
970
|
+
stop_container_id = None
|
971
|
+
timeout = 10
|
972
|
+
remove_container = False
|
973
|
+
remove_container_id = None
|
974
|
+
get_container_logs = False
|
975
|
+
container_logs_id = None
|
976
|
+
tail = "all"
|
977
|
+
exec_in_container = False
|
978
|
+
exec_container_id = None
|
979
|
+
exec_command = None
|
980
|
+
exec_detach = False
|
981
|
+
list_volumes = False
|
982
|
+
create_volume = False
|
983
|
+
create_volume_name = None
|
984
|
+
remove_volume = False
|
985
|
+
remove_volume_name = None
|
986
|
+
list_networks = False
|
987
|
+
create_network = False
|
988
|
+
create_network_name = None
|
989
|
+
driver = "bridge"
|
990
|
+
remove_network = False
|
991
|
+
remove_network_id = None
|
992
|
+
compose_up = False
|
993
|
+
compose_up_file = None
|
994
|
+
compose_build = False
|
995
|
+
compose_detach = True
|
996
|
+
compose_down = False
|
997
|
+
compose_down_file = None
|
998
|
+
compose_ps = False
|
999
|
+
compose_ps_file = None
|
1000
|
+
compose_logs = False
|
1001
|
+
compose_logs_file = None
|
1002
|
+
compose_service = None
|
1003
|
+
init_swarm = False
|
1004
|
+
advertise_addr = None
|
1005
|
+
leave_swarm = False
|
1006
|
+
list_nodes = False
|
1007
|
+
list_services = False
|
1008
|
+
create_service = False
|
1009
|
+
create_service_name = None
|
1010
|
+
service_image = None
|
1011
|
+
replicas = 1
|
1012
|
+
mounts_str = None
|
1013
|
+
remove_service = False
|
1014
|
+
remove_service_id = None
|
1015
|
+
manager_type = "docker"
|
1016
|
+
silent = False
|
1017
|
+
log_file = None
|
1018
|
+
|
1019
|
+
try:
|
1020
|
+
opts, _ = getopt.getopt(
|
1021
|
+
argv,
|
1022
|
+
"hsm:",
|
1023
|
+
[
|
1024
|
+
"help",
|
1025
|
+
"silent",
|
1026
|
+
"manager=",
|
1027
|
+
"log-file=",
|
1028
|
+
"get-version",
|
1029
|
+
"get-info",
|
1030
|
+
"list-images",
|
1031
|
+
"pull-image=",
|
1032
|
+
"tag=",
|
1033
|
+
"platform=",
|
1034
|
+
"remove-image=",
|
1035
|
+
"force",
|
1036
|
+
"list-containers",
|
1037
|
+
"all",
|
1038
|
+
"run-container=",
|
1039
|
+
"name=",
|
1040
|
+
"command=",
|
1041
|
+
"detach",
|
1042
|
+
"ports=",
|
1043
|
+
"volumes=",
|
1044
|
+
"environment=",
|
1045
|
+
"stop-container=",
|
1046
|
+
"timeout=",
|
1047
|
+
"remove-container=",
|
1048
|
+
"get-container-logs=",
|
1049
|
+
"tail=",
|
1050
|
+
"exec-in-container=",
|
1051
|
+
"exec-command=",
|
1052
|
+
"exec-detach",
|
1053
|
+
"list-volumes",
|
1054
|
+
"create-volume=",
|
1055
|
+
"remove-volume=",
|
1056
|
+
"list-networks",
|
1057
|
+
"create-network=",
|
1058
|
+
"driver=",
|
1059
|
+
"remove-network=",
|
1060
|
+
"compose-up=",
|
1061
|
+
"build",
|
1062
|
+
"compose-down=",
|
1063
|
+
"compose-ps=",
|
1064
|
+
"compose-logs=",
|
1065
|
+
"service=",
|
1066
|
+
"init-swarm",
|
1067
|
+
"advertise-addr=",
|
1068
|
+
"leave-swarm",
|
1069
|
+
"list-nodes",
|
1070
|
+
"list-services",
|
1071
|
+
"create-service=",
|
1072
|
+
"image=",
|
1073
|
+
"replicas=",
|
1074
|
+
"mounts=",
|
1075
|
+
"remove-service=",
|
1076
|
+
],
|
1077
|
+
)
|
1078
|
+
except getopt.GetoptError:
|
1079
|
+
usage()
|
1080
|
+
sys.exit(2)
|
1081
|
+
|
1082
|
+
for opt, arg in opts:
|
1083
|
+
if opt in ("-h", "--help"):
|
1084
|
+
usage()
|
1085
|
+
sys.exit()
|
1086
|
+
elif opt in ("-s", "--silent"):
|
1087
|
+
silent = True
|
1088
|
+
elif opt in ("-m", "--manager"):
|
1089
|
+
manager_type = arg
|
1090
|
+
elif opt == "--log-file":
|
1091
|
+
log_file = arg
|
1092
|
+
elif opt == "--get-version":
|
1093
|
+
get_version = True
|
1094
|
+
elif opt == "--get-info":
|
1095
|
+
get_info = True
|
1096
|
+
elif opt == "--list-images":
|
1097
|
+
list_images = True
|
1098
|
+
elif opt == "--pull-image":
|
1099
|
+
pull_image = True
|
1100
|
+
pull_image_str = arg
|
1101
|
+
elif opt == "--tag":
|
1102
|
+
tag = arg
|
1103
|
+
elif opt == "--platform":
|
1104
|
+
platform = arg
|
1105
|
+
elif opt == "--remove-image":
|
1106
|
+
remove_image = True
|
1107
|
+
remove_image_str = arg
|
1108
|
+
elif opt == "--force":
|
1109
|
+
force = True
|
1110
|
+
elif opt == "--list-containers":
|
1111
|
+
list_containers = True
|
1112
|
+
elif opt == "--all":
|
1113
|
+
all_containers = True
|
1114
|
+
elif opt == "--run-container":
|
1115
|
+
run_container = True
|
1116
|
+
run_image = arg
|
1117
|
+
elif opt == "--name":
|
1118
|
+
name = arg
|
1119
|
+
elif opt == "--command":
|
1120
|
+
command = arg
|
1121
|
+
elif opt == "--detach":
|
1122
|
+
detach = True
|
1123
|
+
elif opt == "--ports":
|
1124
|
+
ports_str = arg
|
1125
|
+
elif opt == "--volumes":
|
1126
|
+
volumes_str = arg
|
1127
|
+
elif opt == "--environment":
|
1128
|
+
environment_str = arg
|
1129
|
+
elif opt == "--stop-container":
|
1130
|
+
stop_container = True
|
1131
|
+
stop_container_id = arg
|
1132
|
+
elif opt == "--timeout":
|
1133
|
+
timeout = int(arg)
|
1134
|
+
elif opt == "--remove-container":
|
1135
|
+
remove_container = True
|
1136
|
+
remove_container_id = arg
|
1137
|
+
elif opt == "--get-container-logs":
|
1138
|
+
get_container_logs = True
|
1139
|
+
container_logs_id = arg
|
1140
|
+
elif opt == "--tail":
|
1141
|
+
tail = arg
|
1142
|
+
elif opt == "--exec-in-container":
|
1143
|
+
exec_in_container = True
|
1144
|
+
exec_container_id = arg
|
1145
|
+
elif opt == "--exec-command":
|
1146
|
+
exec_command = arg
|
1147
|
+
elif opt == "--exec-detach":
|
1148
|
+
exec_detach = True
|
1149
|
+
elif opt == "--list-volumes":
|
1150
|
+
list_volumes = True
|
1151
|
+
elif opt == "--create-volume":
|
1152
|
+
create_volume = True
|
1153
|
+
create_volume_name = arg
|
1154
|
+
elif opt == "--remove-volume":
|
1155
|
+
remove_volume = True
|
1156
|
+
remove_volume_name = arg
|
1157
|
+
elif opt == "--list-networks":
|
1158
|
+
list_networks = True
|
1159
|
+
elif opt == "--create-network":
|
1160
|
+
create_network = True
|
1161
|
+
create_network_name = arg
|
1162
|
+
elif opt == "--driver":
|
1163
|
+
driver = arg
|
1164
|
+
elif opt == "--remove-network":
|
1165
|
+
remove_network = True
|
1166
|
+
remove_network_id = arg
|
1167
|
+
elif opt == "--compose-up":
|
1168
|
+
compose_up = True
|
1169
|
+
compose_up_file = arg
|
1170
|
+
elif opt == "--build":
|
1171
|
+
compose_build = True
|
1172
|
+
elif opt == "--compose-down":
|
1173
|
+
compose_down = True
|
1174
|
+
compose_down_file = arg
|
1175
|
+
elif opt == "--compose-ps":
|
1176
|
+
compose_ps = True
|
1177
|
+
compose_ps_file = arg
|
1178
|
+
elif opt == "--compose-logs":
|
1179
|
+
compose_logs = True
|
1180
|
+
compose_logs_file = arg
|
1181
|
+
elif opt == "--service":
|
1182
|
+
compose_service = arg
|
1183
|
+
elif opt == "--init-swarm":
|
1184
|
+
init_swarm = True
|
1185
|
+
elif opt == "--advertise-addr":
|
1186
|
+
advertise_addr = arg
|
1187
|
+
elif opt == "--leave-swarm":
|
1188
|
+
leave_swarm = True
|
1189
|
+
elif opt == "--list-nodes":
|
1190
|
+
list_nodes = True
|
1191
|
+
elif opt == "--list-services":
|
1192
|
+
list_services = True
|
1193
|
+
elif opt == "--create-service":
|
1194
|
+
create_service = True
|
1195
|
+
create_service_name = arg
|
1196
|
+
elif opt == "--image":
|
1197
|
+
service_image = arg
|
1198
|
+
elif opt == "--replicas":
|
1199
|
+
replicas = int(arg)
|
1200
|
+
elif opt == "--mounts":
|
1201
|
+
mounts_str = arg
|
1202
|
+
elif opt == "--remove-service":
|
1203
|
+
remove_service = True
|
1204
|
+
remove_service_id = arg
|
1205
|
+
|
1206
|
+
manager = create_manager(manager_type, silent, log_file)
|
1207
|
+
|
1208
|
+
if get_version:
|
1209
|
+
print(json.dumps(manager.get_version(), indent=2))
|
1210
|
+
|
1211
|
+
if get_info:
|
1212
|
+
print(json.dumps(manager.get_info(), indent=2))
|
1213
|
+
|
1214
|
+
if list_images:
|
1215
|
+
print(json.dumps(manager.list_images(), indent=2))
|
1216
|
+
|
1217
|
+
if pull_image:
|
1218
|
+
if not pull_image_str:
|
1219
|
+
raise ValueError("Image required for pull-image")
|
1220
|
+
print(json.dumps(manager.pull_image(pull_image_str, tag, platform), indent=2))
|
1221
|
+
|
1222
|
+
if remove_image:
|
1223
|
+
if not remove_image_str:
|
1224
|
+
raise ValueError("Image required for remove-image")
|
1225
|
+
print(json.dumps(manager.remove_image(remove_image_str, force), indent=2))
|
1226
|
+
|
1227
|
+
if list_containers:
|
1228
|
+
print(json.dumps(manager.list_containers(all_containers), indent=2))
|
1229
|
+
|
1230
|
+
if run_container:
|
1231
|
+
if not run_image:
|
1232
|
+
raise ValueError("Image required for run-container")
|
1233
|
+
ports = None
|
1234
|
+
if ports_str:
|
1235
|
+
ports = {}
|
1236
|
+
for p in ports_str.split(","):
|
1237
|
+
host, cont = p.split(":")
|
1238
|
+
ports[cont + "/tcp"] = host
|
1239
|
+
volumes = None
|
1240
|
+
if volumes_str:
|
1241
|
+
volumes = {}
|
1242
|
+
for v in volumes_str.split(","):
|
1243
|
+
parts = v.split(":")
|
1244
|
+
host = parts[0]
|
1245
|
+
cont = parts[1]
|
1246
|
+
mode = parts[2] if len(parts) > 2 else "rw"
|
1247
|
+
volumes[host] = {"bind": cont, "mode": mode}
|
1248
|
+
env = None
|
1249
|
+
if environment_str:
|
1250
|
+
env = dict(e.split("=") for e in environment_str.split(","))
|
1251
|
+
print(
|
1252
|
+
json.dumps(
|
1253
|
+
manager.run_container(
|
1254
|
+
run_image, name, command, detach, ports, volumes, env
|
1255
|
+
),
|
1256
|
+
indent=2,
|
1257
|
+
)
|
1258
|
+
)
|
1259
|
+
|
1260
|
+
if stop_container:
|
1261
|
+
if not stop_container_id:
|
1262
|
+
raise ValueError("Container ID required for stop-container")
|
1263
|
+
print(json.dumps(manager.stop_container(stop_container_id, timeout), indent=2))
|
1264
|
+
|
1265
|
+
if remove_container:
|
1266
|
+
if not remove_container_id:
|
1267
|
+
raise ValueError("Container ID required for remove-container")
|
1268
|
+
print(
|
1269
|
+
json.dumps(manager.remove_container(remove_container_id, force), indent=2)
|
1270
|
+
)
|
1271
|
+
|
1272
|
+
if get_container_logs:
|
1273
|
+
if not container_logs_id:
|
1274
|
+
raise ValueError("Container ID required for get-container-logs")
|
1275
|
+
print(manager.get_container_logs(container_logs_id, tail))
|
1276
|
+
|
1277
|
+
if exec_in_container:
|
1278
|
+
if not exec_container_id:
|
1279
|
+
raise ValueError("Container ID required for exec-in-container")
|
1280
|
+
cmd_list = exec_command.split() if exec_command else []
|
1281
|
+
print(
|
1282
|
+
json.dumps(
|
1283
|
+
manager.exec_in_container(exec_container_id, cmd_list, exec_detach),
|
1284
|
+
indent=2,
|
1285
|
+
)
|
1286
|
+
)
|
1287
|
+
|
1288
|
+
if list_volumes:
|
1289
|
+
print(json.dumps(manager.list_volumes(), indent=2))
|
1290
|
+
|
1291
|
+
if create_volume:
|
1292
|
+
if not create_volume_name:
|
1293
|
+
raise ValueError("Name required for create-volume")
|
1294
|
+
print(json.dumps(manager.create_volume(create_volume_name), indent=2))
|
1295
|
+
|
1296
|
+
if remove_volume:
|
1297
|
+
if not remove_volume_name:
|
1298
|
+
raise ValueError("Name required for remove-volume")
|
1299
|
+
print(json.dumps(manager.remove_volume(remove_volume_name, force), indent=2))
|
1300
|
+
|
1301
|
+
if list_networks:
|
1302
|
+
print(json.dumps(manager.list_networks(), indent=2))
|
1303
|
+
|
1304
|
+
if create_network:
|
1305
|
+
if not create_network_name:
|
1306
|
+
raise ValueError("Name required for create-network")
|
1307
|
+
print(json.dumps(manager.create_network(create_network_name, driver), indent=2))
|
1308
|
+
|
1309
|
+
if remove_network:
|
1310
|
+
if not remove_network_id:
|
1311
|
+
raise ValueError("ID required for remove-network")
|
1312
|
+
print(json.dumps(manager.remove_network(remove_network_id), indent=2))
|
1313
|
+
|
1314
|
+
if compose_up:
|
1315
|
+
if not compose_up_file:
|
1316
|
+
raise ValueError("File required for compose-up")
|
1317
|
+
print(manager.compose_up(compose_up_file, compose_detach, compose_build))
|
1318
|
+
|
1319
|
+
if compose_down:
|
1320
|
+
if not compose_down_file:
|
1321
|
+
raise ValueError("File required for compose-down")
|
1322
|
+
print(manager.compose_down(compose_down_file))
|
1323
|
+
|
1324
|
+
if compose_ps:
|
1325
|
+
if not compose_ps_file:
|
1326
|
+
raise ValueError("File required for compose-ps")
|
1327
|
+
print(manager.compose_ps(compose_ps_file))
|
1328
|
+
|
1329
|
+
if compose_logs:
|
1330
|
+
if not compose_logs_file:
|
1331
|
+
raise ValueError("File required for compose-logs")
|
1332
|
+
print(manager.compose_logs(compose_logs_file, compose_service))
|
1333
|
+
|
1334
|
+
if init_swarm:
|
1335
|
+
print(json.dumps(manager.init_swarm(advertise_addr), indent=2))
|
1336
|
+
|
1337
|
+
if leave_swarm:
|
1338
|
+
print(json.dumps(manager.leave_swarm(force), indent=2))
|
1339
|
+
|
1340
|
+
if list_nodes:
|
1341
|
+
print(json.dumps(manager.list_nodes(), indent=2))
|
1342
|
+
|
1343
|
+
if list_services:
|
1344
|
+
print(json.dumps(manager.list_services(), indent=2))
|
1345
|
+
|
1346
|
+
if create_service:
|
1347
|
+
if not create_service_name:
|
1348
|
+
raise ValueError("Name required for create-service")
|
1349
|
+
if not service_image:
|
1350
|
+
raise ValueError("Image required for create-service")
|
1351
|
+
ports = None
|
1352
|
+
if ports_str:
|
1353
|
+
ports = {}
|
1354
|
+
for p in ports_str.split(","):
|
1355
|
+
host, cont = p.split(":")
|
1356
|
+
ports[cont + "/tcp"] = host
|
1357
|
+
mounts = None
|
1358
|
+
if mounts_str:
|
1359
|
+
mounts = mounts_str.split(",")
|
1360
|
+
print(
|
1361
|
+
json.dumps(
|
1362
|
+
manager.create_service(
|
1363
|
+
create_service_name, service_image, replicas, ports, mounts
|
1364
|
+
),
|
1365
|
+
indent=2,
|
1366
|
+
)
|
1367
|
+
)
|
1368
|
+
|
1369
|
+
if remove_service:
|
1370
|
+
if not remove_service_id:
|
1371
|
+
raise ValueError("ID required for remove-service")
|
1372
|
+
print(json.dumps(manager.remove_service(remove_service_id), indent=2))
|
1373
|
+
|
1374
|
+
print("Done!")
|
1375
|
+
|
1376
|
+
|
1377
|
+
if __name__ == "__main__":
|
1378
|
+
if len(sys.argv) < 2:
|
1379
|
+
usage()
|
1380
|
+
sys.exit(2)
|
1381
|
+
main(sys.argv[1:])
|