idmtools-platform-container 0.0.0.dev0__py3-none-any.whl → 0.0.3__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. docker_image/BASE_VERSION +1 -0
  2. docker_image/Dockerfile +48 -0
  3. docker_image/Dockerfile_buildenv +46 -0
  4. docker_image/ImageName +1 -0
  5. docker_image/README.md +78 -0
  6. docker_image/__init__.py +6 -0
  7. docker_image/build_docker_image.py +145 -0
  8. docker_image/debian/BASE_VERSION +1 -0
  9. docker_image/debian/Dockerfile +40 -0
  10. docker_image/debian/ImageName +1 -0
  11. docker_image/debian/README.md +48 -0
  12. docker_image/debian/pip.conf +3 -0
  13. docker_image/debian/requirements.txt +1 -0
  14. docker_image/docker_image_history.py +101 -0
  15. docker_image/pip.conf +3 -0
  16. docker_image/push_docker_image.py +62 -0
  17. docker_image/requirements.txt +1 -0
  18. docker_image/rocky_meta_runtime.txt +37 -0
  19. idmtools_platform_container/__init__.py +18 -8
  20. idmtools_platform_container/cli/__init__.py +5 -0
  21. idmtools_platform_container/cli/container.py +682 -0
  22. idmtools_platform_container/container_operations/__init__.py +5 -0
  23. idmtools_platform_container/container_operations/docker_operations.py +593 -0
  24. idmtools_platform_container/container_platform.py +375 -0
  25. idmtools_platform_container/platform_operations/__init__.py +5 -0
  26. idmtools_platform_container/platform_operations/experiment_operations.py +112 -0
  27. idmtools_platform_container/platform_operations/simulation_operations.py +58 -0
  28. idmtools_platform_container/plugin_info.py +79 -0
  29. idmtools_platform_container/utils/__init__.py +5 -0
  30. idmtools_platform_container/utils/general.py +136 -0
  31. idmtools_platform_container/utils/status.py +130 -0
  32. idmtools_platform_container-0.0.3.dist-info/METADATA +212 -0
  33. idmtools_platform_container-0.0.3.dist-info/RECORD +69 -0
  34. idmtools_platform_container-0.0.3.dist-info/entry_points.txt +5 -0
  35. idmtools_platform_container-0.0.3.dist-info/licenses/LICENSE.TXT +3 -0
  36. {idmtools_platform_container-0.0.0.dev0.dist-info → idmtools_platform_container-0.0.3.dist-info}/top_level.txt +2 -0
  37. tests/inputs/Assets/MyLib/functions.py +2 -0
  38. tests/inputs/__init__.py +0 -0
  39. tests/inputs/model.py +28 -0
  40. tests/inputs/model1.py +31 -0
  41. tests/inputs/model3.py +21 -0
  42. tests/inputs/model_file.py +18 -0
  43. tests/inputs/run.sh +1 -0
  44. tests/inputs/sleep.py +9 -0
  45. tests/test_container_cli/__init__.py +0 -0
  46. tests/test_container_cli/helper.py +57 -0
  47. tests/test_container_cli/test_base.py +14 -0
  48. tests/test_container_cli/test_cancel.py +96 -0
  49. tests/test_container_cli/test_clear_results.py +54 -0
  50. tests/test_container_cli/test_container.py +72 -0
  51. tests/test_container_cli/test_file_container_cli.py +121 -0
  52. tests/test_container_cli/test_get_detail.py +60 -0
  53. tests/test_container_cli/test_history.py +136 -0
  54. tests/test_container_cli/test_history_count.py +53 -0
  55. tests/test_container_cli/test_inspect.py +53 -0
  56. tests/test_container_cli/test_install.py +48 -0
  57. tests/test_container_cli/test_is_running.py +69 -0
  58. tests/test_container_cli/test_jobs.py +138 -0
  59. tests/test_container_cli/test_list_containers.py +99 -0
  60. tests/test_container_cli/test_packages.py +41 -0
  61. tests/test_container_cli/test_path.py +96 -0
  62. tests/test_container_cli/test_ps.py +47 -0
  63. tests/test_container_cli/test_remove_container.py +78 -0
  64. tests/test_container_cli/test_status.py +149 -0
  65. tests/test_container_cli/test_stop_container.py +71 -0
  66. tests/test_container_cli/test_sync_history.py +98 -0
  67. tests/test_container_cli/test_verify_docker.py +28 -0
  68. tests/test_container_cli/test_volume.py +28 -0
  69. idmtools_platform_container-0.0.0.dev0.dist-info/METADATA +0 -41
  70. idmtools_platform_container-0.0.0.dev0.dist-info/RECORD +0 -5
  71. {idmtools_platform_container-0.0.0.dev0.dist-info → idmtools_platform_container-0.0.3.dist-info}/WHEEL +0 -0
