cloud-submit 0.1__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.
- cloud_submit/__init__.py +30 -0
- cloud_submit/cli.py +1014 -0
- cloud_submit/config.py +164 -0
- cloud_submit/controller.py +375 -0
- cloud_submit/environment_handler.py +398 -0
- cloud_submit/envs/aws/__init__.py +2 -0
- cloud_submit/envs/aws/local_environment_handler.py +478 -0
- cloud_submit/envs/aws/local_execution_handler.py +5 -0
- cloud_submit/envs/aws/remote_environment_handler.py +401 -0
- cloud_submit/envs/aws/remote_execution_handler.py +87 -0
- cloud_submit/envs/aws/s3_tools.py +46 -0
- cloud_submit/envs/local/environment_handler.py +121 -0
- cloud_submit/envs/local/execution_handler.py +5 -0
- cloud_submit/execution/base_execution_handler.py +49 -0
- cloud_submit/execution/config.py +248 -0
- cloud_submit/execution/execute.py +108 -0
- cloud_submit/execution/utils.py +98 -0
- cloud_submit/images.py +58 -0
- cloud_submit/utils.py +67 -0
- cloud_submit-0.1.dist-info/METADATA +12 -0
- cloud_submit-0.1.dist-info/RECORD +24 -0
- cloud_submit-0.1.dist-info/WHEEL +4 -0
- cloud_submit-0.1.dist-info/entry_points.txt +2 -0
- cloud_submit-0.1.dist-info/licenses/LICENSE +19 -0
|
@@ -0,0 +1,478 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import sys
|
|
3
|
+
import shutil
|
|
4
|
+
import subprocess
|
|
5
|
+
import json
|
|
6
|
+
import datetime as dt
|
|
7
|
+
import re
|
|
8
|
+
|
|
9
|
+
from cloud_submit import (
|
|
10
|
+
LocalEnv,
|
|
11
|
+
ensure_path,
|
|
12
|
+
CloudSubmitError,
|
|
13
|
+
build_docker_mount_option,
|
|
14
|
+
run_command,
|
|
15
|
+
parse_image_ref,
|
|
16
|
+
BaseImage,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
from .s3_tools import get_remote_artifact_path
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def build_artifacts_mount_option(path, scope):
|
|
24
|
+
return build_docker_mount_option(path, f'/root/artifacts/{scope}')
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class LocalAWSEnv(LocalEnv):
|
|
28
|
+
def __init__(
|
|
29
|
+
self,
|
|
30
|
+
name,
|
|
31
|
+
project,
|
|
32
|
+
user,
|
|
33
|
+
aws_account_id,
|
|
34
|
+
aws_region,
|
|
35
|
+
aws_profile,
|
|
36
|
+
s3_bucket,
|
|
37
|
+
s3_prefix,
|
|
38
|
+
docker_command='docker',
|
|
39
|
+
docker_namespace='csub',
|
|
40
|
+
docker_login_refresh_hours=6,
|
|
41
|
+
aws_command='aws',
|
|
42
|
+
):
|
|
43
|
+
super().__init__(
|
|
44
|
+
name=name,
|
|
45
|
+
project=project,
|
|
46
|
+
user=user,
|
|
47
|
+
docker_command=docker_command,
|
|
48
|
+
docker_registry=\
|
|
49
|
+
f'{aws_account_id}.dkr.ecr.{aws_region}.amazonaws.com',
|
|
50
|
+
docker_namespace=docker_namespace,
|
|
51
|
+
)
|
|
52
|
+
self._aws_account_id = str(aws_account_id)
|
|
53
|
+
self._aws_region = str(aws_region)
|
|
54
|
+
self._aws_profile = str(aws_profile)
|
|
55
|
+
self._s3_bucket = str(s3_bucket)
|
|
56
|
+
self._s3_prefix = str(s3_prefix)
|
|
57
|
+
self._aws_command = str(aws_command)
|
|
58
|
+
self._docker_login_refresh_hours = float(docker_login_refresh_hours)
|
|
59
|
+
|
|
60
|
+
self._last_docker_login_ts = None
|
|
61
|
+
|
|
62
|
+
def _docker_login(self):
|
|
63
|
+
now = dt.datetime.now(tz=dt.UTC)
|
|
64
|
+
if self._last_docker_login_ts is not None:
|
|
65
|
+
seconds_since_last_login = \
|
|
66
|
+
(now - self._last_docker_login_ts).total_seconds()
|
|
67
|
+
if seconds_since_last_login < self._docker_login_refresh_hours*3600:
|
|
68
|
+
return
|
|
69
|
+
|
|
70
|
+
result = run_command(
|
|
71
|
+
[
|
|
72
|
+
self._aws_command,
|
|
73
|
+
'ecr',
|
|
74
|
+
'get-login-password',
|
|
75
|
+
'--profile', self._aws_profile,
|
|
76
|
+
'--region', self._aws_region,
|
|
77
|
+
],
|
|
78
|
+
stdout=subprocess.PIPE,
|
|
79
|
+
text=True,
|
|
80
|
+
hide_stderr=True,
|
|
81
|
+
)
|
|
82
|
+
password = result.stdout.strip()
|
|
83
|
+
|
|
84
|
+
run_command(
|
|
85
|
+
[
|
|
86
|
+
self._docker_command,
|
|
87
|
+
'login',
|
|
88
|
+
'--username', 'AWS',
|
|
89
|
+
'--password', password,
|
|
90
|
+
self._docker_registry,
|
|
91
|
+
],
|
|
92
|
+
hide_stderr=True,
|
|
93
|
+
stdout=subprocess.DEVNULL,
|
|
94
|
+
)
|
|
95
|
+
self._last_docker_login_ts = now
|
|
96
|
+
|
|
97
|
+
def install_execution_handler(self, path):
|
|
98
|
+
sourcedir = os.path.dirname(__file__)
|
|
99
|
+
shutil.copyfile(
|
|
100
|
+
os.path.join(sourcedir, 'local_execution_handler.py'),
|
|
101
|
+
os.path.join(path, 'execution_handler.py'),
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
def _build_image(self, path, image, build_id, push):
|
|
105
|
+
ref = super().build_image(path, image, build_id)
|
|
106
|
+
self._docker_login()
|
|
107
|
+
|
|
108
|
+
if push:
|
|
109
|
+
# push image
|
|
110
|
+
run_command([self._docker_command, 'push', ref])
|
|
111
|
+
|
|
112
|
+
# get image digest
|
|
113
|
+
result = run_command(
|
|
114
|
+
[
|
|
115
|
+
self._docker_command,
|
|
116
|
+
'buildx', 'imagetools', 'inspect',
|
|
117
|
+
'--format', '{{json .Manifest.Digest}}',
|
|
118
|
+
ref,
|
|
119
|
+
],
|
|
120
|
+
stdout=subprocess.PIPE,
|
|
121
|
+
text=True,
|
|
122
|
+
)
|
|
123
|
+
digest = result.stdout.strip('"')
|
|
124
|
+
return ref + '@' + digest
|
|
125
|
+
return ref
|
|
126
|
+
|
|
127
|
+
def build_image(self, path, image, build_id):
|
|
128
|
+
return self._build_image(
|
|
129
|
+
path=path,
|
|
130
|
+
image=image,
|
|
131
|
+
build_id=build_id,
|
|
132
|
+
push=isinstance(image, BaseImage),
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
def pull_base_image(self, ref):
|
|
136
|
+
self._docker_login()
|
|
137
|
+
run_command([
|
|
138
|
+
self._docker_command,
|
|
139
|
+
'pull',
|
|
140
|
+
ref,
|
|
141
|
+
])
|
|
142
|
+
|
|
143
|
+
def list_remote_image_tags(self, repo_name):
|
|
144
|
+
self._docker_login()
|
|
145
|
+
repo_name = '/'.join(repo_name.split('/')[1:])
|
|
146
|
+
result = run_command(
|
|
147
|
+
[
|
|
148
|
+
self._aws_command,
|
|
149
|
+
'ecr',
|
|
150
|
+
'list-images',
|
|
151
|
+
'--region', self._aws_region,
|
|
152
|
+
'--profile', self._aws_profile,
|
|
153
|
+
'--filter', 'tagStatus=TAGGED',
|
|
154
|
+
'--repository-name', repo_name,
|
|
155
|
+
],
|
|
156
|
+
stdout=subprocess.PIPE,
|
|
157
|
+
text=True,
|
|
158
|
+
)
|
|
159
|
+
result = json.loads(result.stdout)
|
|
160
|
+
tags = [img['imageTag'] for img in result['imageIds']]
|
|
161
|
+
return tags
|
|
162
|
+
|
|
163
|
+
def remove_remote_image_refs(self, refs):
|
|
164
|
+
tags = {}
|
|
165
|
+
for ref in refs:
|
|
166
|
+
registry, repo, tag, digest = parse_image_ref(ref)
|
|
167
|
+
if registry != self._docker_registry:
|
|
168
|
+
raise CloudSubmitError(
|
|
169
|
+
f'Cannot delete image ref {ref} due to unknown registry.')
|
|
170
|
+
tags[repo] = tags.get(repo, [])
|
|
171
|
+
tags[repo].append(tag)
|
|
172
|
+
|
|
173
|
+
for repo, tag_list in tags.items():
|
|
174
|
+
run_command([
|
|
175
|
+
self._aws_command,
|
|
176
|
+
'ecr',
|
|
177
|
+
'batch-delete-image',
|
|
178
|
+
'--region', self._aws_region,
|
|
179
|
+
'--profile', self._aws_profile,
|
|
180
|
+
'--repository-name', repo,
|
|
181
|
+
'--image-ids',
|
|
182
|
+
*[f'imageTag={tag}' for tag in tag_list],
|
|
183
|
+
])
|
|
184
|
+
|
|
185
|
+
def get_remote_artifact_path(self, artifact, run_id=None):
|
|
186
|
+
return get_remote_artifact_path(
|
|
187
|
+
artifact,
|
|
188
|
+
self._project,
|
|
189
|
+
self._user,
|
|
190
|
+
run_id,
|
|
191
|
+
self._s3_bucket,
|
|
192
|
+
self._s3_prefix,
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
def _get_s3_objects(self, prefix, recursive=False):
|
|
196
|
+
command = [
|
|
197
|
+
self._aws_command,
|
|
198
|
+
's3',
|
|
199
|
+
'ls',
|
|
200
|
+
'--profile', self._aws_profile,
|
|
201
|
+
'--region', self._aws_region,
|
|
202
|
+
]
|
|
203
|
+
if recursive:
|
|
204
|
+
command.append('--recursive')
|
|
205
|
+
command.append(prefix)
|
|
206
|
+
result = run_command(
|
|
207
|
+
command,
|
|
208
|
+
check=False,
|
|
209
|
+
stdout=subprocess.PIPE,
|
|
210
|
+
text=True,
|
|
211
|
+
)
|
|
212
|
+
lines = result.stdout.strip()
|
|
213
|
+
if not lines:
|
|
214
|
+
return []
|
|
215
|
+
lines = lines.split('\n')
|
|
216
|
+
return [l.split()[-1] for l in lines]
|
|
217
|
+
|
|
218
|
+
def list_remote_artifacts(self, artifacts, run_ids=None):
|
|
219
|
+
if run_ids is not None:
|
|
220
|
+
run_ids = set(run_ids)
|
|
221
|
+
|
|
222
|
+
project_artifacts = set()
|
|
223
|
+
user_artifacts = set()
|
|
224
|
+
run_artifacts = set()
|
|
225
|
+
for a in artifacts:
|
|
226
|
+
if a.kind != 'file':
|
|
227
|
+
continue
|
|
228
|
+
if a.scope == 'project':
|
|
229
|
+
project_artifacts.add(a.name)
|
|
230
|
+
elif a.scope == 'user':
|
|
231
|
+
user_artifacts.add(a.name)
|
|
232
|
+
elif a.scope == 'run':
|
|
233
|
+
run_artifacts.add(a.name)
|
|
234
|
+
|
|
235
|
+
results = {a.name: set() for a in artifacts}
|
|
236
|
+
if project_artifacts:
|
|
237
|
+
keys = self._get_s3_objects('/'.join([
|
|
238
|
+
's3:/', self._s3_bucket, self._s3_prefix, self._project,
|
|
239
|
+
'shared/',
|
|
240
|
+
]))
|
|
241
|
+
for key in keys:
|
|
242
|
+
if key.endswith('/'):
|
|
243
|
+
key = key[:-1]
|
|
244
|
+
artifact = key
|
|
245
|
+
if artifact in project_artifacts:
|
|
246
|
+
results[artifact].add(None)
|
|
247
|
+
if user_artifacts:
|
|
248
|
+
keys = self._get_s3_objects('/'.join([
|
|
249
|
+
's3:/', self._s3_bucket, self._s3_prefix, self._project,
|
|
250
|
+
'users', self._user, 'shared/',
|
|
251
|
+
]))
|
|
252
|
+
for key in keys:
|
|
253
|
+
if key.endswith('/'):
|
|
254
|
+
key = key[:-1]
|
|
255
|
+
artifact = key
|
|
256
|
+
if artifact in user_artifacts:
|
|
257
|
+
results[artifact].add(None)
|
|
258
|
+
if run_artifacts:
|
|
259
|
+
prefix = '/'.join(
|
|
260
|
+
[self._s3_prefix, self._project, 'users', self._user, 'runs/'])
|
|
261
|
+
keys = self._get_s3_objects(
|
|
262
|
+
'/'.join(['s3:/', self._s3_bucket, prefix]),
|
|
263
|
+
recursive=True,
|
|
264
|
+
)
|
|
265
|
+
for key in keys:
|
|
266
|
+
key = key[len(prefix):].split('/')
|
|
267
|
+
run_id = key[0]
|
|
268
|
+
if run_ids is not None and run_id not in run_ids:
|
|
269
|
+
continue
|
|
270
|
+
if len(key) > 1 and key[1] in run_artifacts:
|
|
271
|
+
artifact = key[1]
|
|
272
|
+
results[artifact].add(run_id)
|
|
273
|
+
|
|
274
|
+
results = [sorted(results[a.name]) for a in artifacts]
|
|
275
|
+
return results
|
|
276
|
+
|
|
277
|
+
def remove_remote_artifacts(self, artifacts, run_ids):
|
|
278
|
+
path = '/'.join(
|
|
279
|
+
['s3:/', self._s3_bucket, self._s3_prefix, self._project])
|
|
280
|
+
command = [
|
|
281
|
+
self._aws_command,
|
|
282
|
+
's3',
|
|
283
|
+
'rm',
|
|
284
|
+
'--profile', self._aws_profile,
|
|
285
|
+
'--region', self._aws_region,
|
|
286
|
+
'--recursive',
|
|
287
|
+
path,
|
|
288
|
+
'--exclude', '*',
|
|
289
|
+
]
|
|
290
|
+
for artifact, runs in zip(artifacts, run_ids):
|
|
291
|
+
if artifact.kind != 'file':
|
|
292
|
+
continue
|
|
293
|
+
if artifact.scope == 'project':
|
|
294
|
+
for run_id in runs:
|
|
295
|
+
command.extend([
|
|
296
|
+
'--include',
|
|
297
|
+
'/'.join(['shared', artifact.name]),
|
|
298
|
+
'--include',
|
|
299
|
+
'/'.join(['shared', artifact.name, '*']),
|
|
300
|
+
])
|
|
301
|
+
elif artifact.scope == 'user':
|
|
302
|
+
for run_id in runs:
|
|
303
|
+
command.extend([
|
|
304
|
+
'--include',
|
|
305
|
+
'/'.join([
|
|
306
|
+
'users', self._user, 'shared', artifact.name]),
|
|
307
|
+
'--include',
|
|
308
|
+
'/'.join([
|
|
309
|
+
'users', self._user, 'shared', artifact.name, '*']),
|
|
310
|
+
])
|
|
311
|
+
elif artifact.scope == 'run':
|
|
312
|
+
for run_id in runs:
|
|
313
|
+
command.extend([
|
|
314
|
+
'--include',
|
|
315
|
+
'/'.join([
|
|
316
|
+
'users', self._user, 'runs', run_id,
|
|
317
|
+
artifact.name,
|
|
318
|
+
]),
|
|
319
|
+
'--include',
|
|
320
|
+
'/'.join([
|
|
321
|
+
'users', self._user, 'runs', run_id,
|
|
322
|
+
artifact.name, '*',
|
|
323
|
+
]),
|
|
324
|
+
])
|
|
325
|
+
|
|
326
|
+
run_command(command)
|
|
327
|
+
|
|
328
|
+
def push_artifacts(self, artifacts, run_ids):
|
|
329
|
+
remote_path = '/'.join(
|
|
330
|
+
['s3:/', self._s3_bucket, self._s3_prefix, self._project])
|
|
331
|
+
local_path = 'artifacts'
|
|
332
|
+
project_dir = 'shared'
|
|
333
|
+
user_dir = os.path.join('users', self._user, 'shared')
|
|
334
|
+
run_dir = lambda run_id: os.path.join(
|
|
335
|
+
'users', self._user, 'runs', run_id)
|
|
336
|
+
|
|
337
|
+
command = [
|
|
338
|
+
self._aws_command,
|
|
339
|
+
's3',
|
|
340
|
+
'cp',
|
|
341
|
+
'--profile', self._aws_profile,
|
|
342
|
+
'--region', self._aws_region,
|
|
343
|
+
'--recursive',
|
|
344
|
+
local_path,
|
|
345
|
+
remote_path,
|
|
346
|
+
'--exclude', '*',
|
|
347
|
+
]
|
|
348
|
+
for artifact, runs in zip(artifacts, run_ids):
|
|
349
|
+
if artifact.kind != 'file':
|
|
350
|
+
continue
|
|
351
|
+
if artifact.scope == 'project':
|
|
352
|
+
for run_id in runs:
|
|
353
|
+
command.extend([
|
|
354
|
+
'--include',
|
|
355
|
+
os.path.join(project_dir, artifact.name),
|
|
356
|
+
'--include',
|
|
357
|
+
os.path.join(project_dir, artifact.name, '*'),
|
|
358
|
+
])
|
|
359
|
+
elif artifact.scope == 'user':
|
|
360
|
+
for run_id in runs:
|
|
361
|
+
command.extend([
|
|
362
|
+
'--include',
|
|
363
|
+
os.path.join(user_dir, artifact.name),
|
|
364
|
+
'--include',
|
|
365
|
+
os.path.join(user_dir, artifact.name, '*'),
|
|
366
|
+
])
|
|
367
|
+
elif artifact.scope == 'run':
|
|
368
|
+
for run_id in runs:
|
|
369
|
+
command.extend([
|
|
370
|
+
'--include',
|
|
371
|
+
os.path.join(run_dir(run_id), artifact.name),
|
|
372
|
+
'--include',
|
|
373
|
+
os.path.join(run_dir(run_id), artifact.name, '*'),
|
|
374
|
+
])
|
|
375
|
+
|
|
376
|
+
run_command(command)
|
|
377
|
+
|
|
378
|
+
def pull_artifacts(self, artifacts, run_ids):
|
|
379
|
+
remote_path = '/'.join(
|
|
380
|
+
['s3:/', self._s3_bucket, self._s3_prefix, self._project])
|
|
381
|
+
local_path = 'artifacts'
|
|
382
|
+
project_dir = 'shared'
|
|
383
|
+
user_dir = os.path.join('users', self._user, 'shared')
|
|
384
|
+
run_dir = lambda run_id: os.path.join(
|
|
385
|
+
'users', self._user, 'runs', run_id)
|
|
386
|
+
|
|
387
|
+
command = [
|
|
388
|
+
self._aws_command,
|
|
389
|
+
's3',
|
|
390
|
+
'cp',
|
|
391
|
+
'--profile', self._aws_profile,
|
|
392
|
+
'--region', self._aws_region,
|
|
393
|
+
'--recursive',
|
|
394
|
+
remote_path,
|
|
395
|
+
local_path,
|
|
396
|
+
'--exclude', '*',
|
|
397
|
+
]
|
|
398
|
+
for artifact, runs in zip(artifacts, run_ids):
|
|
399
|
+
if artifact.kind != 'file':
|
|
400
|
+
continue
|
|
401
|
+
if artifact.scope == 'project':
|
|
402
|
+
for run_id in runs:
|
|
403
|
+
command.extend([
|
|
404
|
+
'--include',
|
|
405
|
+
'/'.join([project_dir, artifact.name]),
|
|
406
|
+
'--include',
|
|
407
|
+
'/'.join([project_dir, artifact.name, '*']),
|
|
408
|
+
])
|
|
409
|
+
elif artifact.scope == 'user':
|
|
410
|
+
for run_id in runs:
|
|
411
|
+
command.extend([
|
|
412
|
+
'--include',
|
|
413
|
+
'/'.join([user_dir, artifact.name]),
|
|
414
|
+
'--include',
|
|
415
|
+
'/'.join([user_dir, artifact.name, '*']),
|
|
416
|
+
])
|
|
417
|
+
elif artifact.scope == 'run':
|
|
418
|
+
for run_id in runs:
|
|
419
|
+
command.extend([
|
|
420
|
+
'--include',
|
|
421
|
+
'/'.join([run_dir(run_id), artifact.name]),
|
|
422
|
+
'--include',
|
|
423
|
+
'/'.join([run_dir(run_id), artifact.name, '*']),
|
|
424
|
+
])
|
|
425
|
+
|
|
426
|
+
run_command(command)
|
|
427
|
+
|
|
428
|
+
def copy_remote_artifacts(self, artifacts, from_run_id, to_run_id):
|
|
429
|
+
base_path = '/'.join([
|
|
430
|
+
's3:/', self._s3_bucket, self._s3_prefix, self._project,
|
|
431
|
+
'users', self._user, 'runs',
|
|
432
|
+
])
|
|
433
|
+
src_path = '/'.join([base_path, from_run_id])
|
|
434
|
+
dst_path = '/'.join([base_path, to_run_id])
|
|
435
|
+
command = [
|
|
436
|
+
self._aws_command,
|
|
437
|
+
's3',
|
|
438
|
+
'cp',
|
|
439
|
+
'--profile', self._aws_profile,
|
|
440
|
+
'--region', self._aws_region,
|
|
441
|
+
'--recursive',
|
|
442
|
+
src_path,
|
|
443
|
+
dst_path,
|
|
444
|
+
'--exclude', '*',
|
|
445
|
+
]
|
|
446
|
+
|
|
447
|
+
for artifact in artifacts:
|
|
448
|
+
command.extend([
|
|
449
|
+
'--include', artifact.name,
|
|
450
|
+
'--include', '/'.join([artifact.name, '*'])
|
|
451
|
+
])
|
|
452
|
+
run_command(command)
|
|
453
|
+
|
|
454
|
+
def move_remote_artifacts(self, artifacts, from_run_id, to_run_id):
|
|
455
|
+
base_path = '/'.join([
|
|
456
|
+
's3:/', self._s3_bucket, self._s3_prefix, self._project,
|
|
457
|
+
'users', self._user, 'runs',
|
|
458
|
+
])
|
|
459
|
+
src_path = '/'.join([base_path, from_run_id])
|
|
460
|
+
dst_path = '/'.join([base_path, to_run_id])
|
|
461
|
+
command = [
|
|
462
|
+
self._aws_command,
|
|
463
|
+
's3',
|
|
464
|
+
'mv',
|
|
465
|
+
'--profile', self._aws_profile,
|
|
466
|
+
'--region', self._aws_region,
|
|
467
|
+
'--recursive',
|
|
468
|
+
src_path,
|
|
469
|
+
dst_path,
|
|
470
|
+
'--exclude', '*',
|
|
471
|
+
]
|
|
472
|
+
|
|
473
|
+
for artifact in artifacts:
|
|
474
|
+
command.extend([
|
|
475
|
+
'--include', artifact.name,
|
|
476
|
+
'--include', '/'.join([artifact.name, '*'])
|
|
477
|
+
])
|
|
478
|
+
run_command(command)
|