idmtools-platform-container 0.0.0.dev0__py3-none-any.whl → 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.
- docker_image/BASE_VERSION +1 -0
- docker_image/Dockerfile +48 -0
- docker_image/Dockerfile_buildenv +46 -0
- docker_image/ImageName +1 -0
- docker_image/README.md +78 -0
- docker_image/__init__.py +6 -0
- docker_image/build_docker_image.py +145 -0
- docker_image/debian/BASE_VERSION +1 -0
- docker_image/debian/Dockerfile +40 -0
- docker_image/debian/ImageName +1 -0
- docker_image/debian/README.md +48 -0
- docker_image/debian/pip.conf +3 -0
- docker_image/debian/requirements.txt +1 -0
- docker_image/docker_image_history.py +101 -0
- docker_image/pip.conf +3 -0
- docker_image/push_docker_image.py +62 -0
- docker_image/requirements.txt +1 -0
- docker_image/rocky_meta_runtime.txt +37 -0
- idmtools_platform_container/__init__.py +18 -8
- idmtools_platform_container/cli/__init__.py +5 -0
- idmtools_platform_container/cli/container.py +682 -0
- idmtools_platform_container/container_operations/__init__.py +5 -0
- idmtools_platform_container/container_operations/docker_operations.py +593 -0
- idmtools_platform_container/container_platform.py +375 -0
- idmtools_platform_container/platform_operations/__init__.py +5 -0
- idmtools_platform_container/platform_operations/experiment_operations.py +112 -0
- idmtools_platform_container/platform_operations/simulation_operations.py +58 -0
- idmtools_platform_container/plugin_info.py +79 -0
- idmtools_platform_container/utils/__init__.py +5 -0
- idmtools_platform_container/utils/general.py +136 -0
- idmtools_platform_container/utils/status.py +130 -0
- idmtools_platform_container-0.0.2.dist-info/METADATA +212 -0
- idmtools_platform_container-0.0.2.dist-info/RECORD +69 -0
- idmtools_platform_container-0.0.2.dist-info/entry_points.txt +5 -0
- idmtools_platform_container-0.0.2.dist-info/licenses/LICENSE.TXT +3 -0
- {idmtools_platform_container-0.0.0.dev0.dist-info → idmtools_platform_container-0.0.2.dist-info}/top_level.txt +2 -0
- tests/inputs/Assets/MyLib/functions.py +2 -0
- tests/inputs/__init__.py +0 -0
- tests/inputs/model.py +28 -0
- tests/inputs/model1.py +31 -0
- tests/inputs/model3.py +21 -0
- tests/inputs/model_file.py +18 -0
- tests/inputs/run.sh +1 -0
- tests/inputs/sleep.py +9 -0
- tests/test_container_cli/__init__.py +0 -0
- tests/test_container_cli/helper.py +57 -0
- tests/test_container_cli/test_base.py +14 -0
- tests/test_container_cli/test_cancel.py +96 -0
- tests/test_container_cli/test_clear_results.py +54 -0
- tests/test_container_cli/test_container.py +72 -0
- tests/test_container_cli/test_file_container_cli.py +121 -0
- tests/test_container_cli/test_get_detail.py +60 -0
- tests/test_container_cli/test_history.py +136 -0
- tests/test_container_cli/test_history_count.py +53 -0
- tests/test_container_cli/test_inspect.py +53 -0
- tests/test_container_cli/test_install.py +48 -0
- tests/test_container_cli/test_is_running.py +69 -0
- tests/test_container_cli/test_jobs.py +138 -0
- tests/test_container_cli/test_list_containers.py +99 -0
- tests/test_container_cli/test_packages.py +41 -0
- tests/test_container_cli/test_path.py +96 -0
- tests/test_container_cli/test_ps.py +47 -0
- tests/test_container_cli/test_remove_container.py +78 -0
- tests/test_container_cli/test_status.py +149 -0
- tests/test_container_cli/test_stop_container.py +71 -0
- tests/test_container_cli/test_sync_history.py +98 -0
- tests/test_container_cli/test_verify_docker.py +28 -0
- tests/test_container_cli/test_volume.py +28 -0
- idmtools_platform_container-0.0.0.dev0.dist-info/METADATA +0 -41
- idmtools_platform_container-0.0.0.dev0.dist-info/RECORD +0 -5
- {idmtools_platform_container-0.0.0.dev0.dist-info → idmtools_platform_container-0.0.2.dist-info}/WHEEL +0 -0
|
@@ -0,0 +1,375 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Here we implement the ContainerPlatform object.
|
|
3
|
+
|
|
4
|
+
Copyright 2021, Bill & Melinda Gates Foundation. All rights reserved.
|
|
5
|
+
"""
|
|
6
|
+
import os
|
|
7
|
+
import docker
|
|
8
|
+
import platform
|
|
9
|
+
import subprocess
|
|
10
|
+
from uuid import uuid4
|
|
11
|
+
from docker.models.containers import Container
|
|
12
|
+
from typing import Union, NoReturn, List, Dict
|
|
13
|
+
from dataclasses import dataclass, field
|
|
14
|
+
from idmtools.core.interfaces.ientity import IEntity
|
|
15
|
+
from idmtools.entities import Suite
|
|
16
|
+
from idmtools.entities.experiment import Experiment
|
|
17
|
+
from idmtools.entities.simulation import Simulation
|
|
18
|
+
from idmtools_platform_container.container_operations.docker_operations import validate_container_running, \
|
|
19
|
+
find_container_by_image, compare_mounts, find_running_job, get_container, CONTAINER_STATUS, restart_container, \
|
|
20
|
+
is_docker_installed, is_docker_daemon_running
|
|
21
|
+
from idmtools_platform_container.platform_operations.simulation_operations import ContainerPlatformSimulationOperations
|
|
22
|
+
from idmtools_platform_container.utils.general import map_container_path
|
|
23
|
+
from idmtools_platform_file.tools.job_history import JobHistory
|
|
24
|
+
from idmtools_platform_file.file_platform import FilePlatform
|
|
25
|
+
from idmtools_platform_container.platform_operations.experiment_operations import ContainerPlatformExperimentOperations
|
|
26
|
+
from logging import getLogger, DEBUG
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
logger = getLogger(__name__)
|
|
30
|
+
user_logger = getLogger('user')
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass(repr=False)
|
|
34
|
+
class ContainerPlatform(FilePlatform):
|
|
35
|
+
"""
|
|
36
|
+
Container Platform definition.
|
|
37
|
+
"""
|
|
38
|
+
__CONTAINER_IMAGE = 'docker-production-public.packages.idmod.org/idmtools/container-rocky-runtime:0.0.4'
|
|
39
|
+
__CONTAINER_MOUNT = "/home/container_data"
|
|
40
|
+
docker_image: str = field(default=None, metadata=dict(help="Docker image to run the container"))
|
|
41
|
+
data_mount: str = field(default=None, metadata=dict(help="Data mount point in the container"))
|
|
42
|
+
user_mounts: dict = field(default=None, metadata=dict(help="User-defined mounts"))
|
|
43
|
+
container_prefix: str = field(default=None, metadata=dict(help="Container name prefix"))
|
|
44
|
+
force_start: bool = field(default=False, metadata=dict(help="Force start a new container"))
|
|
45
|
+
new_container: bool = field(default=False, metadata=dict(help="Start a new container"))
|
|
46
|
+
include_stopped: bool = field(default=False, metadata=dict(help="Include stopped containers"))
|
|
47
|
+
debug: bool = field(default=False, metadata=dict(help="Debug mode"))
|
|
48
|
+
container_id: str = field(default=None, metadata=dict(help="Container Id"))
|
|
49
|
+
|
|
50
|
+
def __post_init__(self):
|
|
51
|
+
super().__post_init__()
|
|
52
|
+
self._experiments = ContainerPlatformExperimentOperations(platform=self)
|
|
53
|
+
self._simulations = ContainerPlatformSimulationOperations(platform=self)
|
|
54
|
+
self.job_directory = os.path.abspath(self.job_directory)
|
|
55
|
+
self.run_sequence = False
|
|
56
|
+
if self.docker_image is None:
|
|
57
|
+
self.docker_image = self.__CONTAINER_IMAGE
|
|
58
|
+
if self.data_mount is None:
|
|
59
|
+
self.data_mount = self.__CONTAINER_MOUNT
|
|
60
|
+
|
|
61
|
+
if self.debug:
|
|
62
|
+
root_logger = getLogger()
|
|
63
|
+
root_logger.setLevel(DEBUG)
|
|
64
|
+
|
|
65
|
+
# Check if Docker is installed and running
|
|
66
|
+
if not is_docker_installed():
|
|
67
|
+
user_logger.error("Docker is not installed.")
|
|
68
|
+
exit(-1)
|
|
69
|
+
if not is_docker_daemon_running():
|
|
70
|
+
user_logger.error("Docker daemon is not running.")
|
|
71
|
+
exit(-1)
|
|
72
|
+
|
|
73
|
+
def validate_container(self, container_id: str) -> str:
|
|
74
|
+
"""
|
|
75
|
+
Validate the container.
|
|
76
|
+
Args:
|
|
77
|
+
container_id: container id
|
|
78
|
+
Returns:
|
|
79
|
+
Container short id
|
|
80
|
+
"""
|
|
81
|
+
# Check if the container exists
|
|
82
|
+
container = get_container(container_id)
|
|
83
|
+
if not container:
|
|
84
|
+
user_logger.warning(f"Container {container_id} is not found.")
|
|
85
|
+
exit(-1)
|
|
86
|
+
|
|
87
|
+
# Check if the container is in the right status
|
|
88
|
+
if container.status not in CONTAINER_STATUS:
|
|
89
|
+
user_logger.warning(
|
|
90
|
+
f"Container {container_id} is in {container.status} status, but we only support status: {CONTAINER_STATUS}.")
|
|
91
|
+
exit(-1)
|
|
92
|
+
|
|
93
|
+
# Check if the container is running if we do not include stopped containers
|
|
94
|
+
if not self.include_stopped and container.status != 'running':
|
|
95
|
+
user_logger.warning(f"Container {container_id} is not running.")
|
|
96
|
+
exit(-1)
|
|
97
|
+
|
|
98
|
+
# Check if the container matches the platform mounts
|
|
99
|
+
if not self.validate_mount(container):
|
|
100
|
+
user_logger.warning(f"Container {container_id} does not match the platform mounts.")
|
|
101
|
+
exit(-1)
|
|
102
|
+
|
|
103
|
+
# Restart the container if it is not running
|
|
104
|
+
if container.status != 'running':
|
|
105
|
+
restart_container(container)
|
|
106
|
+
|
|
107
|
+
return container.short_id
|
|
108
|
+
|
|
109
|
+
def run_items(self, items: Union[IEntity, List[IEntity]], **kwargs):
|
|
110
|
+
"""
|
|
111
|
+
Run items on the platform.
|
|
112
|
+
Args:
|
|
113
|
+
items: Runnable items
|
|
114
|
+
kwargs: additional arguments
|
|
115
|
+
Returns:
|
|
116
|
+
None
|
|
117
|
+
"""
|
|
118
|
+
if self.container_id is not None:
|
|
119
|
+
self.container_id = self.validate_container(self.container_id)
|
|
120
|
+
super().run_items(items, **kwargs)
|
|
121
|
+
|
|
122
|
+
def submit_job(self, item: Union[Experiment, Simulation], dry_run: bool = False, **kwargs) -> NoReturn:
|
|
123
|
+
"""
|
|
124
|
+
Submit a Process job in a docker container.
|
|
125
|
+
Args:
|
|
126
|
+
item: Experiment or Simulation
|
|
127
|
+
dry_run: True/False
|
|
128
|
+
kwargs: keyword arguments used to expand functionality
|
|
129
|
+
Returns:
|
|
130
|
+
Any
|
|
131
|
+
"""
|
|
132
|
+
if dry_run:
|
|
133
|
+
user_logger.info(f'\nDry run: {dry_run}')
|
|
134
|
+
return
|
|
135
|
+
|
|
136
|
+
if isinstance(item, Experiment):
|
|
137
|
+
if logger.isEnabledFor(DEBUG):
|
|
138
|
+
logger.debug("Run experiment on container!")
|
|
139
|
+
|
|
140
|
+
# Check if the experiment is already running
|
|
141
|
+
his_job = JobHistory.get_job(item.id)
|
|
142
|
+
if his_job:
|
|
143
|
+
job = find_running_job(item.id, his_job['CONTAINER'])
|
|
144
|
+
if job:
|
|
145
|
+
user_logger.warning(f"Experiment {item.id} is already running on Container {job.container_id}.")
|
|
146
|
+
exit(-1)
|
|
147
|
+
|
|
148
|
+
# Start the container
|
|
149
|
+
if self.container_id is None:
|
|
150
|
+
if logger.isEnabledFor(DEBUG):
|
|
151
|
+
logger.debug("Check provided container!")
|
|
152
|
+
self.container_id = self.check_container(**kwargs)
|
|
153
|
+
|
|
154
|
+
# If the platform is Windows, convert the scripts to Linux format
|
|
155
|
+
if platform.system() in ["Windows"]:
|
|
156
|
+
if logger.isEnabledFor(DEBUG):
|
|
157
|
+
logger.debug("Script runs on Windows!")
|
|
158
|
+
self.convert_scripts_to_linux(item, **kwargs)
|
|
159
|
+
|
|
160
|
+
# Submit the experiment/simulations
|
|
161
|
+
if logger.isEnabledFor(DEBUG):
|
|
162
|
+
logger.debug(f"Submit experiment/simulations to container: {self.container_id}")
|
|
163
|
+
self.submit_experiment(item, **kwargs)
|
|
164
|
+
|
|
165
|
+
# Save the job to history
|
|
166
|
+
JobHistory.save_job(self.job_directory, self.container_id, item, self)
|
|
167
|
+
elif isinstance(item, Simulation):
|
|
168
|
+
raise NotImplementedError("submit_job directly for simulation is not implemented on ContainerPlatform.")
|
|
169
|
+
else:
|
|
170
|
+
raise NotImplementedError(
|
|
171
|
+
f"Submit job is not implemented for {item.__class__.__name__} on ContainerPlatform.")
|
|
172
|
+
|
|
173
|
+
def check_container(self, **kwargs) -> str:
|
|
174
|
+
"""
|
|
175
|
+
Check the container status.
|
|
176
|
+
Args:
|
|
177
|
+
kwargs: keyword arguments used to expand functionality
|
|
178
|
+
Returns:
|
|
179
|
+
container id
|
|
180
|
+
"""
|
|
181
|
+
container_id = validate_container_running(self, **kwargs)
|
|
182
|
+
return container_id
|
|
183
|
+
|
|
184
|
+
def start_container(self, **kwargs) -> str:
|
|
185
|
+
"""
|
|
186
|
+
Execute a command in a container.
|
|
187
|
+
Args:
|
|
188
|
+
kwargs: keyword arguments used to expand functionality
|
|
189
|
+
Returns:
|
|
190
|
+
container id
|
|
191
|
+
"""
|
|
192
|
+
# Create a Docker client
|
|
193
|
+
client = docker.from_env()
|
|
194
|
+
env_vars = {"HOME": self.__CONTAINER_MOUNT, "PIP_USER": "yes"}
|
|
195
|
+
volumes = self.build_binding_volumes()
|
|
196
|
+
docker_user = None
|
|
197
|
+
if hasattr(os, 'getuid'):
|
|
198
|
+
uid = os.getuid()
|
|
199
|
+
gid = os.getgid()
|
|
200
|
+
docker_user = f"{uid}:{gid}"
|
|
201
|
+
# Run the container
|
|
202
|
+
container = client.containers.run(
|
|
203
|
+
self.docker_image,
|
|
204
|
+
command="bash",
|
|
205
|
+
volumes=volumes,
|
|
206
|
+
environment=env_vars,
|
|
207
|
+
stdin_open=True,
|
|
208
|
+
tty=True,
|
|
209
|
+
detach=True,
|
|
210
|
+
name=f"{self.container_prefix}_{str(uuid4())}" if self.container_prefix else None,
|
|
211
|
+
user=docker_user
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
return container.short_id
|
|
215
|
+
|
|
216
|
+
def convert_scripts_to_linux(self, experiment: Experiment, **kwargs) -> NoReturn:
|
|
217
|
+
"""
|
|
218
|
+
Convert the scripts to Linux format.
|
|
219
|
+
Args:
|
|
220
|
+
experiment: Experiment
|
|
221
|
+
kwargs: keyword arguments used to expand functionality
|
|
222
|
+
Returns:
|
|
223
|
+
No return
|
|
224
|
+
"""
|
|
225
|
+
directory = self.get_container_directory(experiment)
|
|
226
|
+
|
|
227
|
+
try:
|
|
228
|
+
commands = [
|
|
229
|
+
f"cd {directory}",
|
|
230
|
+
r"sed -i 's/\r//g' batch.sh;sed -i 's/\r//g' run_simulation.sh"
|
|
231
|
+
]
|
|
232
|
+
|
|
233
|
+
# Constructing the overall command
|
|
234
|
+
full_command = ["docker", "exec", self.container_id, "bash", "-c", ";".join(commands)]
|
|
235
|
+
# Execute the command
|
|
236
|
+
subprocess.run(full_command, stdout=subprocess.PIPE)
|
|
237
|
+
except subprocess.CalledProcessError as e:
|
|
238
|
+
user_logger.warning(f"Failed to convert script: {e}")
|
|
239
|
+
except Exception as ex:
|
|
240
|
+
user_logger.warning(f"Failed to convert script to Linux: {ex}")
|
|
241
|
+
|
|
242
|
+
def submit_experiment(self, experiment: Experiment, **kwargs) -> NoReturn:
|
|
243
|
+
"""
|
|
244
|
+
Submit an experiment to the container.
|
|
245
|
+
Args:
|
|
246
|
+
experiment: Experiment
|
|
247
|
+
kwargs: keyword arguments used to expand functionality
|
|
248
|
+
Returns:
|
|
249
|
+
No return
|
|
250
|
+
"""
|
|
251
|
+
directory = self.get_container_directory(experiment)
|
|
252
|
+
if logger.isEnabledFor(DEBUG):
|
|
253
|
+
logger.debug(f"Directory: {directory}")
|
|
254
|
+
logger.debug(f"container_id: {self.container_id}")
|
|
255
|
+
|
|
256
|
+
try:
|
|
257
|
+
# Commands to change directory and run the script
|
|
258
|
+
command = f'exec -a "EXPERIMENT:{experiment.id}" bash batch.sh &'
|
|
259
|
+
# Constructing the overall command
|
|
260
|
+
full_command = ["docker", "exec", "--workdir", directory, self.container_id, "bash", "-c", command]
|
|
261
|
+
|
|
262
|
+
# Execute the command using Popen for handling background processes
|
|
263
|
+
subprocess.Popen(full_command)
|
|
264
|
+
|
|
265
|
+
# Optionally, you can wait for a short period to ensure the command starts
|
|
266
|
+
# process = subprocess.Popen(full_command)
|
|
267
|
+
# process.wait(timeout=5)
|
|
268
|
+
|
|
269
|
+
logger.debug(f"Submit experiment {experiment.id} successfully")
|
|
270
|
+
except subprocess.TimeoutExpired:
|
|
271
|
+
user_logger.error(f"Submit experiment {experiment.id} timed out")
|
|
272
|
+
exit(-1)
|
|
273
|
+
except Exception as ex:
|
|
274
|
+
user_logger.error(f"Submit experiment {experiment.id} encounter Error: {ex}")
|
|
275
|
+
exit(-1)
|
|
276
|
+
|
|
277
|
+
def build_binding_volumes(self) -> Dict:
|
|
278
|
+
"""
|
|
279
|
+
Build the binding volumes for the container.
|
|
280
|
+
Returns:
|
|
281
|
+
bindings in dict format
|
|
282
|
+
"""
|
|
283
|
+
volumes = {
|
|
284
|
+
self.job_directory: {"bind": self.data_mount, "mode": "rw"}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
# Add user-defined volume mappings
|
|
288
|
+
if self.user_mounts is not None:
|
|
289
|
+
for key, value in self.user_mounts.items():
|
|
290
|
+
volumes[key] = {"bind": value, "mode": "rw"}
|
|
291
|
+
return volumes
|
|
292
|
+
|
|
293
|
+
def get_mounts(self) -> List:
|
|
294
|
+
"""
|
|
295
|
+
Build the mounts of the container.
|
|
296
|
+
Returns:
|
|
297
|
+
List of mounts (Dict)
|
|
298
|
+
"""
|
|
299
|
+
mounts = []
|
|
300
|
+
mount = {'Type': 'bind',
|
|
301
|
+
'Source': self.job_directory,
|
|
302
|
+
'Destination': self.data_mount,
|
|
303
|
+
'Mode': 'rw'}
|
|
304
|
+
mounts.append(mount)
|
|
305
|
+
|
|
306
|
+
# Add user-defined volume mappings
|
|
307
|
+
if self.user_mounts is not None:
|
|
308
|
+
for key, value in self.user_mounts.items():
|
|
309
|
+
mount = {'Type': 'bind',
|
|
310
|
+
'Source': key,
|
|
311
|
+
'Destination': value,
|
|
312
|
+
'Mode': 'rw'}
|
|
313
|
+
mounts.append(mount)
|
|
314
|
+
|
|
315
|
+
return mounts
|
|
316
|
+
|
|
317
|
+
def validate_mount(self, container: Union[str, Container]) -> bool:
|
|
318
|
+
"""
|
|
319
|
+
Compare the mounts of the container with the platform.
|
|
320
|
+
Args:
|
|
321
|
+
container: a container object or id.
|
|
322
|
+
Returns:
|
|
323
|
+
True/False
|
|
324
|
+
"""
|
|
325
|
+
if isinstance(container, str):
|
|
326
|
+
ct = get_container(container)
|
|
327
|
+
else:
|
|
328
|
+
ct = container
|
|
329
|
+
|
|
330
|
+
if ct is None:
|
|
331
|
+
logger.warning(f"Container {container} is not found.")
|
|
332
|
+
return False
|
|
333
|
+
mounts1 = self.get_mounts()
|
|
334
|
+
mounts2 = ct.attrs['Mounts']
|
|
335
|
+
return compare_mounts(mounts1, mounts2)
|
|
336
|
+
|
|
337
|
+
def get_container_directory(self, item: Union[Suite, Experiment, Simulation]) -> str:
|
|
338
|
+
"""
|
|
339
|
+
Get the container corresponding directory of an item.
|
|
340
|
+
Args:
|
|
341
|
+
item: Suite, Experiment or Simulation
|
|
342
|
+
Returns:
|
|
343
|
+
string Path
|
|
344
|
+
"""
|
|
345
|
+
item_dir = self.get_directory(item)
|
|
346
|
+
item_container_dir = map_container_path(self.job_directory, self.data_mount, str(item_dir))
|
|
347
|
+
|
|
348
|
+
return item_container_dir
|
|
349
|
+
|
|
350
|
+
def retrieve_match_containers(self, image: str = None) -> List:
|
|
351
|
+
"""
|
|
352
|
+
Find the containers that match math the image.
|
|
353
|
+
Args:
|
|
354
|
+
image: docker image
|
|
355
|
+
Returns:
|
|
356
|
+
list of containers
|
|
357
|
+
"""
|
|
358
|
+
if image is None:
|
|
359
|
+
image = self.docker_image
|
|
360
|
+
container_found = find_container_by_image(image, self.include_stopped)
|
|
361
|
+
container_match = []
|
|
362
|
+
if len(container_found) > 0:
|
|
363
|
+
for status, containers in container_found.items():
|
|
364
|
+
for container in containers:
|
|
365
|
+
if self.validate_mount(container):
|
|
366
|
+
container_match.append((status, container))
|
|
367
|
+
|
|
368
|
+
if len(container_match) == 0:
|
|
369
|
+
if logger.isEnabledFor(DEBUG):
|
|
370
|
+
logger.debug(f"Found container with image {image}, but no one match platform mounts.")
|
|
371
|
+
else:
|
|
372
|
+
if logger.isEnabledFor(DEBUG):
|
|
373
|
+
logger.debug(f"Not found container matching image {image}.")
|
|
374
|
+
|
|
375
|
+
return container_match
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Here we implement the ContainerPlatform experiment operations.
|
|
3
|
+
|
|
4
|
+
Copyright 2021, Bill & Melinda Gates Foundation. All rights reserved.
|
|
5
|
+
"""
|
|
6
|
+
import shutil
|
|
7
|
+
import subprocess
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
from typing import NoReturn, Dict, TYPE_CHECKING
|
|
10
|
+
from idmtools.core import ItemType
|
|
11
|
+
from idmtools.entities.experiment import Experiment
|
|
12
|
+
from idmtools_platform_file.platform_operations.experiment_operations import FilePlatformExperimentOperations
|
|
13
|
+
from idmtools_platform_container.container_operations.docker_operations import find_running_job
|
|
14
|
+
from logging import getLogger
|
|
15
|
+
|
|
16
|
+
logger = getLogger(__name__)
|
|
17
|
+
user_logger = getLogger('user')
|
|
18
|
+
|
|
19
|
+
if TYPE_CHECKING:
|
|
20
|
+
from idmtools_platform_container.container_platform import ContainerPlatform
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass
|
|
24
|
+
class ContainerPlatformExperimentOperations(FilePlatformExperimentOperations):
|
|
25
|
+
"""
|
|
26
|
+
Experiment Operations for Process Platform.
|
|
27
|
+
"""
|
|
28
|
+
platform: 'ContainerPlatform'
|
|
29
|
+
|
|
30
|
+
def platform_run_item(self, experiment: Experiment, **kwargs):
|
|
31
|
+
"""
|
|
32
|
+
Run experiment.
|
|
33
|
+
Args:
|
|
34
|
+
experiment: idmtools Experiment
|
|
35
|
+
kwargs: keyword arguments used to expand functionality
|
|
36
|
+
Returns:
|
|
37
|
+
None
|
|
38
|
+
"""
|
|
39
|
+
super().platform_run_item(experiment, **kwargs)
|
|
40
|
+
# Commission
|
|
41
|
+
self.platform.submit_job(experiment, **kwargs)
|
|
42
|
+
|
|
43
|
+
def post_run_item(self, experiment: Experiment, **kwargs):
|
|
44
|
+
"""
|
|
45
|
+
Trigger right after commissioning experiment on platform.
|
|
46
|
+
Args:
|
|
47
|
+
experiment: Experiment just commissioned
|
|
48
|
+
kwargs: keyword arguments used to expand functionality
|
|
49
|
+
Returns:
|
|
50
|
+
None
|
|
51
|
+
"""
|
|
52
|
+
super().post_run_item(experiment, **kwargs)
|
|
53
|
+
dry_run = kwargs.get('dry_run', False)
|
|
54
|
+
if not dry_run:
|
|
55
|
+
user_logger.info(f"\nContainer ID: {self.platform.container_id}")
|
|
56
|
+
user_logger.info(
|
|
57
|
+
f'\nYou may try the following command to check simulations running status: \n idmtools container status {experiment.id}')
|
|
58
|
+
|
|
59
|
+
def platform_cancel(self, experiment_id: str, force: bool = True) -> NoReturn:
|
|
60
|
+
"""
|
|
61
|
+
Cancel platform experiment's container job.
|
|
62
|
+
Args:
|
|
63
|
+
experiment_id: Experiment ID
|
|
64
|
+
force: bool, True/False
|
|
65
|
+
Returns:
|
|
66
|
+
No Return
|
|
67
|
+
"""
|
|
68
|
+
job = find_running_job(experiment_id)
|
|
69
|
+
if job:
|
|
70
|
+
logger.debug(
|
|
71
|
+
f"{job.item_type.name} {experiment_id} is running on Container {job.container_id}.")
|
|
72
|
+
kill_cmd = f"docker exec {job.container_id} pkill -TERM -g {job.job_id}"
|
|
73
|
+
result = subprocess.run(kill_cmd, shell=True, stderr=subprocess.PIPE, text=True)
|
|
74
|
+
if result.returncode == 0:
|
|
75
|
+
logger.debug(f"Successfully killed {job.item_type.name} {experiment_id}")
|
|
76
|
+
else:
|
|
77
|
+
logger.debug(f"Error killing {job.item_type.name} {experiment_id}: {result.stderr}")
|
|
78
|
+
else:
|
|
79
|
+
logger.debug(f"Experiment {experiment_id} is not running, no cancel needed...")
|
|
80
|
+
|
|
81
|
+
def platform_delete(self, experiment_id: str) -> NoReturn:
|
|
82
|
+
"""
|
|
83
|
+
Delete platform experiment.
|
|
84
|
+
Args:
|
|
85
|
+
experiment_id: Experiment ID
|
|
86
|
+
Returns:
|
|
87
|
+
No Return
|
|
88
|
+
"""
|
|
89
|
+
from idmtools_platform_file.tools.job_history import JobHistory
|
|
90
|
+
job = JobHistory.get_job(experiment_id)
|
|
91
|
+
exp_dir = job['EXPERIMENT_DIR']
|
|
92
|
+
try:
|
|
93
|
+
logger.debug(f"Deleting experiment {experiment_id}")
|
|
94
|
+
shutil.rmtree(exp_dir)
|
|
95
|
+
# Delete the job history
|
|
96
|
+
logger.debug(f"Deleting job history {experiment_id}")
|
|
97
|
+
JobHistory.delete(experiment_id)
|
|
98
|
+
except RuntimeError:
|
|
99
|
+
logger.debug(f"Could not delete the associated experiment {experiment_id}")
|
|
100
|
+
|
|
101
|
+
def create_sim_directory_map(self, experiment_id: str) -> Dict:
|
|
102
|
+
"""
|
|
103
|
+
Build simulation working directory mapping.
|
|
104
|
+
Args:
|
|
105
|
+
experiment_id: experiment id
|
|
106
|
+
|
|
107
|
+
Returns:
|
|
108
|
+
Dict of simulation id as key and working dir as value
|
|
109
|
+
"""
|
|
110
|
+
exp = self.platform.get_item(experiment_id, ItemType.EXPERIMENT, raw=False)
|
|
111
|
+
sims = exp.simulations
|
|
112
|
+
return {sim.id: str(self.platform.get_container_directory(sim)) for sim in sims}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Here we implement the ContainerPlatform simulation operations.
|
|
3
|
+
|
|
4
|
+
Copyright 2021, Bill & Melinda Gates Foundation. All rights reserved.
|
|
5
|
+
"""
|
|
6
|
+
import subprocess
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from typing import NoReturn, Dict
|
|
9
|
+
from idmtools.core import ItemType
|
|
10
|
+
from idmtools_platform_file.platform_operations.simulation_operations import FilePlatformSimulationOperations
|
|
11
|
+
from idmtools_platform_container.container_operations.docker_operations import find_running_job
|
|
12
|
+
from logging import getLogger
|
|
13
|
+
|
|
14
|
+
logger = getLogger(__name__)
|
|
15
|
+
user_logger = getLogger('user')
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass
|
|
19
|
+
class ContainerPlatformSimulationOperations(FilePlatformSimulationOperations):
|
|
20
|
+
"""
|
|
21
|
+
Simulation Operation for Container Platform.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
def platform_cancel(self, sim_id: str, force: bool = False) -> NoReturn:
|
|
25
|
+
"""
|
|
26
|
+
Cancel platform simulation's container job.
|
|
27
|
+
Args:
|
|
28
|
+
sim_id: simulation id
|
|
29
|
+
force: bool, True/False
|
|
30
|
+
Returns:
|
|
31
|
+
NoReturn
|
|
32
|
+
"""
|
|
33
|
+
job = find_running_job(sim_id)
|
|
34
|
+
if job:
|
|
35
|
+
if job.item_type != ItemType.SIMULATION:
|
|
36
|
+
pass
|
|
37
|
+
user_logger.debug(
|
|
38
|
+
f"{job.item_type.name} {sim_id} is running on Container {job.container_id}.")
|
|
39
|
+
kill_cmd = f"docker exec {job.container_id} kill -9 {job.job_id}"
|
|
40
|
+
result = subprocess.run(kill_cmd, shell=True, stderr=subprocess.PIPE, text=True)
|
|
41
|
+
if result.returncode == 0:
|
|
42
|
+
print(f"Successfully killed {job.item_type.name} {sim_id}")
|
|
43
|
+
else:
|
|
44
|
+
print(f"Error killing {job.item_type.name} {sim_id}: {result.stderr}")
|
|
45
|
+
else:
|
|
46
|
+
user_logger.info(f"Simulation {sim_id} is not running, no cancel needed...")
|
|
47
|
+
|
|
48
|
+
def create_sim_directory_map(self, simulation_id: str) -> Dict:
|
|
49
|
+
"""
|
|
50
|
+
Build simulation working directory mapping.
|
|
51
|
+
Args:
|
|
52
|
+
simulation_id: simulation id
|
|
53
|
+
|
|
54
|
+
Returns:
|
|
55
|
+
Dict of simulation id as key and working dir as value
|
|
56
|
+
"""
|
|
57
|
+
sim = self.platform.get_item(simulation_id, ItemType.SIMULATION, raw=False)
|
|
58
|
+
return {sim.id: str(self.platform.get_container_directory(sim))}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"""
|
|
2
|
+
idmtools process platform plugin definition.
|
|
3
|
+
|
|
4
|
+
Copyright 2021, Bill & Melinda Gates Foundation. All rights reserved.
|
|
5
|
+
"""
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Type, Dict
|
|
8
|
+
from idmtools.entities.iplatform import IPlatform
|
|
9
|
+
from idmtools.registry.platform_specification import example_configuration_impl, get_platform_impl, \
|
|
10
|
+
get_platform_type_impl, PlatformSpecification
|
|
11
|
+
from idmtools.registry.plugin_specification import get_description_impl
|
|
12
|
+
from idmtools_platform_container.container_platform import ContainerPlatform
|
|
13
|
+
|
|
14
|
+
CONTAINER_EXAMPLE_CONFIG = """
|
|
15
|
+
[Process]
|
|
16
|
+
job_directory = /data
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class ContainerPlatformSpecification(PlatformSpecification):
|
|
21
|
+
"""
|
|
22
|
+
Process Platform Specification definition.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
@get_description_impl
|
|
26
|
+
def get_description(self) -> str:
|
|
27
|
+
"""
|
|
28
|
+
Retrieve description.
|
|
29
|
+
"""
|
|
30
|
+
return "Provides access to the Container Platform to IDM Tools"
|
|
31
|
+
|
|
32
|
+
@get_platform_impl
|
|
33
|
+
def get(self, **configuration) -> IPlatform:
|
|
34
|
+
"""
|
|
35
|
+
Build our process platform from the passed in configuration object.
|
|
36
|
+
|
|
37
|
+
We do our import of platform here to avoid any weirdness
|
|
38
|
+
Args:
|
|
39
|
+
configuration:
|
|
40
|
+
Returns:
|
|
41
|
+
IPlatform
|
|
42
|
+
"""
|
|
43
|
+
return ContainerPlatform(**configuration)
|
|
44
|
+
|
|
45
|
+
@example_configuration_impl
|
|
46
|
+
def example_configuration(self):
|
|
47
|
+
"""
|
|
48
|
+
Retrieve example configuration.
|
|
49
|
+
"""
|
|
50
|
+
return CONTAINER_EXAMPLE_CONFIG
|
|
51
|
+
|
|
52
|
+
@get_platform_type_impl
|
|
53
|
+
def get_type(self) -> Type[ContainerPlatform]: # noqa: F821
|
|
54
|
+
"""
|
|
55
|
+
Get type.
|
|
56
|
+
Returns:
|
|
57
|
+
Type
|
|
58
|
+
"""
|
|
59
|
+
return ContainerPlatform
|
|
60
|
+
|
|
61
|
+
def get_version(self) -> str:
|
|
62
|
+
"""
|
|
63
|
+
Returns the version of the plugin.
|
|
64
|
+
Returns:
|
|
65
|
+
Plugin Version
|
|
66
|
+
"""
|
|
67
|
+
from idmtools_platform_container import __version__
|
|
68
|
+
return __version__
|
|
69
|
+
|
|
70
|
+
def get_configuration_aliases(self) -> Dict[str, Dict]:
|
|
71
|
+
"""
|
|
72
|
+
Provides configuration aliases that exist in CONTAINER.
|
|
73
|
+
"""
|
|
74
|
+
config_aliases = dict(
|
|
75
|
+
CONTAINER=dict(
|
|
76
|
+
job_directory=str(Path.home())
|
|
77
|
+
)
|
|
78
|
+
)
|
|
79
|
+
return config_aliases
|