@@ -0,0 +1,682 @@
1
+ """
2
+ idmtools ContainerPlatform CLI commands.
3
+
4
+ Copyright 2021, Bill & Melinda Gates Foundation. All rights reserved.
5
+ """
6
+ import json
7
+ import click
8
+ import shutil
9
+ import subprocess
10
+ from typing import Union, List, Tuple
11
+ from pathlib import Path
12
+ from rich.console import Console
13
+ from rich.table import Table
14
+ from idmtools.core import ItemType
15
+ from idmtools_platform_container.container_operations.docker_operations import list_running_jobs, find_running_job, \
16
+ is_docker_installed, is_docker_daemon_running, get_working_containers, get_containers, get_container
17
+ from idmtools_platform_container.utils.status import summarize_status_files, get_simulation_status
18
+ from idmtools_platform_container.utils.general import convert_byte_size, format_timestamp
19
+ from idmtools_platform_file.tools.job_history import JobHistory
20
+ from logging import getLogger
21
+
22
+ logger = getLogger(__name__)
23
+ user_logger = getLogger('user')
24
+
25
+ EXPERIMENT_FILES = ['stdout.txt', 'stderr.txt']
26
+ SIMULATION_FILES = ['stdout.txt', 'stderr.txt', 'job_status.txt', 'status.txt', 'output']
27
+
28
+ ##########################
29
+ # Container Commands
30
+ #########################
31
+
32
+ IMPORTANT_COMMANDS = ['status', 'cancel', 'jobs', 'history']
33
+
34
+
35
+ class CustomGroup(click.Group):
36
+ """Custom Group class for Container Platform CLI commands."""
37
+
38
+ def __init__(self, *args, **kwargs):
39
+ """
40
+ Initialize CustomGroup.
41
+ Args:
42
+ args: Positional arguments
43
+ kwargs: USer defined arguments
44
+ """
45
+ self.allowed_commands = kwargs.pop('allowed_commands', None)
46
+ super().__init__(*args, **kwargs)
47
+
48
+ def parse_args(self, ctx, args):
49
+ """
50
+ Parse arguments.
51
+ Args:
52
+ ctx: click context
53
+ args: user arguments
54
+ Returns:
55
+ None
56
+ """
57
+ # Intercept and process --all flag early
58
+ if '--all' in args:
59
+ ctx.params['all'] = True
60
+ self.allowed_commands = None
61
+ super().parse_args(ctx, args)
62
+
63
+ def list_commands(self, ctx) -> List[str]:
64
+ """
65
+ List commands.
66
+ Args:
67
+ ctx: click context
68
+ Returns:
69
+ list of commands
70
+ """
71
+ commands = super().list_commands(ctx)
72
+ if not ctx.params.get('all') and self.allowed_commands:
73
+ commands = [cmd for cmd in commands if cmd in self.allowed_commands]
74
+ return commands
75
+
76
+
77
+ @click.group(cls=CustomGroup, allowed_commands=IMPORTANT_COMMANDS, short_help="Container platform related commands.")
78
+ @click.option('--all', is_flag=True, help="Show all commands")
79
+ def container(all):
80
+ """
81
+ Container Platform CLI commands.
82
+
83
+ Args:
84
+ all: Bool, show all commands
85
+
86
+ Returns:
87
+ None
88
+ """
89
+ pass
90
+
91
+
92
+ @container.command(help="Verify the Docker environment.")
93
+ def verify_docker():
94
+ """Check docker environment."""
95
+ if not is_docker_installed():
96
+ user_logger.error("Docker is not installed.")
97
+ exit(-1)
98
+
99
+ if not is_docker_daemon_running():
100
+ user_logger.warning("Docker daemon is not running.")
101
+ exit(-1)
102
+
103
+ # Check docker version
104
+ result = subprocess.run(['docker', '--version'], stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
105
+ console = Console()
106
+ console.print(f"{result.stdout.strip()}.")
107
+
108
+
109
+ @container.command(help="Cancel an Experiment/Simulation job.\n\n"
110
+ "Arguments:\n\n"
111
+ " ITEM_ID: Experiment/Simulation ID or Job ID")
112
+ @click.argument('item-id', required=True)
113
+ @click.option('-c', '--container_id', help="Container Id")
114
+ def cancel(item_id: Union[int, str], container_id: str = None):
115
+ """
116
+ Cancel Experiment/Simulation job.
117
+ Args:
118
+ item_id: Experiment/Simulation ID or Job ID
119
+ container_id: Container ID
120
+ Returns:
121
+ None
122
+ """
123
+ console = Console()
124
+ job = find_running_job(item_id, container_id)
125
+ if job:
126
+ if job.item_type == ItemType.EXPERIMENT:
127
+ kill_cmd = f"docker exec {job.container_id} pkill -TERM -g {job.job_id}"
128
+ else:
129
+ kill_cmd = f"docker exec {job.container_id} kill -9 {job.job_id}"
130
+
131
+ result = subprocess.run(kill_cmd, shell=True, stderr=subprocess.PIPE, text=True) # default: check=False
132
+ if result.returncode == 0:
133
+ console.print(f"Successfully killed {job.item_type.name} {job.job_id}")
134
+ else:
135
+ console.print(f"Error killing {job.item_type.name} {job.item_id}: {result.stderr}")
136
+ else:
137
+ user_logger.warning(f"Not found job {item_id}.")
138
+
139
+
140
+ @container.command(help="Check the status of an Experiment/Simulation.\n\n"
141
+ "Arguments:\n\n"
142
+ " ITEM_ID: Experiment/Simulation ID or Job ID")
143
+ @click.argument('item-id', required=True)
144
+ @click.option('-c', '--container_id', help="Container Id")
145
+ @click.option('-l', '--limit', default=10, help="Max number of simulations to show")
146
+ @click.option('--verbose/--no-verbose', default=False, help="Display with working directory or not")
147
+ def status(item_id: Union[int, str], container_id: str = None, limit: int = 10, verbose: bool = False):
148
+ """
149
+ Check Experiment/Simulation status.
150
+ Args:
151
+ item_id: Experiment/Simulation ID or Job ID
152
+ container_id: Container ID
153
+ limit: number of simulations to display
154
+ verbose: display simulation details or not
155
+ Returns:
156
+ None
157
+ """
158
+ console = Console()
159
+ item_dir = JobHistory.get_item_path(item_id)
160
+ if item_dir is not None:
161
+ # Experiment/Simulation case
162
+ item_type = item_dir[1]
163
+ if item_type == ItemType.SIMULATION:
164
+ st = get_simulation_status(item_dir[0])
165
+ console.print(f"{item_type.name} {item_id} is {st}.")
166
+ elif item_type == ItemType.EXPERIMENT:
167
+ exp_dir = item_dir[0]
168
+ summarize_status_files(exp_dir, max_display=limit, verbose=verbose)
169
+ else:
170
+ user_logger.warning(f"{item_type.name} {item_id} status id not defined.")
171
+ else:
172
+ # Job ID case
173
+ job = find_running_job(item_id, container_id)
174
+ if job:
175
+ if job.item_type == ItemType.EXPERIMENT:
176
+ job_cache = JobHistory.get_job(job.item_id)
177
+ exp_dir = job_cache['EXPERIMENT_DIR']
178
+ summarize_status_files(exp_dir, max_display=limit, verbose=verbose)
179
+ elif job.item_type == ItemType.SIMULATION:
180
+ console.print(f"Simulation {job.item_id} is RUNNING.")
181
+ else:
182
+ user_logger.warning(f"Job {item_id} not found.")
183
+
184
+
185
+ @container.command(help="List running Experiment/Simulation jobs.\n\n"
186
+ "Arguments:\n\n"
187
+ " CONTAINER_ID: Container ID (optional)")
188
+ @click.argument('container-id', required=False)
189
+ @click.option('-l', '--limit', default=10, help="Max number of simulations to show")
190
+ @click.option('-n', '--next', default=0, type=int, help="Next number of jobs to show")
191
+ def jobs(container_id: str = None, limit: int = 10, next: int = 0):
192
+ """
193
+ List running Experiment/Simulation jobs in Container(s).
194
+ Args:
195
+ container_id: Container ID
196
+ limit: number of simulations to display
197
+ next: next number of jobs to show
198
+ Returns:
199
+ None
200
+ """
201
+ containers = get_working_containers(container_id)
202
+ if len(containers) == 0:
203
+ if container_id:
204
+ user_logger.warning(f"Container {container_id} not found.")
205
+ else:
206
+ user_logger.warning("No containers found.")
207
+ return
208
+
209
+ for container_id in containers:
210
+ running_jobs = list_running_jobs(container_id)
211
+ if not running_jobs:
212
+ continue
213
+
214
+ # Separate jobs by group_pid
215
+ group = {}
216
+ for job in running_jobs:
217
+ if job.group_pid not in group:
218
+ group[job.group_pid] = []
219
+ group[job.group_pid].append(job)
220
+
221
+ console = Console()
222
+ for g in group:
223
+ _jobs = group[g]
224
+ # Get total number of running simulations
225
+ total_jobs = len(_jobs)
226
+ # Take the first job which is the experiment
227
+ exp_job = _jobs[0]
228
+ # Skip the first job which is the experiment
229
+ sim_jobs = _jobs[1:]
230
+
231
+ start = next * limit
232
+ end = start + limit
233
+ sim_next = sim_jobs[start:end]
234
+ # Include the experiment job
235
+ sim_next.insert(0, exp_job)
236
+
237
+ # Skip the first job which is the experiment
238
+ console.print(
239
+ f"[bold][cyan]Experiment[/][/] {exp_job.item_id} on [bold][cyan]Container[/][/] [red]{container_id}[/] has {total_jobs - 1} running [bold][cyan]simulations[/][/].")
240
+ table = Table()
241
+ table.add_column("Entity Type", justify="right", style="cyan", no_wrap=True)
242
+ table.add_column("Entity ID", style="yellow")
243
+ table.add_column("Job ID", justify="right", style="green")
244
+ table.add_column("Container", justify="right", style="plum2")
245
+ table.add_column("Status", justify="right", style="red")
246
+ table.add_column("Elapsed", justify="right", style="blue")
247
+
248
+ for job in sim_next:
249
+ table.add_row(job.item_type.name, str(job.item_id), str(job.job_id), job.container_id, 'running',
250
+ job.elapsed)
251
+
252
+ console.print(table)
253
+
254
+
255
+ @container.command(help="Retrieve Experiment history.\n\n"
256
+ "Arguments:\n\n"
257
+ " EXP_ID: Experiment ID")
258
+ @click.argument('exp-id', type=str, required=True)
259
+ def get_detail(exp_id: str):
260
+ """
261
+ Get Experiment job history.
262
+ Args:
263
+ exp_id: Experiment ID
264
+ Returns:
265
+ None
266
+ """
267
+ item = JobHistory.get_job(exp_id)
268
+ if item:
269
+ console = Console()
270
+ console.print_json(json.dumps(item, indent=2))
271
+
272
+
273
+ @container.command(help="View the job history.\n\n"
274
+ "Arguments:\n\n"
275
+ " CONTAINER_ID: Container ID")
276
+ @click.argument('container-id', required=False)
277
+ @click.option('-l', '--limit', default=10, type=int, help="Max number of jobs to show")
278
+ @click.option('-n', '--next', default=0, type=int, help="Next number of jobs to show")
279
+ def history(container_id: str = None, limit: int = 10, next: int = 0):
280
+ """
281
+ View job history.
282
+ Args:
283
+ container_id: Container ID
284
+ limit: number of jobs to show
285
+ next: next number of jobs to show
286
+ Returns:
287
+ None
288
+ """
289
+ data = JobHistory.view_history(container_id)
290
+
291
+ start = next * limit
292
+ end = start + limit
293
+ data_next = data[start:end]
294
+
295
+ console = Console()
296
+ console.print(f"There are {len(data)} Experiment cache in history.")
297
+ for job in data_next:
298
+ console.print(f"{'':-^100}")
299
+ for k, v in job.items():
300
+ # Skip some keys
301
+ if k in ('EXPERIMENT_DIR', 'SUITE_DIR'):
302
+ continue
303
+ console.print(f"[bold][cyan]{k:16}[/][/]: {v}")
304
+
305
+
306
+ @container.command(help="Locate Suite/Experiment/Simulation file directory.\n\n"
307
+ "Arguments:\n\n"
308
+ " ITEM_ID: Suite/Experiment/Simulation ID")
309
+ @click.argument('item-id', type=str, required=True)
310
+ def path(item_id: str):
311
+ """
312
+ Find Suite/Experiment/Simulation file directory.
313
+ Args:
314
+ item_id: Suite/Experiment/Simulation ID
315
+ Returns:
316
+ None
317
+ """
318
+ item = JobHistory.get_item_path(item_id)
319
+ if item:
320
+ console = Console()
321
+ console.print(f"{item[1].name}: {item[0]}")
322
+
323
+
324
+ @container.command(help="Check if an Experiment/Simulation is running.\n\n"
325
+ "Arguments:\n\n"
326
+ " ITEM_ID: Experiment/Simulation ID")
327
+ @click.argument('item-id', type=str, required=True)
328
+ def is_running(item_id: str):
329
+ """
330
+ Check if Experiment/Simulation is running.
331
+ Args:
332
+ item_id: Experiment/Simulation ID
333
+ Returns:
334
+ None
335
+ """
336
+ console = Console()
337
+ job = find_running_job(item_id)
338
+ if job:
339
+ console.print(f"{job.item_type.name} {job.item_id} is running on container {job.container_id}.")
340
+ else:
341
+ # Check if it is a valid Experiment/Simulation ID
342
+ his_path = JobHistory.get_item_path(item_id)
343
+ if his_path:
344
+ # Check item type
345
+ item_type = his_path[1]
346
+ if item_type == ItemType.SUITE:
347
+ console.print(f"{item_id} is not a valid Experiment/Simulation ID.")
348
+ else:
349
+ console.print(f"{item_type.name} {item_id} is not running.")
350
+ else:
351
+ console.print(f"Job {item_id} is not found.")
352
+
353
+
354
+ @container.command(help="Check the history volume.")
355
+ def volume():
356
+ """Get job history volume."""
357
+ v = JobHistory.volume()
358
+ mv = convert_byte_size(v)
359
+ console = Console()
360
+ console.print(f"Job history volume: {mv}")
361
+
362
+
363
+ @container.command(help="Clear the job history.\n\n"
364
+ "Arguments:\n\n"
365
+ " CONTAINER_ID: Container ID (optional)")
366
+ @click.argument('container-id', required=False)
367
+ def clear_history(container_id: str = None):
368
+ """
369
+ Clear Job History.
370
+ Args:
371
+ container_id: Container ID
372
+ Returns:
373
+ None
374
+ """
375
+ JobHistory.clear(container_id)
376
+
377
+
378
+ @container.command(help="Sync the file system with job history.")
379
+ def sync_history():
380
+ """Sync file system with job history."""
381
+ JobHistory.sync()
382
+
383
+
384
+ @container.command(help="Get the count of count histories.\n\n"
385
+ "Arguments:\n\n"
386
+ " CONTAINER_ID: Container ID (optional)")
387
+ @click.argument('container-id', required=False)
388
+ def history_count(container_id: str = None):
389
+ """
390
+ Get History Count.
391
+ Args:
392
+ container_id: Container ID
393
+ Returns:
394
+ None
395
+ """
396
+ console = Console()
397
+ console.print(JobHistory.count(container_id))
398
+
399
+
400
+ @container.command(help="Clear job results files and folders.\n\n"
401
+ "Arguments:\n\n"
402
+ " ITEM_ID: Experiment/Simulation ID")
403
+ @click.argument('item-id', type=str, required=True)
404
+ @click.option('-r', '--remove', multiple=True, help="Extra files/folders to be removed from simulation")
405
+ def clear_results(item_id: str, remove: Tuple = None):
406
+ """
407
+ Clear the generated output files for a job.
408
+ Args:
409
+ item_id: Experiment/Simulation ID
410
+ remove: list of files/folders
411
+ Returns:
412
+ None
413
+ """
414
+
415
+ def _clear_simulation(sim_dir, remove_list):
416
+ """
417
+ Delete generated output files for simulation.
418
+ Args:
419
+ sim_dir: simulation directory
420
+ remove_list: extra files to be deleted
421
+ Returns:
422
+ None
423
+ """
424
+ for f in SIMULATION_FILES + list(remove_list):
425
+ if sim_dir.joinpath(f).exists():
426
+ if sim_dir.joinpath(f).is_dir():
427
+ shutil.rmtree(sim_dir.joinpath(f))
428
+ else:
429
+ sim_dir.joinpath(f).unlink(missing_ok=True)
430
+
431
+ # Get item path
432
+ item = JobHistory.get_item_path(item_id)
433
+ # Check item type
434
+ item_type = item[1]
435
+ if item_type == ItemType.SIMULATION:
436
+ sim_dir = item[0]
437
+ _clear_simulation(sim_dir, remove)
438
+ elif item_type == ItemType.EXPERIMENT:
439
+ exp_dir = item[0]
440
+ # Delete generated files from experiment past run
441
+ for f in EXPERIMENT_FILES:
442
+ if exp_dir.joinpath(f).exists():
443
+ if exp_dir.joinpath(f).is_dir():
444
+ shutil.rmtree(exp_dir.joinpath(f))
445
+ else:
446
+ exp_dir.joinpath(f).unlink(missing_ok=True)
447
+
448
+ # Delete generated files for each of simulations
449
+ pattern = '*/metadata.json'
450
+ for meta_file in Path(exp_dir).glob(pattern=pattern):
451
+ sim_dir = meta_file.parent
452
+ _clear_simulation(sim_dir, remove)
453
+ else:
454
+ user_logger.warning("Suite level not supported, must provide Experiment/Simulation ID!")
455
+ exit(-1)
456
+
457
+
458
+ @container.command(help="Inspect a container.\n\n"
459
+ "Arguments:\n\n"
460
+ " CONTAINER_ID: Container ID")
461
+ @click.argument('container-id', required=True)
462
+ def inspect(container_id: str):
463
+ """
464
+ Check container information.
465
+ Args:
466
+ container_id: Container ID
467
+ Returns:
468
+ None
469
+ """
470
+ console = Console()
471
+ container = get_container(container_id)
472
+ if container is None:
473
+ console.print(f"Container {container_id} not found.")
474
+ return
475
+
476
+ console.print('-' * 100)
477
+ console.print(f"[bold][cyan]Container ID[/][/]: {container.short_id}")
478
+ console.print(f"[bold][cyan]Container Name[/][/]: {container.name}")
479
+ console.print(f"[bold][cyan]Status[/][/]: {container.status}")
480
+ console.print(f"[bold][cyan]Created[/][/]: {format_timestamp(container.attrs['Created'])}")
481
+ console.print(f"[bold][cyan]StartedAt[/][/]: {format_timestamp(container.attrs['State']['StartedAt'])}")
482
+
483
+ console.print("[bold][cyan]Image[/][/]:")
484
+ console.print_json(json.dumps(container.attrs['Config']['Image']))
485
+
486
+ console.print("[bold][cyan]Image Tags[/][/]:")
487
+ console.print_json(json.dumps(container.image.tags))
488
+
489
+ console.print("[bold][cyan]State[/][/]:")
490
+ console.print_json(json.dumps(container.attrs['State']))
491
+
492
+ console.print("[bold][cyan]Mounts[/][/]:")
493
+ mounts = [m for m in container.attrs['Mounts'] if m['Type'] == 'bind']
494
+ console.print_json(json.dumps(mounts))
495
+
496
+
497
+ @container.command(help="Stop running container(s).\n\n"
498
+ "Arguments:\n\n"
499
+ " CONTAINER_ID: Container ID (optional)")
500
+ @click.argument('container-id', required=False)
501
+ @click.option('--remove/--no-remove', default=False, help="Remove the container or not")
502
+ def stop_container(container_id: str = None, remove: bool = False):
503
+ """
504
+ Sopp running container(s).
505
+ Args:
506
+ container_id: container id
507
+ remove: remove container or not
508
+ Returns:
509
+ None
510
+ """
511
+ console = Console()
512
+ # Get working containers
513
+ containers = get_working_containers(container_id, entity=True)
514
+ if len(containers) == 0:
515
+ if container_id:
516
+ user_logger.warning(f"Not found running Container {container_id}.")
517
+ else:
518
+ user_logger.warning("No running containers found.")
519
+ return
520
+
521
+ for container in containers:
522
+ container.stop()
523
+ if remove:
524
+ container.remove()
525
+ console.print(f"Container {container.short_id} is stopped and removed.")
526
+ else:
527
+ console.print(f"Container {container.short_id} is stopped.")
528
+
529
+
530
+ @container.command(help="Remove stopped containers.\n\n"
531
+ "Arguments:\n\n"
532
+ " CONTAINER_ID: Container ID (optional)")
533
+ @click.argument('container-id', required=False)
534
+ def remove_container(container_id: str = None):
535
+ """
536
+ Remove stopped containers.
537
+ Args:
538
+ container_id: container id
539
+ Returns:
540
+ None
541
+ """
542
+ console = Console()
543
+ if container_id:
544
+ container = get_container(container_id)
545
+ if container:
546
+ if container.status != 'running':
547
+ container.remove()
548
+ console.print(f"Container {container_id} is removed.")
549
+ else:
550
+ user_logger.warning(f"Container {container_id} is running, need to stop first.")
551
+ else:
552
+ user_logger.warning(f"Container {container_id} not found.")
553
+ return
554
+
555
+ # Remove all stopped containers
556
+ container_list = get_containers(include_stopped=True)['stopped']
557
+ container_removed = []
558
+ for container in container_list:
559
+ container.remove()
560
+ container_removed.append(container.short_id)
561
+
562
+ if len(container_removed) > 0:
563
+ console.print(f"{len(container_removed)} container(s) removed.")
564
+ else:
565
+ user_logger.warning("No container removed.")
566
+
567
+
568
+ @container.command(help="pip install a package on a container.\n\n"
569
+ "Arguments:\n\n"
570
+ " PACKAGE: package to be installed")
571
+ @click.argument('package', required=True)
572
+ @click.option('-c', '--container-id', type=str, help="Container ID")
573
+ @click.option('-i', '--index-url', type=str, help="index-url for pip install")
574
+ @click.option('-e', '--extra-index-url', type=str, help="extra-index-url for pip install")
575
+ def install(package: str, container_id: str, index_url: str = None, extra_index_url: str = None):
576
+ """
577
+ Pip install package on container.
578
+ Args:
579
+ package: package name
580
+ container_id: Container ID
581
+ index_url: index-url for pip install
582
+ extra_index_url: extra-index-url for pip install
583
+ Returns:
584
+ None
585
+ """
586
+ console = Console()
587
+ if index_url:
588
+ package = f"--index-url {index_url} {package}"
589
+ elif extra_index_url:
590
+ package = f"--extra-index-url {extra_index_url} {package}"
591
+ else:
592
+ package = f"{package}"
593
+
594
+ command = f'docker exec {container_id} bash -c "pip3 install {package}"'
595
+ try:
596
+ result = subprocess.run(command, shell=True, check=True, capture_output=True, text=True)
597
+ console.print(result.stdout)
598
+ except subprocess.CalledProcessError as e:
599
+ user_logger.error(e.stderr)
600
+
601
+
602
+ @container.command(help="List packages installed on a container.\n\n"
603
+ "Arguments:\n\n"
604
+ " CONTAINER_ID: Container ID")
605
+ @click.argument('container-id', required=True)
606
+ def packages(container_id: str):
607
+ """
608
+ List packages installed on container.
609
+ Args:
610
+ container_id: Container ID
611
+ Returns:
612
+ None
613
+ """
614
+ console = Console()
615
+ if not JobHistory.verify_container(container_id):
616
+ user_logger.error(f"Container {container_id} not found.")
617
+ return
618
+ command = f'docker exec {container_id} bash -c "pip list"'
619
+ try:
620
+ result = subprocess.run(command, shell=True, check=True, capture_output=True, text=True)
621
+ console.print(result.stdout)
622
+ except subprocess.CalledProcessError as e:
623
+ user_logger.error(e.stderr)
624
+
625
+
626
+ @container.command(help="List running processes in a container.\n\n"
627
+ "Arguments:\n\n"
628
+ " CONTAINER_ID: Container ID")
629
+ @click.argument('container-id', required=True)
630
+ def ps(container_id: str):
631
+ """
632
+ List running processes in container.
633
+ Args:
634
+ container_id: Container ID
635
+ Returns:
636
+ None
637
+ """
638
+ if not JobHistory.verify_container(container_id):
639
+ user_logger.error(f"Container {container_id} not found.")
640
+ return
641
+ command = f'docker exec {container_id} bash -c "ps -efj"'
642
+ try:
643
+ result = subprocess.run(command, shell=True, check=True, capture_output=True, text=True)
644
+ console = Console()
645
+ console.print(result.stdout)
646
+ except subprocess.CalledProcessError as e:
647
+ user_logger.error(e.stderr)
648
+
649
+
650
+ @container.command(help="List all available containers.")
651
+ @click.option('--all/--no-all', default=False, help="Include stopped containers or not")
652
+ def list_containers(all: bool = False):
653
+ """
654
+ List available containers.
655
+ Args:
656
+ all: bool, include stopped containers or not
657
+ Returns:
658
+ None
659
+ """
660
+ containers = get_containers(include_stopped=all)
661
+
662
+ table = Table()
663
+ table.add_column("Container ID", justify="right", style="cyan", no_wrap=True)
664
+ table.add_column("Image", style="bright_magenta")
665
+ table.add_column("Status", style="red")
666
+ table.add_column("Created", style="yellow")
667
+ table.add_column("Started", style="orange1")
668
+ table.add_column("Name", style="wheat4")
669
+
670
+ for status, container_list in containers.items():
671
+ for container in container_list:
672
+ if container.status == 'running':
673
+ status = f"[green]{container.status}[/]"
674
+ else:
675
+ status = f"[red]{container.status}[/]"
676
+ table.add_row(container.short_id, container.attrs['Config']['Image'], status,
677
+ format_timestamp(container.attrs['Created']),
678
+ format_timestamp(container.attrs['State']['StartedAt']), container.name)
679
+
680
+ console = Console()
681
+ console.print(f"There are {table.row_count} container(s).")
682
+ console.print(table)
@@ -0,0 +1,5 @@
1
+ """
2
+ idmtools ProcessPlatform platform operations module.
3
+
4
+ Copyright 2021, Bill & Melinda Gates Foundation. All rights reserved.
5
+ """