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,401 @@
1
+ import datetime as dt
2
+ import os
3
+ import shutil
4
+ import json
5
+ import subprocess
6
+
7
+ from cloud_submit import run_command, write_json, CloudSubmitError
8
+
9
+ from .local_environment_handler import LocalAWSEnv
10
+
11
+
12
+ class RemoteAWSEnv(LocalAWSEnv):
13
+ def __init__(
14
+ self,
15
+ name,
16
+ project,
17
+ user,
18
+ aws_account_id,
19
+ aws_region,
20
+ aws_profile,
21
+ s3_bucket,
22
+ s3_prefix,
23
+ ecs_cluster_arn,
24
+ ecs_capacity_provider,
25
+ ecs_infrastructure_role_arn,
26
+ ecs_execution_role_arn,
27
+ ecs_task_role_arn,
28
+ stepfunctions_role_arn,
29
+ docker_command='docker',
30
+ docker_namespace='csub',
31
+ docker_login_refresh_hours=6,
32
+ aws_command='aws',
33
+ log_group_prefix='/ecs/csub/',
34
+ container_aws_command='aws',
35
+ ):
36
+ super().__init__(
37
+ name=name,
38
+ project=project,
39
+ user=user,
40
+ aws_account_id=aws_account_id,
41
+ aws_region=aws_region,
42
+ aws_profile=aws_profile,
43
+ s3_bucket=s3_bucket,
44
+ s3_prefix=s3_prefix,
45
+ docker_command=docker_command,
46
+ docker_namespace=docker_namespace,
47
+ docker_login_refresh_hours=docker_login_refresh_hours,
48
+ aws_command=aws_command,
49
+ )
50
+ self._ecs_cluster_arn = str(ecs_cluster_arn)
51
+ self._ecs_capacity_provider = str(ecs_capacity_provider)
52
+ self._ecs_infrastructure_role_arn = str(ecs_infrastructure_role_arn)
53
+ self._ecs_execution_role_arn = str(ecs_execution_role_arn)
54
+ self._ecs_task_role_arn = str(ecs_task_role_arn)
55
+ self._stepfunctions_role_arn = str(stepfunctions_role_arn)
56
+ self._log_group_prefix = str(log_group_prefix)
57
+ self._container_aws_command = str(container_aws_command)
58
+
59
+ def build_image(self, path, image, build_id):
60
+ return self._build_image(
61
+ path=path,
62
+ image=image,
63
+ build_id=build_id,
64
+ push=True,
65
+ )
66
+
67
+ def install_execution_handler(self, path):
68
+ sourcedir = os.path.dirname(__file__)
69
+ shutil.copyfile(
70
+ os.path.join(sourcedir, 'remote_execution_handler.py'),
71
+ os.path.join(path, 'execution_handler.py'),
72
+ )
73
+ shutil.copyfile(
74
+ os.path.join(sourcedir, 's3_tools.py'),
75
+ os.path.join(path, 's3_tools.py'),
76
+ )
77
+ write_json(
78
+ {
79
+ 'project': self._project,
80
+ 'user': self._user,
81
+ 'container_aws_command': self._container_aws_command,
82
+ 's3_bucket': self._s3_bucket,
83
+ 's3_prefix': self._s3_prefix,
84
+ },
85
+ os.path.join(path, 'execution_config.json'),
86
+ )
87
+
88
+ def _make_workflow_name(self, run_id):
89
+ return '--'.join([self._project, self._user, run_id])
90
+
91
+ def _make_workflow_arn(self, workflow_name):
92
+ return ':'.join([
93
+ 'arn', 'aws', 'states',
94
+ self._aws_region, self._aws_account_id,
95
+ 'stateMachine', workflow_name,
96
+ ])
97
+
98
+ def _check_workflow_exists(self, workflow_name):
99
+ workflow_arn = self._make_workflow_arn(workflow_name)
100
+ result = run_command(
101
+ [
102
+ self._aws_command,
103
+ 'stepfunctions',
104
+ 'list-state-machines',
105
+ '--profile', self._aws_profile,
106
+ '--region', self._aws_region,
107
+ ],
108
+ stdout=subprocess.PIPE,
109
+ text=True,
110
+ hide_stderr=True,
111
+ )
112
+ result = json.loads(result.stdout)
113
+ arns = set(s['stateMachineArn'] for s in result['stateMachines'])
114
+ return workflow_arn in arns
115
+
116
+ def _group_steps(self, pipeline, image_refs):
117
+ groups = []
118
+ last_spec = None
119
+ for i, step in enumerate(pipeline.steps):
120
+ if step.name in image_refs:
121
+ spec = (image_refs[step.name], step.spec)
122
+ if spec is None or spec != last_spec:
123
+ groups.append([])
124
+ groups[-1].append(i)
125
+ last_spec = spec
126
+ return groups
127
+
128
+ def _build_task(
129
+ self,
130
+ workflow_name,
131
+ task_index,
132
+ image_ref,
133
+ spec,
134
+ pipeline_name,
135
+ step_names,
136
+ timestamp,
137
+ run_id,
138
+ run_steps,
139
+ is_last,
140
+ ):
141
+ cpu = spec.get('cpu', 1)
142
+ memory = spec.get('memory', 2)
143
+ disk = spec.get('disk', 10)
144
+ return {
145
+ f"RegisterTask{task_index}": {
146
+ "Type": "Task",
147
+ "Resource": "arn:aws:states:::aws-sdk:ecs:registerTaskDefinition",
148
+ "Parameters": {
149
+ "Family": f"{workflow_name}--{task_index}",
150
+ "RequiresCompatibilities": ["EC2"],
151
+ "NetworkMode": "host",
152
+ "Cpu": f'{cpu} vCPU',
153
+ "Memory": f'{memory} GB',
154
+ "ExecutionRoleArn": self._ecs_execution_role_arn,
155
+ "TaskRoleArn": self._ecs_task_role_arn,
156
+ "ContainerDefinitions": [
157
+ {
158
+ "Name": "main",
159
+ "Image": image_ref,
160
+ "Command": [pipeline_name, ','.join(step_names)],
161
+ "Essential": True,
162
+ "MountPoints": [
163
+ {
164
+ "SourceVolume": "artifacts",
165
+ "ContainerPath": "/mnt/artifacts"
166
+ }
167
+ ],
168
+ "Environment": [
169
+ {
170
+ "Name": "CSUB_TIMESTAMP",
171
+ "Value": timestamp.isoformat(),
172
+ },
173
+ {
174
+ "Name": "CSUB_RUN_ID",
175
+ "Value": run_id,
176
+ },
177
+ {
178
+ "Name": "CSUB_WORKER_INDEX",
179
+ "Value": "-1",
180
+ },
181
+ {
182
+ "Name": "CSUB_RUN_STEPS",
183
+ "Value": run_steps,
184
+ },
185
+ ],
186
+ "LogConfiguration": {
187
+ "LogDriver": "awslogs",
188
+ "Options": {
189
+ "awslogs-group":
190
+ self._log_group_prefix + workflow_name,
191
+ "awslogs-region": self._aws_region,
192
+ "awslogs-stream-prefix": "ecs",
193
+ "awslogs-create-group": "true"
194
+ }
195
+ }
196
+ }
197
+ ],
198
+ "Volumes": [
199
+ {
200
+ "Name": "artifacts",
201
+ "ConfiguredAtLaunch": True
202
+ }
203
+ ]
204
+ },
205
+ "ResultPath": "$.RegisteredTask",
206
+ "Next": f"RunTask{task_index}"
207
+ },
208
+ f"RunTask{task_index}": {
209
+ "Type": "Task",
210
+ "Resource": "arn:aws:states:::ecs:runTask.sync",
211
+ "Parameters": {
212
+ "Cluster": self._ecs_cluster_arn,
213
+ "TaskDefinition.$": "$.RegisteredTask.TaskDefinition.TaskDefinitionArn",
214
+ "CapacityProviderStrategy": [
215
+ {
216
+ "CapacityProvider": self._ecs_capacity_provider,
217
+ "Weight": 1,
218
+ "Base": 0
219
+ }
220
+ ],
221
+ "VolumeConfigurations": [
222
+ {
223
+ "Name": "artifacts",
224
+ "ManagedEBSVolume": {
225
+ "SizeInGiB": disk,
226
+ "VolumeType": "gp3",
227
+ "Encrypted": False,
228
+ "FilesystemType": "ext4",
229
+ "TerminationPolicy": {
230
+ "DeleteOnTermination": True
231
+ },
232
+ "RoleArn": self._ecs_infrastructure_role_arn,
233
+ }
234
+ }
235
+ ]
236
+ },
237
+ "ResultPath": "$.ExecutionResult",
238
+ "Catch": [
239
+ {
240
+ "ErrorEquals": ["States.ALL"],
241
+ "ResultPath": "$.ExecutionError",
242
+ "Next": f"DeregisterTask{task_index}OnFailure"
243
+ }
244
+ ],
245
+ "Next": f"DeregisterTask{task_index}OnSuccess"
246
+ },
247
+ f"DeregisterTask{task_index}OnSuccess": {
248
+ "Type": "Task",
249
+ "Resource": "arn:aws:states:::aws-sdk:ecs:deregisterTaskDefinition",
250
+ "Parameters": {
251
+ "TaskDefinition.$": "$.RegisteredTask.TaskDefinition.TaskDefinitionArn"
252
+ },
253
+ ("End" if is_last else "Next"): (
254
+ True if is_last else f"RegisterTask{task_index + 1}"),
255
+ },
256
+ f"DeregisterTask{task_index}OnFailure": {
257
+ "Type": "Task",
258
+ "Resource": "arn:aws:states:::aws-sdk:ecs:deregisterTaskDefinition",
259
+ "Parameters": {
260
+ "TaskDefinition.$": "$.RegisteredTask.TaskDefinition.TaskDefinitionArn"
261
+ },
262
+ "Next": "WorkflowFailed"
263
+ },
264
+ }
265
+
266
+ def _build_workflow(
267
+ self,
268
+ pipeline,
269
+ image_refs,
270
+ workflow_name,
271
+ timestamp,
272
+ run_id,
273
+ ):
274
+ run_steps = ','.join(image_refs.keys())
275
+ groups = self._group_steps(pipeline, image_refs)
276
+ states = {}
277
+ for task_index, group in enumerate(groups):
278
+ steps = [pipeline.steps[i] for i in group]
279
+ for step in steps:
280
+ if step.num_workers is not None and step.num_workers != 1:
281
+ raise CloudSubmitError(
282
+ f'Step {step.name} requests multiple workers '
283
+ 'but multi-worker steps are not supported by the '
284
+ 'environment.'
285
+ )
286
+ step_names = [step.name for step in steps]
287
+ image_ref = image_refs[step_names[0]]
288
+ spec = pipeline.steps[group[0]].spec
289
+ states.update(
290
+ self._build_task(
291
+ workflow_name=workflow_name,
292
+ task_index=task_index,
293
+ image_ref=image_ref,
294
+ spec=spec,
295
+ pipeline_name=pipeline.name,
296
+ step_names=step_names,
297
+ timestamp=timestamp,
298
+ run_id=run_id,
299
+ run_steps=run_steps,
300
+ is_last=(task_index == len(groups) - 1),
301
+ )
302
+ )
303
+ states["WorkflowFailed"] = {
304
+ "Type": "Fail",
305
+ "Cause": "The ECS container task failed execution.",
306
+ "Error": "TaskExecutionError"
307
+ }
308
+
309
+ return {
310
+ "StartAt": "RegisterTask0",
311
+ "States": states,
312
+ }
313
+
314
+ def run_pipeline(
315
+ self,
316
+ pipeline,
317
+ image_refs,
318
+ overwrite_artifacts,
319
+ timestamp,
320
+ run_id,
321
+ temp_path
322
+ ):
323
+ workflow_name = self._make_workflow_name(run_id)
324
+ workflow = self._build_workflow(
325
+ pipeline=pipeline,
326
+ image_refs=image_refs,
327
+ workflow_name=workflow_name,
328
+ timestamp=timestamp,
329
+ run_id=run_id,
330
+ )
331
+
332
+ workflow_path = os.path.join(temp_path, f'{workflow_name}.json')
333
+ with open(workflow_path, 'w') as stream:
334
+ json.dump(workflow, stream)
335
+
336
+ if self._check_workflow_exists(workflow_name):
337
+ workflow_arn = self._make_workflow_arn(workflow_name)
338
+ run_command(
339
+ [
340
+ self._aws_command,
341
+ 'stepfunctions',
342
+ 'update-state-machine',
343
+ '--profile', self._aws_profile,
344
+ '--region', self._aws_region,
345
+ '--state-machine-arn', workflow_arn,
346
+ '--role-arn', self._stepfunctions_role_arn,
347
+ '--definition', f'file://{workflow_path}',
348
+ ],
349
+ stdout=subprocess.DEVNULL,
350
+ )
351
+ else:
352
+ run_command(
353
+ [
354
+ self._aws_command,
355
+ 'stepfunctions',
356
+ 'create-state-machine',
357
+ '--profile', self._aws_profile,
358
+ '--region', self._aws_region,
359
+ '--name', workflow_name,
360
+ '--role-arn', self._stepfunctions_role_arn,
361
+ '--definition', f'file://{workflow_path}',
362
+ ],
363
+ stdout=subprocess.DEVNULL,
364
+ )
365
+
366
+ print('Clearing output and temporary artifacts.')
367
+ self.remove_remote_artifacts(
368
+ overwrite_artifacts, [[run_id]]*len(overwrite_artifacts))
369
+
370
+ result = run_command(
371
+ [
372
+ self._aws_command,
373
+ 'stepfunctions',
374
+ 'start-execution',
375
+ '--profile', self._aws_profile,
376
+ '--region', self._aws_region,
377
+ '--state-machine-arn', workflow_arn,
378
+ ],
379
+ stdout=subprocess.PIPE,
380
+ text=True,
381
+ )
382
+ result = json.loads(result.stdout)
383
+ print(f'Workflow execution: {result["executionArn"]}')
384
+ print(f'Started at {result["startDate"]}')
385
+
386
+ def print_logs(self, run_id, start_timestamp, stream=False):
387
+ print('\nStreaming logs...')
388
+ workflow_name = self._make_workflow_name(run_id)
389
+ command = [
390
+ self._aws_command,
391
+ 'logs',
392
+ 'tail',
393
+ '--profile', self._aws_profile,
394
+ '--region', self._aws_region,
395
+ '--since', start_timestamp.isoformat(),
396
+ '--format', 'short',
397
+ ]
398
+ if stream:
399
+ command.append('--follow')
400
+ command.append(self._log_group_prefix + workflow_name)
401
+ run_command(command)
@@ -0,0 +1,87 @@
1
+ import os
2
+ import json
3
+ import subprocess
4
+
5
+ from .base_execution_handler import BaseExecutionHandler
6
+ from .utils import CloudSubmitError, read_json, run_command
7
+ from .s3_tools import get_remote_artifact_path
8
+
9
+
10
+ class ExecutionHandler(BaseExecutionHandler):
11
+ def __init__(self):
12
+ super().__init__()
13
+ self._run_id = self.get_run_id()
14
+ config = read_json('src/csub/execution_config.json')
15
+ self._project = config['project']
16
+ self._user = config['user']
17
+ self._container_aws_command = config['container_aws_command']
18
+ self._s3_bucket = config['s3_bucket']
19
+ self._s3_prefix = config['s3_prefix']
20
+
21
+ def get_remote_artifact_path(self, artifact):
22
+ return get_remote_artifact_path(
23
+ artifact,
24
+ self._project,
25
+ self._user,
26
+ self._run_id,
27
+ self._s3_bucket,
28
+ self._s3_prefix,
29
+ )
30
+
31
+ def download_artifact(self, artifact):
32
+ if artifact.kind != 'file':
33
+ raise CloudSubmitError(
34
+ f'Artifact {artifact.name} is of kind {repr(artifact.kind)} '
35
+ "but only artifacts of kind 'file' are supported by this "
36
+ 'environment.'
37
+ )
38
+ remote_path = self.get_remote_artifact_path(artifact)
39
+ remote_path = '/'.join(remote_path.split('/')[:-1])
40
+ local_path = self.get_local_artifact_path(artifact)
41
+ local_path = os.path.dirname(local_path)
42
+
43
+ print(f'Downloading artifact {artifact.name}.')
44
+ command = [
45
+ self._container_aws_command,
46
+ 's3',
47
+ 'cp',
48
+ '--recursive',
49
+ '--no-progress',
50
+ remote_path,
51
+ local_path,
52
+ '--exclude', '*',
53
+ '--include', artifact.name,
54
+ '--include', '/'.join([artifact.name, '*'])
55
+ ]
56
+ run_command(command)
57
+
58
+ def upload_artifact(self, artifact):
59
+ if artifact.kind != 'file':
60
+ raise CloudSubmitError(
61
+ f'Artifact {artifact.name} is of kind {repr(artifact.kind)} '
62
+ "but only artifacts of kind 'file' are supported by this "
63
+ 'environment.'
64
+ )
65
+ remote_path = self.get_remote_artifact_path(artifact)
66
+ remote_path = '/'.join(remote_path.split('/')[:-1])
67
+ local_path = self.get_local_artifact_path(artifact)
68
+ local_path = os.path.dirname(local_path)
69
+
70
+ print(f'Uploading artifact {artifact.name}.')
71
+ command = [
72
+ self._container_aws_command,
73
+ 's3',
74
+ 'cp',
75
+ '--recursive',
76
+ '--no-progress',
77
+ local_path,
78
+ remote_path,
79
+ '--exclude', '*',
80
+ '--include', artifact.name,
81
+ '--include', '/'.join([artifact.name, '*'])
82
+ ]
83
+ run_command(command)
84
+
85
+
86
+ def create_execution_handler():
87
+ return ExecutionHandler()
@@ -0,0 +1,46 @@
1
+ import os
2
+
3
+ try:
4
+ from .utils import CloudSubmitError
5
+ except ImportError:
6
+ from cloud_submit import CloudSubmitError
7
+
8
+
9
+ def get_remote_artifact_path(
10
+ artifact,
11
+ project,
12
+ user,
13
+ run_id,
14
+ s3_bucket,
15
+ s3_prefix,
16
+ ):
17
+ if artifact.kind != 'file':
18
+ raise CloudSubmitError(
19
+ f'Cannot get remote path for artifact {artifact.name}. '
20
+ "Only artifacts of kind 'file' are supported and this one "
21
+ f'is of kind {repr(artifact.kind)}.'
22
+ )
23
+ if artifact.scope == 'project':
24
+ return '/'.join([
25
+ 's3:/', s3_bucket, s3_prefix, project,
26
+ 'shared', artifact.name
27
+ ])
28
+ elif artifact.scope == 'user':
29
+ return '/'.join([
30
+ 's3:/', s3_bucket, s3_prefix, project,
31
+ 'users', user, 'shared', artifact.name
32
+ ])
33
+ elif artifact.scope == 'run':
34
+ if run_id is None:
35
+ raise ValueError(
36
+ 'You must specify `run_id` to get the path '
37
+ "for an artifact with scope 'run'"
38
+ )
39
+ return '/'.join([
40
+ 's3:/', s3_bucket, s3_prefix, project,
41
+ 'users', user, 'runs', run_id, artifact.name
42
+ ])
43
+ else:
44
+ raise CloudSubmitError(
45
+ f'Unknown scope {artifact.scope} for artifact {artifact.name}.')
46
+
@@ -0,0 +1,121 @@
1
+ import os
2
+ import sys
3
+ import shutil
4
+ import subprocess
5
+
6
+ # We use relative imports here to avoid circular imports because this module is
7
+ # used by some of the core cloud-submit modules. Normally you can just do
8
+ #
9
+ # from cloud_submit import ...
10
+ from ...environment_handler import EnvironmentHandler
11
+ from ...utils import build_docker_mount_option
12
+ from ...execution.utils import (
13
+ ensure_path,
14
+ CloudSubmitError,
15
+ run_command,
16
+ )
17
+
18
+
19
+ def build_artifacts_mount_option(path, scope):
20
+ return build_docker_mount_option(path, f'/mnt/artifacts/{scope}')
21
+
22
+
23
+ class LocalEnv(EnvironmentHandler):
24
+ def install_execution_handler(self, path):
25
+ sourcedir = os.path.dirname(__file__)
26
+ shutil.copyfile(
27
+ os.path.join(sourcedir, 'execution_handler.py'),
28
+ os.path.join(path, 'execution_handler.py'),
29
+ )
30
+
31
+ def pull_base_image(self, ref):
32
+ result = run_command(
33
+ [
34
+ self._docker_command,
35
+ 'images',
36
+ ref,
37
+ '-q',
38
+ ],
39
+ stdout=subprocess.PIPE,
40
+ text=True,
41
+ )
42
+ if not result.stdout.strip():
43
+ raise CloudSubmitError(
44
+ f'Could not find image {ref}. You may have to build it again.'
45
+ )
46
+
47
+ def list_remote_image_tags(self, repo_name):
48
+ return []
49
+
50
+ def remove_remote_image_refs(self, refs):
51
+ pass
52
+
53
+ def get_remote_artifact_path(self, artifact, run_id=None):
54
+ return ''
55
+
56
+ def list_remote_artifacts(self, artifacts, run_ids=None):
57
+ return [[] for a in artifacts]
58
+
59
+ def remove_remote_artifacts(self, artifacts, run_ids):
60
+ pass
61
+
62
+ def copy_remote_artifacts(self, artifacts, from_run_id, to_run_id):
63
+ pass
64
+
65
+ def move_remote_artifacts(self, artifacts, from_run_id, to_run_id):
66
+ pass
67
+
68
+ def run_pipeline(
69
+ self,
70
+ pipeline,
71
+ image_refs,
72
+ overwrite_artifacts,
73
+ timestamp,
74
+ run_id,
75
+ temp_path,
76
+ ):
77
+ artifacts_project_path = os.path.join('artifacts', 'shared')
78
+ artifacts_user_path = os.path.join(
79
+ 'artifacts', 'users', self._user, 'shared')
80
+ artifacts_run_path = os.path.join(
81
+ 'artifacts', 'users', self._user, 'runs', run_id)
82
+ ensure_path(artifacts_project_path)
83
+ ensure_path(artifacts_user_path)
84
+ ensure_path(artifacts_run_path)
85
+
86
+ self.remove_local_artifacts(
87
+ overwrite_artifacts, [[run_id]]*len(overwrite_artifacts))
88
+
89
+ for step in pipeline.steps:
90
+ if step.name not in image_refs:
91
+ continue
92
+ ref = image_refs[step.name]
93
+
94
+ worker_indices = [-1]
95
+ if step.num_workers is not None and step.num_workers > 1:
96
+ worker_indices = range(step.num_workers)
97
+
98
+ for worker_index in worker_indices:
99
+ run_command([
100
+ self._docker_command,
101
+ 'run',
102
+ '--rm',
103
+ '--mount',
104
+ build_artifacts_mount_option(
105
+ artifacts_project_path, 'project'),
106
+ '--mount',
107
+ build_artifacts_mount_option(artifacts_user_path, 'user'),
108
+ '--mount',
109
+ build_artifacts_mount_option(artifacts_run_path, 'run'),
110
+ '--env', f'CSUB_TIMESTAMP={timestamp.isoformat()}',
111
+ '--env', f'CSUB_RUN_ID={run_id}',
112
+ '--env', f'CSUB_WORKER_INDEX={worker_index}',
113
+ '--env', f'CSUB_RUN_STEPS={",".join(image_refs.keys())}',
114
+ ref,
115
+ pipeline.name,
116
+ step.name,
117
+ ])
118
+
119
+ def print_logs(self, run_id, start_timestamp, stream=False):
120
+ raise CloudSubmitError(
121
+ 'Log retrieval is not supported by this environment.')
@@ -0,0 +1,5 @@
1
+ from .base_execution_handler import BaseExecutionHandler
2
+
3
+
4
+ def create_execution_handler():
5
+ return BaseExecutionHandler()