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.
@@ -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)
@@ -0,0 +1,5 @@
1
+ from .base_execution_handler import BaseExecutionHandler
2
+
3
+
4
+ def create_execution_handler():
5
+ return BaseExecutionHandler()