proximl 0.5.17__py3-none-any.whl → 1.0.0__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 (55) hide show
  1. examples/local_storage.py +0 -2
  2. proximl/__init__.py +1 -1
  3. proximl/checkpoints.py +56 -57
  4. proximl/cli/__init__.py +6 -3
  5. proximl/cli/checkpoint.py +18 -57
  6. proximl/cli/dataset.py +17 -57
  7. proximl/cli/job/__init__.py +11 -53
  8. proximl/cli/job/create.py +51 -24
  9. proximl/cli/model.py +14 -56
  10. proximl/cli/volume.py +18 -57
  11. proximl/datasets.py +50 -55
  12. proximl/jobs.py +239 -68
  13. proximl/models.py +51 -55
  14. proximl/proximl.py +50 -16
  15. proximl/utils/__init__.py +1 -0
  16. proximl/{auth.py → utils/auth.py} +4 -3
  17. proximl/utils/transfer.py +587 -0
  18. proximl/volumes.py +48 -53
  19. {proximl-0.5.17.dist-info → proximl-1.0.0.dist-info}/METADATA +3 -3
  20. {proximl-0.5.17.dist-info → proximl-1.0.0.dist-info}/RECORD +52 -50
  21. tests/integration/test_checkpoints_integration.py +4 -3
  22. tests/integration/test_datasets_integration.py +5 -3
  23. tests/integration/test_jobs_integration.py +33 -27
  24. tests/integration/test_models_integration.py +7 -3
  25. tests/integration/test_volumes_integration.py +2 -2
  26. tests/unit/cli/test_cli_checkpoint_unit.py +312 -1
  27. tests/unit/cloudbender/test_nodes_unit.py +112 -0
  28. tests/unit/cloudbender/test_providers_unit.py +96 -0
  29. tests/unit/cloudbender/test_regions_unit.py +106 -0
  30. tests/unit/cloudbender/test_services_unit.py +141 -0
  31. tests/unit/conftest.py +23 -10
  32. tests/unit/projects/test_project_data_connectors_unit.py +39 -0
  33. tests/unit/projects/test_project_datastores_unit.py +37 -0
  34. tests/unit/projects/test_project_members_unit.py +46 -0
  35. tests/unit/projects/test_project_services_unit.py +65 -0
  36. tests/unit/projects/test_projects_unit.py +16 -0
  37. tests/unit/test_auth_unit.py +17 -2
  38. tests/unit/test_checkpoints_unit.py +256 -71
  39. tests/unit/test_datasets_unit.py +218 -68
  40. tests/unit/test_exceptions.py +133 -0
  41. tests/unit/test_gpu_types_unit.py +11 -1
  42. tests/unit/test_jobs_unit.py +1014 -95
  43. tests/unit/test_main_unit.py +20 -0
  44. tests/unit/test_models_unit.py +218 -70
  45. tests/unit/test_proximl_unit.py +627 -3
  46. tests/unit/test_volumes_unit.py +211 -70
  47. tests/unit/utils/__init__.py +1 -0
  48. tests/unit/utils/test_transfer_unit.py +4260 -0
  49. proximl/cli/connection.py +0 -61
  50. proximl/connections.py +0 -621
  51. tests/unit/test_connections_unit.py +0 -182
  52. {proximl-0.5.17.dist-info → proximl-1.0.0.dist-info}/LICENSE +0 -0
  53. {proximl-0.5.17.dist-info → proximl-1.0.0.dist-info}/WHEEL +0 -0
  54. {proximl-0.5.17.dist-info → proximl-1.0.0.dist-info}/entry_points.txt +0 -0
  55. {proximl-0.5.17.dist-info → proximl-1.0.0.dist-info}/top_level.txt +0 -0
