trainml 0.5.5__py3-none-any.whl → 0.5.7__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.
- tests/integration/test_jobs_integration.py +13 -0
- tests/unit/cli/cloudbender/test_cli_reservation_unit.py +10 -14
- tests/unit/cli/cloudbender/test_cli_service_unit.py +34 -0
- tests/unit/cli/test_cli_project_unit.py +5 -9
- tests/unit/cloudbender/test_data_connectors_unit.py +176 -0
- tests/unit/cloudbender/test_services_unit.py +167 -0
- tests/unit/conftest.py +13 -13
- tests/unit/test_projects_unit.py +77 -51
- trainml/__init__.py +1 -1
- trainml/checkpoints.py +25 -25
- trainml/cli/cloudbender/__init__.py +2 -1
- trainml/cli/cloudbender/data_connector.py +159 -0
- trainml/cli/cloudbender/service.py +146 -0
- trainml/cli/project.py +10 -15
- trainml/cloudbender/cloudbender.py +4 -2
- trainml/cloudbender/data_connectors.py +112 -0
- trainml/cloudbender/services.py +179 -0
- trainml/datasets.py +19 -8
- trainml/jobs.py +13 -6
- trainml/models.py +22 -19
- trainml/projects.py +72 -31
- trainml/volumes.py +9 -2
- {trainml-0.5.5.dist-info → trainml-0.5.7.dist-info}/METADATA +1 -1
- {trainml-0.5.5.dist-info → trainml-0.5.7.dist-info}/RECORD +28 -21
- {trainml-0.5.5.dist-info → trainml-0.5.7.dist-info}/LICENSE +0 -0
- {trainml-0.5.5.dist-info → trainml-0.5.7.dist-info}/WHEEL +0 -0
- {trainml-0.5.5.dist-info → trainml-0.5.7.dist-info}/entry_points.txt +0 -0
- {trainml-0.5.5.dist-info → trainml-0.5.7.dist-info}/top_level.txt +0 -0
tests/unit/test_projects_unit.py
CHANGED
|
@@ -49,16 +49,27 @@ def project_datastore(mock_trainml):
|
|
|
49
49
|
|
|
50
50
|
|
|
51
51
|
@fixture
|
|
52
|
-
def
|
|
53
|
-
yield specimen.
|
|
52
|
+
def project_data_connector(mock_trainml):
|
|
53
|
+
yield specimen.ProjectDataConnector(
|
|
54
|
+
mock_trainml,
|
|
55
|
+
id="ds-id-1",
|
|
56
|
+
name="connector 1",
|
|
57
|
+
project_uuid="proj-id-1",
|
|
58
|
+
type="custom",
|
|
59
|
+
region_uuid="reg-id-1",
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@fixture
|
|
64
|
+
def project_service(mock_trainml):
|
|
65
|
+
yield specimen.ProjectService(
|
|
54
66
|
mock_trainml,
|
|
55
67
|
id="res-id-1",
|
|
56
|
-
name="
|
|
68
|
+
name="service 1",
|
|
57
69
|
project_uuid="proj-id-1",
|
|
58
70
|
region_uuid="reg-id-1",
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
hostname="service.local",
|
|
71
|
+
public=False,
|
|
72
|
+
hostname="asdf.proximl.cloud",
|
|
62
73
|
)
|
|
63
74
|
|
|
64
75
|
|
|
@@ -72,9 +83,7 @@ class ProjectsTests:
|
|
|
72
83
|
api_response = dict()
|
|
73
84
|
mock_trainml._query = AsyncMock(return_value=api_response)
|
|
74
85
|
await projects.get("1234")
|
|
75
|
-
mock_trainml._query.assert_called_once_with(
|
|
76
|
-
"/project/1234", "GET", dict()
|
|
77
|
-
)
|
|
86
|
+
mock_trainml._query.assert_called_once_with("/project/1234", "GET", dict())
|
|
78
87
|
|
|
79
88
|
@mark.asyncio
|
|
80
89
|
async def test_list_projects(
|
|
@@ -96,9 +105,7 @@ class ProjectsTests:
|
|
|
96
105
|
api_response = dict()
|
|
97
106
|
mock_trainml._query = AsyncMock(return_value=api_response)
|
|
98
107
|
await projects.remove("4567")
|
|
99
|
-
mock_trainml._query.assert_called_once_with(
|
|
100
|
-
"/project/4567", "DELETE", dict()
|
|
101
|
-
)
|
|
108
|
+
mock_trainml._query.assert_called_once_with("/project/4567", "DELETE", dict())
|
|
102
109
|
|
|
103
110
|
@mark.asyncio
|
|
104
111
|
async def test_create_project_simple(self, projects, mock_trainml):
|
|
@@ -156,36 +163,65 @@ class ProjectDatastoreTests:
|
|
|
156
163
|
assert not bool(empty_project_datastore)
|
|
157
164
|
|
|
158
165
|
|
|
159
|
-
class
|
|
160
|
-
def
|
|
161
|
-
assert isinstance(
|
|
162
|
-
assert isinstance(
|
|
163
|
-
assert isinstance(
|
|
164
|
-
assert isinstance(
|
|
165
|
-
assert isinstance(
|
|
166
|
-
|
|
167
|
-
|
|
166
|
+
class ProjectDataConnectorTests:
|
|
167
|
+
def test_project_data_connector_properties(self, project_data_connector):
|
|
168
|
+
assert isinstance(project_data_connector.id, str)
|
|
169
|
+
assert isinstance(project_data_connector.name, str)
|
|
170
|
+
assert isinstance(project_data_connector.project_uuid, str)
|
|
171
|
+
assert isinstance(project_data_connector.type, str)
|
|
172
|
+
assert isinstance(project_data_connector.region_uuid, str)
|
|
173
|
+
|
|
174
|
+
def test_project_data_connector_str(self, project_data_connector):
|
|
175
|
+
string = str(project_data_connector)
|
|
176
|
+
regex = r"^{.*\"id\": \"" + project_data_connector.id + r"\".*}$"
|
|
177
|
+
assert isinstance(string, str)
|
|
178
|
+
assert re.match(regex, string)
|
|
179
|
+
|
|
180
|
+
def test_project_data_connector_repr(self, project_data_connector):
|
|
181
|
+
string = repr(project_data_connector)
|
|
182
|
+
regex = (
|
|
183
|
+
r"^ProjectDataConnector\( trainml , \*\*{.*'id': '"
|
|
184
|
+
+ project_data_connector.id
|
|
185
|
+
+ r"'.*}\)$"
|
|
186
|
+
)
|
|
187
|
+
assert isinstance(string, str)
|
|
188
|
+
assert re.match(regex, string)
|
|
189
|
+
|
|
190
|
+
def test_project_data_connector_bool(self, project_data_connector, mock_trainml):
|
|
191
|
+
empty_project_data_connector = specimen.ProjectDataConnector(mock_trainml)
|
|
192
|
+
assert bool(project_data_connector)
|
|
193
|
+
assert not bool(empty_project_data_connector)
|
|
168
194
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
195
|
+
|
|
196
|
+
class ProjectServiceTests:
|
|
197
|
+
def test_project_service_properties(self, project_service):
|
|
198
|
+
assert isinstance(project_service.id, str)
|
|
199
|
+
assert isinstance(project_service.name, str)
|
|
200
|
+
assert isinstance(project_service.project_uuid, str)
|
|
201
|
+
assert isinstance(project_service.hostname, str)
|
|
202
|
+
assert isinstance(project_service.public, bool)
|
|
203
|
+
assert isinstance(project_service.region_uuid, str)
|
|
204
|
+
|
|
205
|
+
def test_project_service_str(self, project_service):
|
|
206
|
+
string = str(project_service)
|
|
207
|
+
regex = r"^{.*\"id\": \"" + project_service.id + r"\".*}$"
|
|
172
208
|
assert isinstance(string, str)
|
|
173
209
|
assert re.match(regex, string)
|
|
174
210
|
|
|
175
|
-
def
|
|
176
|
-
string = repr(
|
|
211
|
+
def test_project_service_repr(self, project_service):
|
|
212
|
+
string = repr(project_service)
|
|
177
213
|
regex = (
|
|
178
|
-
r"^
|
|
179
|
-
+
|
|
214
|
+
r"^ProjectService\( trainml , \*\*{.*'id': '"
|
|
215
|
+
+ project_service.id
|
|
180
216
|
+ r"'.*}\)$"
|
|
181
217
|
)
|
|
182
218
|
assert isinstance(string, str)
|
|
183
219
|
assert re.match(regex, string)
|
|
184
220
|
|
|
185
|
-
def
|
|
186
|
-
|
|
187
|
-
assert bool(
|
|
188
|
-
assert not bool(
|
|
221
|
+
def test_project_service_bool(self, project_service, mock_trainml):
|
|
222
|
+
empty_project_service = specimen.ProjectService(mock_trainml)
|
|
223
|
+
assert bool(project_service)
|
|
224
|
+
assert not bool(empty_project_service)
|
|
189
225
|
|
|
190
226
|
|
|
191
227
|
class ProjectTests:
|
|
@@ -203,9 +239,7 @@ class ProjectTests:
|
|
|
203
239
|
|
|
204
240
|
def test_project_repr(self, project):
|
|
205
241
|
string = repr(project)
|
|
206
|
-
regex = (
|
|
207
|
-
r"^Project\( trainml , \*\*{.*'id': '" + project.id + r"'.*}\)$"
|
|
208
|
-
)
|
|
242
|
+
regex = r"^Project\( trainml , \*\*{.*'id': '" + project.id + r"'.*}\)$"
|
|
209
243
|
assert isinstance(string, str)
|
|
210
244
|
assert re.match(regex, string)
|
|
211
245
|
|
|
@@ -226,18 +260,14 @@ class ProjectTests:
|
|
|
226
260
|
api_response = dict()
|
|
227
261
|
mock_trainml._query = AsyncMock(return_value=api_response)
|
|
228
262
|
await project.refresh_datastores()
|
|
229
|
-
mock_trainml._query.assert_called_once_with(
|
|
230
|
-
"/project/1/datastores", "PATCH"
|
|
231
|
-
)
|
|
263
|
+
mock_trainml._query.assert_called_once_with("/project/1/datastores", "PATCH")
|
|
232
264
|
|
|
233
265
|
@mark.asyncio
|
|
234
|
-
async def
|
|
266
|
+
async def test_project_refresh_services(self, project, mock_trainml):
|
|
235
267
|
api_response = dict()
|
|
236
268
|
mock_trainml._query = AsyncMock(return_value=api_response)
|
|
237
|
-
await project.
|
|
238
|
-
mock_trainml._query.assert_called_once_with(
|
|
239
|
-
"/project/1/reservations", "PATCH"
|
|
240
|
-
)
|
|
269
|
+
await project.refresh_services()
|
|
270
|
+
mock_trainml._query.assert_called_once_with("/project/1/services", "PATCH")
|
|
241
271
|
|
|
242
272
|
@mark.asyncio
|
|
243
273
|
async def test_project_list_datastores(self, project, mock_trainml):
|
|
@@ -259,13 +289,11 @@ class ProjectTests:
|
|
|
259
289
|
]
|
|
260
290
|
mock_trainml._query = AsyncMock(return_value=api_response)
|
|
261
291
|
resp = await project.list_datastores()
|
|
262
|
-
mock_trainml._query.assert_called_once_with(
|
|
263
|
-
"/project/1/datastores", "GET"
|
|
264
|
-
)
|
|
292
|
+
mock_trainml._query.assert_called_once_with("/project/1/datastores", "GET")
|
|
265
293
|
assert len(resp) == 2
|
|
266
294
|
|
|
267
295
|
@mark.asyncio
|
|
268
|
-
async def
|
|
296
|
+
async def test_project_list_services(self, project, mock_trainml):
|
|
269
297
|
api_response = [
|
|
270
298
|
{
|
|
271
299
|
"project_uuid": "proj-id-1",
|
|
@@ -287,8 +315,6 @@ class ProjectTests:
|
|
|
287
315
|
},
|
|
288
316
|
]
|
|
289
317
|
mock_trainml._query = AsyncMock(return_value=api_response)
|
|
290
|
-
resp = await project.
|
|
291
|
-
mock_trainml._query.assert_called_once_with(
|
|
292
|
-
"/project/1/reservations", "GET"
|
|
293
|
-
)
|
|
318
|
+
resp = await project.list_services()
|
|
319
|
+
mock_trainml._query.assert_called_once_with("/project/1/services", "GET")
|
|
294
320
|
assert len(resp) == 2
|
trainml/__init__.py
CHANGED
trainml/checkpoints.py
CHANGED
|
@@ -23,9 +23,7 @@ class Checkpoints(object):
|
|
|
23
23
|
|
|
24
24
|
async def list(self, **kwargs):
|
|
25
25
|
resp = await self.trainml._query(f"/checkpoint", "GET", kwargs)
|
|
26
|
-
checkpoints = [
|
|
27
|
-
Checkpoint(self.trainml, **checkpoint) for checkpoint in resp
|
|
28
|
-
]
|
|
26
|
+
checkpoints = [Checkpoint(self.trainml, **checkpoint) for checkpoint in resp]
|
|
29
27
|
return checkpoints
|
|
30
28
|
|
|
31
29
|
async def list_public(self, **kwargs):
|
|
@@ -39,8 +37,7 @@ class Checkpoints(object):
|
|
|
39
37
|
source_type=source_type,
|
|
40
38
|
source_uri=source_uri,
|
|
41
39
|
source_options=kwargs.get("source_options"),
|
|
42
|
-
project_uuid=kwargs.get("project_uuid")
|
|
43
|
-
or self.trainml.active_project,
|
|
40
|
+
project_uuid=kwargs.get("project_uuid") or self.trainml.active_project,
|
|
44
41
|
)
|
|
45
42
|
payload = {k: v for k, v in data.items() if v is not None}
|
|
46
43
|
logging.info(f"Creating Checkpoint {name}")
|
|
@@ -60,9 +57,7 @@ class Checkpoint:
|
|
|
60
57
|
def __init__(self, trainml, **kwargs):
|
|
61
58
|
self.trainml = trainml
|
|
62
59
|
self._checkpoint = kwargs
|
|
63
|
-
self._id = self._checkpoint.get(
|
|
64
|
-
"id", self._checkpoint.get("checkpoint_uuid")
|
|
65
|
-
)
|
|
60
|
+
self._id = self._checkpoint.get("id", self._checkpoint.get("checkpoint_uuid"))
|
|
66
61
|
self._status = self._checkpoint.get("status")
|
|
67
62
|
self._name = self._checkpoint.get("name")
|
|
68
63
|
self._size = self._checkpoint.get("size")
|
|
@@ -123,15 +118,17 @@ class Checkpoint:
|
|
|
123
118
|
entity_type="checkpoint",
|
|
124
119
|
project_uuid=self._checkpoint.get("project_uuid"),
|
|
125
120
|
cidr=self._checkpoint.get("vpn").get("cidr"),
|
|
126
|
-
ssh_port=self._checkpoint.get("vpn")
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
output_path=
|
|
133
|
-
|
|
134
|
-
|
|
121
|
+
ssh_port=self._checkpoint.get("vpn").get("client").get("ssh_port"),
|
|
122
|
+
input_path=(
|
|
123
|
+
self._checkpoint.get("source_uri")
|
|
124
|
+
if self.status in ["new", "downloading"]
|
|
125
|
+
else None
|
|
126
|
+
),
|
|
127
|
+
output_path=(
|
|
128
|
+
self._checkpoint.get("output_uri")
|
|
129
|
+
if self.status == "exporting"
|
|
130
|
+
else None
|
|
131
|
+
),
|
|
135
132
|
)
|
|
136
133
|
else:
|
|
137
134
|
details = dict()
|
|
@@ -195,9 +192,7 @@ class Checkpoint:
|
|
|
195
192
|
if msg_handler:
|
|
196
193
|
msg_handler(data)
|
|
197
194
|
else:
|
|
198
|
-
timestamp = datetime.fromtimestamp(
|
|
199
|
-
int(data.get("time")) / 1000
|
|
200
|
-
)
|
|
195
|
+
timestamp = datetime.fromtimestamp(int(data.get("time")) / 1000)
|
|
201
196
|
print(
|
|
202
197
|
f"{timestamp.strftime('%m/%d/%Y, %H:%M:%S')}: {data.get('msg').rstrip()}"
|
|
203
198
|
)
|
|
@@ -224,19 +219,24 @@ class Checkpoint:
|
|
|
224
219
|
return self
|
|
225
220
|
|
|
226
221
|
async def wait_for(self, status, timeout=300):
|
|
222
|
+
if self.status == status:
|
|
223
|
+
return
|
|
227
224
|
valid_statuses = ["downloading", "ready", "archived"]
|
|
228
225
|
if not status in valid_statuses:
|
|
229
226
|
raise SpecificationError(
|
|
230
227
|
"status",
|
|
231
228
|
f"Invalid wait_for status {status}. Valid statuses are: {valid_statuses}",
|
|
232
229
|
)
|
|
233
|
-
|
|
234
|
-
|
|
230
|
+
|
|
231
|
+
MAX_TIMEOUT = 24 * 60 * 60
|
|
232
|
+
if timeout > MAX_TIMEOUT:
|
|
233
|
+
raise SpecificationError(
|
|
234
|
+
"timeout",
|
|
235
|
+
f"timeout must be less than {MAX_TIMEOUT} seconds.",
|
|
236
|
+
)
|
|
235
237
|
POLL_INTERVAL_MIN = 5
|
|
236
238
|
POLL_INTERVAL_MAX = 60
|
|
237
|
-
POLL_INTERVAL = max(
|
|
238
|
-
min(timeout / 60, POLL_INTERVAL_MAX), POLL_INTERVAL_MIN
|
|
239
|
-
)
|
|
239
|
+
POLL_INTERVAL = max(min(timeout / 60, POLL_INTERVAL_MAX), POLL_INTERVAL_MIN)
|
|
240
240
|
retry_count = math.ceil(timeout / POLL_INTERVAL)
|
|
241
241
|
count = 0
|
|
242
242
|
while count < retry_count:
|
|
@@ -15,4 +15,5 @@ from trainml.cli.cloudbender.region import region
|
|
|
15
15
|
from trainml.cli.cloudbender.node import node
|
|
16
16
|
from trainml.cli.cloudbender.device import device
|
|
17
17
|
from trainml.cli.cloudbender.datastore import datastore
|
|
18
|
-
from trainml.cli.cloudbender.
|
|
18
|
+
from trainml.cli.cloudbender.data_connector import data_connector
|
|
19
|
+
from trainml.cli.cloudbender.service import service
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import click
|
|
2
|
+
from trainml.cli import cli, pass_config, search_by_id_name
|
|
3
|
+
from trainml.cli.cloudbender import cloudbender
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@cloudbender.group()
|
|
7
|
+
@pass_config
|
|
8
|
+
def data_connector(config):
|
|
9
|
+
"""trainML CloudBender data connector commands."""
|
|
10
|
+
pass
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@data_connector.command()
|
|
14
|
+
@click.option(
|
|
15
|
+
"--provider",
|
|
16
|
+
"-p",
|
|
17
|
+
type=click.STRING,
|
|
18
|
+
required=True,
|
|
19
|
+
help="The provider ID of the region.",
|
|
20
|
+
)
|
|
21
|
+
@click.option(
|
|
22
|
+
"--region",
|
|
23
|
+
"-r",
|
|
24
|
+
type=click.STRING,
|
|
25
|
+
required=True,
|
|
26
|
+
help="The region ID to list data connectors for.",
|
|
27
|
+
)
|
|
28
|
+
@pass_config
|
|
29
|
+
def list(config, provider, region):
|
|
30
|
+
"""List data connectors."""
|
|
31
|
+
data = [
|
|
32
|
+
["ID", "NAME", "TYPE"],
|
|
33
|
+
[
|
|
34
|
+
"-" * 80,
|
|
35
|
+
"-" * 80,
|
|
36
|
+
"-" * 80,
|
|
37
|
+
],
|
|
38
|
+
]
|
|
39
|
+
|
|
40
|
+
data_connectors = config.trainml.run(
|
|
41
|
+
config.trainml.client.cloudbender.data_connectors.list(
|
|
42
|
+
provider_uuid=provider, region_uuid=region
|
|
43
|
+
)
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
for data_connector in data_connectors:
|
|
47
|
+
data.append(
|
|
48
|
+
[
|
|
49
|
+
data_connector.id,
|
|
50
|
+
data_connector.name,
|
|
51
|
+
data_connector.type,
|
|
52
|
+
]
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
for row in data:
|
|
56
|
+
click.echo(
|
|
57
|
+
"{: >37.36} {: >29.28} {: >9.8}" "".format(*row),
|
|
58
|
+
file=config.stdout,
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@data_connector.command()
|
|
63
|
+
@click.option(
|
|
64
|
+
"--provider",
|
|
65
|
+
"-p",
|
|
66
|
+
type=click.STRING,
|
|
67
|
+
required=True,
|
|
68
|
+
help="The provider ID of the region.",
|
|
69
|
+
)
|
|
70
|
+
@click.option(
|
|
71
|
+
"--region",
|
|
72
|
+
"-r",
|
|
73
|
+
type=click.STRING,
|
|
74
|
+
required=True,
|
|
75
|
+
help="The region ID to create the data_connector in.",
|
|
76
|
+
)
|
|
77
|
+
@click.option(
|
|
78
|
+
"--type",
|
|
79
|
+
"-t",
|
|
80
|
+
type=click.Choice(
|
|
81
|
+
[
|
|
82
|
+
"custom",
|
|
83
|
+
],
|
|
84
|
+
case_sensitive=False,
|
|
85
|
+
),
|
|
86
|
+
required=True,
|
|
87
|
+
help="The type of data connector to create.",
|
|
88
|
+
)
|
|
89
|
+
@click.option(
|
|
90
|
+
"--protocol",
|
|
91
|
+
"-r",
|
|
92
|
+
type=click.STRING,
|
|
93
|
+
help="The transport protocol of the data connector",
|
|
94
|
+
)
|
|
95
|
+
@click.option(
|
|
96
|
+
"--port-range",
|
|
97
|
+
"-p",
|
|
98
|
+
type=click.STRING,
|
|
99
|
+
help="The port range of the data connector",
|
|
100
|
+
)
|
|
101
|
+
@click.option(
|
|
102
|
+
"--cidr",
|
|
103
|
+
"-i",
|
|
104
|
+
type=click.STRING,
|
|
105
|
+
help="The IP range to allow in CIDR notation",
|
|
106
|
+
)
|
|
107
|
+
@click.argument("name", type=click.STRING, required=True)
|
|
108
|
+
@pass_config
|
|
109
|
+
def create(config, provider, region, type, protocol, port_range, cidr, name):
|
|
110
|
+
"""
|
|
111
|
+
Creates a data_connector.
|
|
112
|
+
"""
|
|
113
|
+
return config.trainml.run(
|
|
114
|
+
config.trainml.client.cloudbender.data_connectors.create(
|
|
115
|
+
provider_uuid=provider,
|
|
116
|
+
region_uuid=region,
|
|
117
|
+
name=name,
|
|
118
|
+
type=type,
|
|
119
|
+
protocol=protocol,
|
|
120
|
+
port_range=port_range,
|
|
121
|
+
cidr=cidr,
|
|
122
|
+
)
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
@data_connector.command()
|
|
127
|
+
@click.option(
|
|
128
|
+
"--provider",
|
|
129
|
+
"-p",
|
|
130
|
+
type=click.STRING,
|
|
131
|
+
required=True,
|
|
132
|
+
help="The provider ID of the region.",
|
|
133
|
+
)
|
|
134
|
+
@click.option(
|
|
135
|
+
"--region",
|
|
136
|
+
"-r",
|
|
137
|
+
type=click.STRING,
|
|
138
|
+
required=True,
|
|
139
|
+
help="The region ID to remove the data_connector from.",
|
|
140
|
+
)
|
|
141
|
+
@click.argument("data_connector", type=click.STRING)
|
|
142
|
+
@pass_config
|
|
143
|
+
def remove(config, provider, region, data_connector):
|
|
144
|
+
"""
|
|
145
|
+
Remove a data_connector.
|
|
146
|
+
|
|
147
|
+
DATASTORE may be specified by name or ID, but ID is preferred.
|
|
148
|
+
"""
|
|
149
|
+
data_connectors = config.trainml.run(
|
|
150
|
+
config.trainml.client.cloudbender.data_connectors.list(
|
|
151
|
+
provider_uuid=provider, region_uuid=region
|
|
152
|
+
)
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
found = search_by_id_name(data_connector, data_connectors)
|
|
156
|
+
if None is found:
|
|
157
|
+
raise click.UsageError("Cannot find specified data_connector.")
|
|
158
|
+
|
|
159
|
+
return config.trainml.run(found.remove())
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import click
|
|
2
|
+
from trainml.cli import cli, pass_config, search_by_id_name
|
|
3
|
+
from trainml.cli.cloudbender import cloudbender
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@cloudbender.group()
|
|
7
|
+
@pass_config
|
|
8
|
+
def service(config):
|
|
9
|
+
"""trainML CloudBender service commands."""
|
|
10
|
+
pass
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@service.command()
|
|
14
|
+
@click.option(
|
|
15
|
+
"--provider",
|
|
16
|
+
"-p",
|
|
17
|
+
type=click.STRING,
|
|
18
|
+
required=True,
|
|
19
|
+
help="The provider ID of the region.",
|
|
20
|
+
)
|
|
21
|
+
@click.option(
|
|
22
|
+
"--region",
|
|
23
|
+
"-r",
|
|
24
|
+
type=click.STRING,
|
|
25
|
+
required=True,
|
|
26
|
+
help="The region ID to list services for.",
|
|
27
|
+
)
|
|
28
|
+
@pass_config
|
|
29
|
+
def list(config, provider, region):
|
|
30
|
+
"""List services."""
|
|
31
|
+
data = [
|
|
32
|
+
["ID", "NAME", "HOSTNAME"],
|
|
33
|
+
[
|
|
34
|
+
"-" * 80,
|
|
35
|
+
"-" * 80,
|
|
36
|
+
"-" * 80,
|
|
37
|
+
],
|
|
38
|
+
]
|
|
39
|
+
|
|
40
|
+
services = config.trainml.run(
|
|
41
|
+
config.trainml.client.cloudbender.services.list(
|
|
42
|
+
provider_uuid=provider, region_uuid=region
|
|
43
|
+
)
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
for service in services:
|
|
47
|
+
data.append(
|
|
48
|
+
[
|
|
49
|
+
service.id,
|
|
50
|
+
service.name,
|
|
51
|
+
service.hostname,
|
|
52
|
+
]
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
for row in data:
|
|
56
|
+
click.echo(
|
|
57
|
+
"{: >25.24} {: >29.28} {: >40.39}" "".format(*row),
|
|
58
|
+
file=config.stdout,
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@service.command()
|
|
63
|
+
@click.option(
|
|
64
|
+
"--provider",
|
|
65
|
+
"-p",
|
|
66
|
+
type=click.STRING,
|
|
67
|
+
required=True,
|
|
68
|
+
help="The provider ID of the region.",
|
|
69
|
+
)
|
|
70
|
+
@click.option(
|
|
71
|
+
"--region",
|
|
72
|
+
"-r",
|
|
73
|
+
type=click.STRING,
|
|
74
|
+
required=True,
|
|
75
|
+
help="The region ID to create the service in.",
|
|
76
|
+
)
|
|
77
|
+
@click.option(
|
|
78
|
+
"--type",
|
|
79
|
+
"-t",
|
|
80
|
+
type=click.Choice(
|
|
81
|
+
[
|
|
82
|
+
"https",
|
|
83
|
+
"tcp",
|
|
84
|
+
"udp",
|
|
85
|
+
],
|
|
86
|
+
),
|
|
87
|
+
required=True,
|
|
88
|
+
help="The type of regional service.",
|
|
89
|
+
)
|
|
90
|
+
@click.option(
|
|
91
|
+
"--public/--no-public",
|
|
92
|
+
default=True,
|
|
93
|
+
show_default=True,
|
|
94
|
+
help="Service should be accessible from the public internet.",
|
|
95
|
+
)
|
|
96
|
+
@click.argument("name", type=click.STRING, required=True)
|
|
97
|
+
@pass_config
|
|
98
|
+
def create(config, provider, region, type, public, name):
|
|
99
|
+
"""
|
|
100
|
+
Creates a service.
|
|
101
|
+
"""
|
|
102
|
+
return config.trainml.run(
|
|
103
|
+
config.trainml.client.cloudbender.services.create(
|
|
104
|
+
provider_uuid=provider,
|
|
105
|
+
region_uuid=region,
|
|
106
|
+
name=name,
|
|
107
|
+
type=type,
|
|
108
|
+
public=public,
|
|
109
|
+
)
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
@service.command()
|
|
114
|
+
@click.option(
|
|
115
|
+
"--provider",
|
|
116
|
+
"-p",
|
|
117
|
+
type=click.STRING,
|
|
118
|
+
required=True,
|
|
119
|
+
help="The provider ID of the region.",
|
|
120
|
+
)
|
|
121
|
+
@click.option(
|
|
122
|
+
"--region",
|
|
123
|
+
"-r",
|
|
124
|
+
type=click.STRING,
|
|
125
|
+
required=True,
|
|
126
|
+
help="The region ID to remove the service from.",
|
|
127
|
+
)
|
|
128
|
+
@click.argument("service", type=click.STRING)
|
|
129
|
+
@pass_config
|
|
130
|
+
def remove(config, provider, region, service):
|
|
131
|
+
"""
|
|
132
|
+
Remove a service.
|
|
133
|
+
|
|
134
|
+
RESERVATION may be specified by name or ID, but ID is preferred.
|
|
135
|
+
"""
|
|
136
|
+
services = config.trainml.run(
|
|
137
|
+
config.trainml.client.cloudbender.services.list(
|
|
138
|
+
provider_uuid=provider, region_uuid=region
|
|
139
|
+
)
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
found = search_by_id_name(service, services)
|
|
143
|
+
if None is found:
|
|
144
|
+
raise click.UsageError("Cannot find specified service.")
|
|
145
|
+
|
|
146
|
+
return config.trainml.run(found.remove())
|
trainml/cli/project.py
CHANGED
|
@@ -115,40 +115,35 @@ def list_datastores(config):
|
|
|
115
115
|
|
|
116
116
|
@project.command()
|
|
117
117
|
@pass_config
|
|
118
|
-
def
|
|
119
|
-
"""List project
|
|
118
|
+
def list_services(config):
|
|
119
|
+
"""List project services."""
|
|
120
120
|
data = [
|
|
121
|
-
["ID", "NAME", "
|
|
121
|
+
["ID", "NAME", "HOSTNAME", "REGION_UUID"],
|
|
122
122
|
[
|
|
123
123
|
"-" * 80,
|
|
124
124
|
"-" * 80,
|
|
125
125
|
"-" * 80,
|
|
126
126
|
"-" * 80,
|
|
127
|
-
"-" * 80,
|
|
128
|
-
"-" * 80,
|
|
129
127
|
],
|
|
130
128
|
]
|
|
131
129
|
project = config.trainml.run(
|
|
132
130
|
config.trainml.client.projects.get(config.trainml.client.project)
|
|
133
131
|
)
|
|
134
132
|
|
|
135
|
-
|
|
133
|
+
services = config.trainml.run(project.list_services())
|
|
136
134
|
|
|
137
|
-
for
|
|
135
|
+
for service in services:
|
|
138
136
|
data.append(
|
|
139
137
|
[
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
reservation.hostname,
|
|
145
|
-
reservation.region_uuid,
|
|
138
|
+
service.id,
|
|
139
|
+
service.name,
|
|
140
|
+
service.hostname,
|
|
141
|
+
service.region_uuid,
|
|
146
142
|
]
|
|
147
143
|
)
|
|
148
144
|
|
|
149
145
|
for row in data:
|
|
150
146
|
click.echo(
|
|
151
|
-
"{: >38.36} {: >30.28} {: >
|
|
152
|
-
"".format(*row),
|
|
147
|
+
"{: >38.36} {: >30.28} {: >30.28} {: >38.36}" "".format(*row),
|
|
153
148
|
file=config.stdout,
|
|
154
149
|
)
|
|
@@ -3,7 +3,8 @@ from .regions import Regions
|
|
|
3
3
|
from .nodes import Nodes
|
|
4
4
|
from .devices import Devices
|
|
5
5
|
from .datastores import Datastores
|
|
6
|
-
from .
|
|
6
|
+
from .data_connectors import DataConnectors
|
|
7
|
+
from .services import Services
|
|
7
8
|
from .device_configs import DeviceConfigs
|
|
8
9
|
|
|
9
10
|
|
|
@@ -15,5 +16,6 @@ class Cloudbender(object):
|
|
|
15
16
|
self.nodes = Nodes(trainml)
|
|
16
17
|
self.devices = Devices(trainml)
|
|
17
18
|
self.datastores = Datastores(trainml)
|
|
18
|
-
self.
|
|
19
|
+
self.data_connectors = DataConnectors(trainml)
|
|
20
|
+
self.services = Services(trainml)
|
|
19
21
|
self.device_configs = DeviceConfigs(trainml)
|