proximl/cli/connection.py DELETED
@@ -1,61 +0,0 @@
1
- import click
2
- from proximl.cli import cli, pass_config, search_by_id_name
3
-
4
-
5
- @cli.group()
6
- @pass_config
7
- def connection(config):
8
- """proxiML connection commands."""
9
- pass
10
-
11
-
12
- @connection.command()
13
- @pass_config
14
- def list(config):
15
- """List connections."""
16
- data = [["ID", "TYPE", "STATUS"], ["-" * 80, "-" * 80, "-" * 80]]
17
-
18
- connections = config.proximl.run(config.proximl.client.connections.list())
19
-
20
- for con in connections:
21
- data.append([con.id, con.type, con.status])
22
- for row in data:
23
- click.echo(
24
- "{: >38.36} {: >9.7} {: >15.13}".format(*row), file=config.stdout
25
- )
26
-
27
-
28
- @connection.command()
29
- @click.argument("id", type=click.STRING)
30
- @pass_config
31
- def remove(config, id):
32
- """Remove connection."""
33
- connections = config.proximl.run(config.proximl.client.connections.list())
34
-
35
- found = search_by_id_name(id, connections)
36
- if None is found:
37
- raise click.UsageError("Connection ID specified does not exist.")
38
-
39
- if found.type == "dataset":
40
- this = config.proximl.run(config.proximl.client.datasets.get(id))
41
- elif found.type == "job":
42
- this = config.proximl.run(config.proximl.client.jobs.get(id))
43
- else:
44
- raise click.UsageError("Unknown connection type.")
45
-
46
- return config.proximl.run(this.disconnect())
47
-
48
-
49
- @connection.command()
50
- @click.option(
51
- "--all-projects/--no-all-projects",
52
- default=False,
53
- show_default=True,
54
- help="Auto attach to dataset and show creation logs.",
55
- )
56
- @pass_config
57
- def remove_all(config, all_projects):
58
- """Clear and clean-up all proxiML connections."""
59
- return config.proximl.run(
60
- config.proximl.client.connections.remove_all(all_projects=all_projects)
61
- )
proximl/connections.py DELETED
@@ -1,621 +0,0 @@
1
- import json
2
- import os
3
- import shutil
4
- import asyncio
5
- import aiohttp
6
- import aiodocker
7
- import zipfile
8
- import re
9
- import logging
10
-
11
- from .exceptions import (
12
- ConnectionError,
13
- ApiError,
14
- SpecificationError,
15
- ProxiMLException,
16
- )
17
- from aiodocker.exceptions import DockerError
18
-
19
-
20
- VPN_IMAGE = "proximl/connection:no-upnp"
21
- STORAGE_IMAGE = "proximl/local-storage"
22
- STATUSES = dict(
23
- UNKNOWN="unknown",
24
- NEW="new",
25
- CONNECTING="connecting",
26
- CONNECTED="connected",
27
- NOT_CONNECTED="not connected",
28
- STOPPED="stopped",
29
- REMOVED="removed",
30
- )
31
- CONFIG_DIR = os.path.expanduser(
32
- os.environ.get("PROXIML_CONFIG_DIR") or "~/.proximl"
33
- )
34
-
35
-
36
- class Connections(object):
37
- def __init__(self, proximl):
38
- self.proximl = proximl
39
- self.dir = f"{CONFIG_DIR}/connections/{self.proximl.project}"
40
- os.makedirs(
41
- self.dir,
42
- exist_ok=True,
43
- )
44
-
45
- async def list(self):
46
- con_dirs = os.listdir(self.dir)
47
- connections = []
48
- con_tasks = []
49
- for con_dir in con_dirs:
50
- try:
51
- con_type, con_id = con_dir.split("_")
52
- except ValueError:
53
- # unintelligible directory
54
- continue
55
- connection = Connection(self.proximl, con_type, con_id)
56
- connections.append(connection)
57
- con_task = asyncio.create_task(connection.check())
58
- con_tasks.append(con_task)
59
- await asyncio.gather(*con_tasks)
60
- return connections
61
-
62
- async def cleanup_connections(self):
63
- logging.info("Start cleanup connections")
64
- con_dirs = os.listdir(self.dir)
65
- con_tasks = []
66
- logging.debug(f"con_dirs: {con_dirs}")
67
- for con_dir in con_dirs:
68
- try:
69
- con_type, con_id = con_dir.split("_")
70
- except ValueError:
71
- # unintelligible directory
72
- logging.debug(f"unintelligible con_dir: {con_dir}")
73
- shutil.rmtree(con_dir)
74
- continue
75
- connection = Connection(self.proximl, con_type, con_id)
76
- con_task = asyncio.create_task(connection._validate_entity())
77
- con_tasks.append(con_task)
78
- await asyncio.gather(*con_tasks, return_exceptions=True)
79
- await self.cleanup_containers(project=self.proximl.project)
80
- logging.info("Finish cleanup connections")
81
-
82
- async def cleanup_containers(self, project=None):
83
- logging.info("Start cleanup containers")
84
- if project:
85
- con_dirs = os.listdir(f"{CONFIG_DIR}/connections/{project}")
86
- await asyncio.gather(
87
- asyncio.create_task(
88
- _cleanup_containers(
89
- project,
90
- self.dir,
91
- con_dirs,
92
- "vpn",
93
- )
94
- ),
95
- asyncio.create_task(
96
- _cleanup_containers(
97
- project,
98
- self.dir,
99
- con_dirs,
100
- "storage",
101
- )
102
- ),
103
- )
104
- else:
105
- con_dirs = []
106
- proj_dirs = os.listdir(f"{CONFIG_DIR}/connections")
107
- logging.info(f"proj_dirs {proj_dirs}")
108
- for proj_dir in proj_dirs:
109
- con_dirs += [
110
- f"{proj_dir}/{con_dir}"
111
- for con_dir in os.listdir(
112
- f"{CONFIG_DIR}/connections/{project}"
113
- )
114
- ]
115
-
116
- logging.info(f"con_dirs {con_dirs}")
117
- await asyncio.gather(
118
- asyncio.create_task(
119
- _cleanup_containers(
120
- None,
121
- f"{CONFIG_DIR}/connections",
122
- con_dirs,
123
- "vpn",
124
- )
125
- ),
126
- asyncio.create_task(
127
- _cleanup_containers(
128
- None,
129
- f"{CONFIG_DIR}/connections",
130
- con_dirs,
131
- "storage",
132
- )
133
- ),
134
- )
135
-
136
- logging.info("Finish cleanup containers")
137
-
138
- async def remove_all(self, all_projects=False):
139
- if all_projects:
140
- proj_dirs = os.listdir(f"{CONFIG_DIR}/connections")
141
- for proj_dir in proj_dirs:
142
- shutil.rmtree(f"{CONFIG_DIR}/connections/{proj_dir}")
143
- os.makedirs(
144
- f"{CONFIG_DIR}/connections/{proj_dir}",
145
- exist_ok=True,
146
- )
147
- await self.cleanup_containers()
148
- else:
149
- shutil.rmtree(self.dir)
150
- os.makedirs(
151
- self.dir,
152
- exist_ok=True,
153
- )
154
- await self.cleanup_containers(project=self.proximl.project)
155
-
156
-
157
- class Connection:
158
- def __init__(self, proximl, entity_type, id, entity=None, **kwargs):
159
- self.proximl = proximl
160
- self._id = id
161
- self._type = entity_type
162
- self._status = STATUSES.get("UNKNOWN")
163
- self._entity = entity
164
- CONNECTIONS_DIR = f"{CONFIG_DIR}/connections/{self.proximl.project}"
165
- self._dir = f"{CONNECTIONS_DIR}/{entity_type}_{id}"
166
- os.makedirs(
167
- self._dir,
168
- exist_ok=True,
169
- )
170
-
171
- @property
172
- def id(self) -> str:
173
- return self._id
174
-
175
- @property
176
- def type(self) -> str:
177
- return self._type
178
-
179
- @property
180
- def status(self) -> str:
181
- return self._status
182
-
183
- def __str__(self):
184
- return f"Connection for {self.type} - {self.id}: {self.status}"
185
-
186
- def __repr__(self):
187
- return f"Connection( proximl , {self.id}, {self.type})"
188
-
189
- async def _get_entity(self):
190
- if self.type == "dataset":
191
- self._entity = await self.proximl.datasets.get(self.id)
192
- elif self.type == "job":
193
- self._entity = await self.proximl.jobs.get(self.id)
194
- elif self.type == "model":
195
- self._entity = await self.proximl.models.get(self.id)
196
- elif self.type == "checkpoint":
197
- self._entity = await self.proximl.checkpoints.get(self.id)
198
- else:
199
- raise TypeError(
200
- "Connection type must be in: ['dataset', 'model', 'checkpoint', 'job']"
201
- )
202
-
203
- async def _download_connection_details(self):
204
- zip_file = f"{self._dir}/details.zip"
205
- url = await self._entity.get_connection_utility_url()
206
- async with aiohttp.ClientSession() as session:
207
- async with session.request("GET", url) as resp:
208
- with open(
209
- zip_file,
210
- "wb",
211
- ) as fd:
212
- content = await resp.read()
213
- fd.write(content)
214
- with zipfile.ZipFile(zip_file, "r") as zipf:
215
- for info in zipf.infolist():
216
- extracted_path = zipf.extract(info, self._dir)
217
- if info.create_system == 3 and os.path.isfile(
218
- extracted_path
219
- ): ## 3 - ZIP_UNIX_SYSTEM
220
- unix_attributes = info.external_attr >> 16
221
- if unix_attributes:
222
- os.chmod(extracted_path, unix_attributes)
223
-
224
- os.remove(zip_file)
225
-
226
- async def _test_connection(self, container):
227
- entity_details = self._entity.get_connection_details()
228
- if not entity_details:
229
- return False
230
- net = _parse_cidr(entity_details.get("cidr"))
231
- target_ip = f"{net.get('first_octet')}.{net.get('second_octet')}.{net.get('third_octet')}.254"
232
-
233
- logging.debug("Testing connection")
234
- ping = await container.exec(
235
- ["ping", "-c", "1", target_ip],
236
- stdout=True,
237
- stderr=True,
238
- )
239
- stream = ping.start()
240
- await stream.read_out()
241
- data = await ping.inspect()
242
- while data["ExitCode"] is None:
243
- await stream.read_out()
244
- data = await ping.inspect()
245
- await stream.close()
246
- if data["ExitCode"] == 0:
247
- return True
248
- return False
249
-
250
- async def _validate_entity(self):
251
- try:
252
- await self._get_entity()
253
- logging.debug(f"entity: {self._entity}")
254
- if self._entity.status in [
255
- "failed",
256
- "finished",
257
- "canceled",
258
- "archived",
259
- "removed",
260
- "removing",
261
- "ready",
262
- ]:
263
- shutil.rmtree(self._dir)
264
- logging.debug(f"remove: {self._dir}")
265
- return False
266
- else:
267
- return True
268
- except ApiError as e:
269
- if e.status == 404:
270
- shutil.rmtree(self._dir)
271
- logging.debug(f"remove: {self._dir}")
272
- return False
273
- else:
274
- raise e
275
-
276
- async def check(self):
277
- if not self._entity:
278
- valid = await self._validate_entity()
279
- if not valid:
280
- self._status = STATUSES.get("REMOVED")
281
- return
282
- if not os.path.isdir(f"{self._dir}/data"):
283
- self._status = STATUSES.get("NEW")
284
- return
285
-
286
- try:
287
- with open(f"{self._dir}/vpn_id", "r") as f:
288
- vpn_id = f.read()
289
- except OSError as e:
290
- self._status = STATUSES.get("STOPPED")
291
- return
292
-
293
- docker = aiodocker.Docker()
294
- await _check_docker_status(docker)
295
-
296
- try:
297
- container = await docker.containers.get(vpn_id)
298
- except DockerError as e:
299
- if e.status == 404:
300
- self._status = STATUSES.get("STOPPED")
301
- await docker.close()
302
- return
303
- raise e
304
-
305
- data = await container.show()
306
- if not data["State"]["Running"]:
307
- self._status = STATUSES.get("STOPPED")
308
- await container.delete()
309
- os.remove(f"{self._dir}/vpn_id")
310
- try:
311
- with open(f"{self._dir}/storage_id", "r") as f:
312
- storage_id = f.read()
313
- try:
314
- storage_container = await docker.containers.get(storage_id)
315
- await storage_container.delete(force=True)
316
- except DockerError as e:
317
- if e.status != 404:
318
- raise e
319
- except OSError as e:
320
- pass
321
- await docker.close()
322
- return
323
-
324
- connected = await self._test_connection(container)
325
- await docker.close()
326
- if connected:
327
- self._status = STATUSES.get("CONNECTED")
328
- else:
329
- self._status = STATUSES.get("NOT_CONNECTED")
330
-
331
- async def start(self):
332
- logging.info(f"Beginning start {self.type} connection {self.id}")
333
-
334
- if self.status == STATUSES.get("UNKNOWN"):
335
- await self.check()
336
- if self.status in [
337
- STATUSES.get("CONNECTING"),
338
- STATUSES.get("CONNECTED"),
339
- STATUSES.get("NOT_CONNECTED"),
340
- ]:
341
- raise SpecificationError(
342
- "status", "Only inactive connections can be started."
343
- )
344
- self._status = STATUSES.get("CONNECTING")
345
- logging.info(f"Connecting...")
346
- if not self._entity:
347
- await self._get_entity()
348
- if not os.path.isdir(f"{self._dir}/data"):
349
- await self._download_connection_details()
350
-
351
- docker = aiodocker.Docker()
352
- await _check_docker_status(docker)
353
- try:
354
- await asyncio.gather(
355
- docker.pull(VPN_IMAGE), docker.pull(STORAGE_IMAGE)
356
- )
357
- except DockerError as e:
358
- exists = await asyncio.gather(
359
- _image_exists(docker, VPN_IMAGE),
360
- _image_exists(docker, STORAGE_IMAGE),
361
- )
362
- if any([not i for i in exists]):
363
- raise e
364
-
365
- entity_details = self._entity.get_connection_details()
366
- if (
367
- entity_details.get("model_path")
368
- or entity_details.get("input_path")
369
- or entity_details.get("output_path")
370
- ):
371
- logging.debug(f"Starting storage container")
372
- storage_container = await docker.containers.run(
373
- _get_storage_container_config(
374
- self.id,
375
- entity_details.get("project_uuid"),
376
- entity_details.get("entity_type"),
377
- entity_details.get("cidr"),
378
- f"{self._dir}/data",
379
- entity_details.get("ssh_port"),
380
- model_path=entity_details.get("model_path"),
381
- input_path=entity_details.get("input_path"),
382
- output_path=entity_details.get("output_path"),
383
- )
384
- )
385
- logging.debug(
386
- f"Storage container started, id: {storage_container.id}"
387
- )
388
- with open(f"{self._dir}/storage_id", "w") as f:
389
- f.write(storage_container.id)
390
-
391
- logging.debug(f"Starting VPN container")
392
- vpn_container = await docker.containers.run(
393
- _get_vpn_container_config(
394
- self.id,
395
- entity_details.get("project_uuid"),
396
- entity_details.get("entity_type"),
397
- entity_details.get("cidr"),
398
- f"{self._dir}/data",
399
- )
400
- )
401
- logging.debug(f"VPN container started, id: {vpn_container.id}")
402
-
403
- with open(f"{self._dir}/vpn_id", "w") as f:
404
- f.write(vpn_container.id)
405
-
406
- # cleanup_task = asyncio.create_task(
407
- # self.proximl.connections.cleanup_connections()
408
- # )
409
-
410
- count = 0
411
- while count <= 30:
412
- logging.debug(f"Test connectivity attempt {count+1}")
413
- res = await self._test_connection(vpn_container)
414
- if res:
415
- logging.debug(f"Test connectivity successful {count+1}")
416
- break
417
- count += 1
418
- await docker.close()
419
- if count > 30:
420
- self._status = STATUSES.get("NOT_CONNECTED")
421
- raise ConnectionError(f"Unable to connect {self.type} {self.id}")
422
- self._status = STATUSES.get("CONNECTED")
423
- logging.info(f"Connection Successful.")
424
- # await cleanup_task
425
- logging.debug(f"Completed start {self.type} connection {self.id}")
426
-
427
- async def stop(self):
428
- logging.debug(f"Beginning stop {self.type} connection {self.id}")
429
- if not self._entity:
430
- await self._get_entity()
431
- docker = aiodocker.Docker()
432
- await _check_docker_status(docker)
433
-
434
- tasks = []
435
- logging.info("Disconnecting...")
436
- try:
437
- with open(f"{self._dir}/vpn_id", "r") as f:
438
- vpn_id = f.read()
439
- logging.debug(f"vpn container id: {vpn_id}")
440
- vpn_container = await docker.containers.get(vpn_id)
441
- vpn_delete_task = asyncio.create_task(
442
- vpn_container.delete(force=True)
443
- )
444
- tasks.append(vpn_delete_task)
445
- os.remove(f"{self._dir}/vpn_id")
446
- except OSError:
447
- logging.debug("vpn container not found")
448
-
449
- storage_delete_task = None
450
- try:
451
- with open(f"{self._dir}/storage_id", "r") as f:
452
- storage_id = f.read()
453
- logging.debug(f"storage container id: {vpn_id}")
454
- storage_container = await docker.containers.get(storage_id)
455
- storage_delete_task = asyncio.create_task(
456
- storage_container.delete(force=True)
457
- )
458
- tasks.append(storage_delete_task)
459
- os.remove(f"{self._dir}/storage_id")
460
- except OSError:
461
- logging.debug("storage container not found")
462
-
463
- await asyncio.gather(*tasks)
464
- await docker.close()
465
- self._status = STATUSES.get("REMOVED")
466
- shutil.rmtree(self._dir)
467
- logging.info("Disconnected.")
468
- logging.debug(f"Completed stop {self.type} connection {self.id}")
469
-
470
-
471
- async def _cleanup_containers(project, path, con_dirs, type):
472
- containers_target = []
473
- for con_dir in con_dirs:
474
- try:
475
- with open(f"{path}/{con_dir}/{type}_id", "r") as f:
476
- id = f.read()
477
- containers_target.append(id)
478
- except OSError:
479
- continue
480
-
481
- docker = aiodocker.Docker()
482
- await _check_docker_status(docker)
483
-
484
- if project:
485
- filter = dict(
486
- label=[
487
- "service=proximl",
488
- f"type={type}",
489
- f"project={project}",
490
- ]
491
- )
492
- else:
493
- filter = dict(
494
- label=[
495
- "service=proximl",
496
- f"type={type}",
497
- ]
498
- )
499
-
500
- containers = await docker.containers.list(
501
- all=True,
502
- filters=json.dumps(filter),
503
- )
504
-
505
- tasks = [
506
- asyncio.create_task(container.delete(force=True))
507
- for container in containers
508
- if container.id not in containers_target
509
- ]
510
-
511
- await asyncio.gather(*tasks, return_exceptions=True)
512
- await docker.close()
513
-
514
-
515
- def _parse_cidr(cidr):
516
- res = re.match(
517
- r"(?P<first_octet>[0-9]{1,3})\.(?P<second_octet>[0-9]{1,3})\.(?P<third_octet>[0-9]{1,3})\.(?P<fourth_octet>[0-9]{1,3})/(?P<mask_length>[0-9]{1,2})",
518
- cidr,
519
- )
520
- net = res.groupdict()
521
- return net
522
-
523
-
524
- def _get_vpn_container_config(id, project_uuid, entity_type, cidr, data_dir):
525
- config = dict(
526
- Image=VPN_IMAGE,
527
- Hostname=id,
528
- Cmd=[],
529
- AttachStdin=False,
530
- AttachStdout=False,
531
- AttachStderr=False,
532
- Tty=False,
533
- Env=[
534
- f"NETWORK={id}",
535
- "DEBUG=1",
536
- ],
537
- HostConfig=dict(
538
- Init=True,
539
- Binds=[f"{data_dir}:/etc/tinc:rw"],
540
- NetworkMode="host",
541
- CapAdd=["NET_ADMIN"],
542
- ),
543
- Labels=dict(
544
- type="vpn",
545
- service="proximl",
546
- id=id,
547
- project=project_uuid,
548
- entity_type=entity_type,
549
- ),
550
- )
551
- return config
552
-
553
-
554
- def _get_storage_container_config(
555
- id,
556
- project_uuid,
557
- entity_type,
558
- cidr,
559
- data_dir,
560
- ssh_port,
561
- model_path=None,
562
- input_path=None,
563
- output_path=None,
564
- ):
565
- Binds = [f"{data_dir}/.ssh:/opt/ssh"]
566
- if model_path:
567
- Binds.append(f"{os.path.expanduser(model_path)}:/opt/model:ro")
568
- if input_path:
569
- Binds.append(f"{os.path.expanduser(input_path)}:/opt/data:ro")
570
- if output_path:
571
- Binds.append(f"{os.path.expanduser(output_path)}:/opt/output:rw")
572
- config = dict(
573
- Image=STORAGE_IMAGE,
574
- Hostname=id,
575
- Cmd=[],
576
- AttachStdin=False,
577
- AttachStdout=False,
578
- AttachStderr=False,
579
- Tty=False,
580
- Env=[
581
- f"VPN_CIDR={cidr}",
582
- ],
583
- ExposedPorts={f"22/tcp": {}},
584
- HostConfig=dict(
585
- Init=True,
586
- Binds=Binds,
587
- PortBindings={
588
- f"22/tcp": [dict(HostPort=f"{ssh_port}", HostIP="0.0.0.0")],
589
- },
590
- ),
591
- Labels=dict(
592
- type="storage",
593
- service="proximl",
594
- id=id,
595
- project=project_uuid,
596
- entity_type=entity_type,
597
- ),
598
- )
599
- return config
600
-
601
-
602
- async def _image_exists(client, id):
603
- if not id:
604
- return False
605
- try:
606
- await client.images.inspect(id)
607
- return True
608
- except DockerError:
609
- return False
610
-
611
-
612
- async def _check_docker_status(client):
613
- try:
614
- await client.version()
615
- except DockerError:
616
- raise ProxiMLException(
617
- "Docker is not installed, not running, or your user does not have docker privileges. "
618
- + "Ensure docker is installed, then add your user to the docker group. "
619
- + "Commonly, this is accomplished by entering 'sudo gpasswd -a <username> docker'. "
620
- + "You may need to logout and log back in to your computer for this to take effect"
621
- )