trainml 0.5.5__tar.gz → 0.5.6__tar.gz
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.
- {trainml-0.5.5/trainml.egg-info → trainml-0.5.6}/PKG-INFO +1 -1
- {trainml-0.5.5 → trainml-0.5.6}/pyproject.toml +1 -1
- trainml-0.5.6/tests/unit/cli/cloudbender/test_cli_reservation_unit.py +34 -0
- {trainml-0.5.5 → trainml-0.5.6}/tests/unit/cli/test_cli_project_unit.py +5 -9
- trainml-0.5.6/tests/unit/cloudbender/test_services_unit.py +161 -0
- {trainml-0.5.5 → trainml-0.5.6}/tests/unit/conftest.py +13 -13
- {trainml-0.5.5 → trainml-0.5.6}/tests/unit/test_projects_unit.py +34 -48
- {trainml-0.5.5 → trainml-0.5.6}/trainml/__init__.py +1 -1
- {trainml-0.5.5 → trainml-0.5.6}/trainml/cli/cloudbender/__init__.py +1 -1
- trainml-0.5.6/trainml/cli/cloudbender/service.py +129 -0
- {trainml-0.5.5 → trainml-0.5.6}/trainml/cli/project.py +10 -15
- {trainml-0.5.5 → trainml-0.5.6}/trainml/cloudbender/cloudbender.py +2 -2
- trainml-0.5.5/trainml/cloudbender/reservations.py → trainml-0.5.6/trainml/cloudbender/services.py +28 -39
- {trainml-0.5.5 → trainml-0.5.6}/trainml/projects.py +19 -30
- {trainml-0.5.5 → trainml-0.5.6/trainml.egg-info}/PKG-INFO +1 -1
- {trainml-0.5.5 → trainml-0.5.6}/trainml.egg-info/SOURCES.txt +3 -3
- trainml-0.5.5/tests/unit/cli/cloudbender/test_cli_reservation_unit.py +0 -38
- trainml-0.5.5/tests/unit/cloudbender/test_reservations_unit.py +0 -173
- trainml-0.5.5/trainml/cli/cloudbender/reservation.py +0 -159
- {trainml-0.5.5 → trainml-0.5.6}/LICENSE +0 -0
- {trainml-0.5.5 → trainml-0.5.6}/README.md +0 -0
- {trainml-0.5.5 → trainml-0.5.6}/examples/__init__.py +0 -0
- {trainml-0.5.5 → trainml-0.5.6}/examples/create_dataset_and_training_job.py +0 -0
- {trainml-0.5.5 → trainml-0.5.6}/examples/local_storage.py +0 -0
- {trainml-0.5.5 → trainml-0.5.6}/examples/training_inference_pipeline.py +0 -0
- {trainml-0.5.5 → trainml-0.5.6}/setup.cfg +0 -0
- {trainml-0.5.5 → trainml-0.5.6}/setup.py +0 -0
- {trainml-0.5.5 → trainml-0.5.6}/tests/integration/__init__.py +0 -0
- {trainml-0.5.5 → trainml-0.5.6}/tests/integration/cloudbender/__init__.py +0 -0
- {trainml-0.5.5 → trainml-0.5.6}/tests/integration/cloudbender/test_providers_integration.py +0 -0
- {trainml-0.5.5 → trainml-0.5.6}/tests/integration/conftest.py +0 -0
- {trainml-0.5.5 → trainml-0.5.6}/tests/integration/test_checkpoints_integration.py +0 -0
- {trainml-0.5.5 → trainml-0.5.6}/tests/integration/test_datasets_integration.py +0 -0
- {trainml-0.5.5 → trainml-0.5.6}/tests/integration/test_environments_integration.py +0 -0
- {trainml-0.5.5 → trainml-0.5.6}/tests/integration/test_gpu_types_integration.py +0 -0
- {trainml-0.5.5 → trainml-0.5.6}/tests/integration/test_jobs_integration.py +0 -0
- {trainml-0.5.5 → trainml-0.5.6}/tests/integration/test_models_integration.py +0 -0
- {trainml-0.5.5 → trainml-0.5.6}/tests/integration/test_projects_integration.py +0 -0
- {trainml-0.5.5 → trainml-0.5.6}/tests/integration/test_volumes_integration.py +0 -0
- {trainml-0.5.5 → trainml-0.5.6}/tests/unit/__init__.py +0 -0
- {trainml-0.5.5 → trainml-0.5.6}/tests/unit/cli/__init__.py +0 -0
- {trainml-0.5.5 → trainml-0.5.6}/tests/unit/cli/cloudbender/__init__.py +0 -0
- {trainml-0.5.5 → trainml-0.5.6}/tests/unit/cli/cloudbender/test_cli_datastore_unit.py +0 -0
- {trainml-0.5.5 → trainml-0.5.6}/tests/unit/cli/cloudbender/test_cli_device_unit.py +0 -0
- {trainml-0.5.5 → trainml-0.5.6}/tests/unit/cli/cloudbender/test_cli_node_unit.py +0 -0
- {trainml-0.5.5 → trainml-0.5.6}/tests/unit/cli/cloudbender/test_cli_provider_unit.py +0 -0
- {trainml-0.5.5 → trainml-0.5.6}/tests/unit/cli/cloudbender/test_cli_region_unit.py +0 -0
- {trainml-0.5.5 → trainml-0.5.6}/tests/unit/cli/conftest.py +0 -0
- {trainml-0.5.5 → trainml-0.5.6}/tests/unit/cli/test_cli_checkpoint_unit.py +0 -0
- {trainml-0.5.5 → trainml-0.5.6}/tests/unit/cli/test_cli_datasets_unit.py +0 -0
- {trainml-0.5.5 → trainml-0.5.6}/tests/unit/cli/test_cli_environment_unit.py +0 -0
- {trainml-0.5.5 → trainml-0.5.6}/tests/unit/cli/test_cli_gpu_unit.py +0 -0
- {trainml-0.5.5 → trainml-0.5.6}/tests/unit/cli/test_cli_job_unit.py +0 -0
- {trainml-0.5.5 → trainml-0.5.6}/tests/unit/cli/test_cli_model_unit.py +0 -0
- {trainml-0.5.5 → trainml-0.5.6}/tests/unit/cli/test_cli_volume_unit.py +0 -0
- {trainml-0.5.5 → trainml-0.5.6}/tests/unit/cloudbender/__init__.py +0 -0
- {trainml-0.5.5 → trainml-0.5.6}/tests/unit/cloudbender/test_datastores_unit.py +0 -0
- {trainml-0.5.5 → trainml-0.5.6}/tests/unit/cloudbender/test_device_configs_unit.py +0 -0
- {trainml-0.5.5 → trainml-0.5.6}/tests/unit/cloudbender/test_devices_unit.py +0 -0
- {trainml-0.5.5 → trainml-0.5.6}/tests/unit/cloudbender/test_nodes_unit.py +0 -0
- {trainml-0.5.5 → trainml-0.5.6}/tests/unit/cloudbender/test_providers_unit.py +0 -0
- {trainml-0.5.5 → trainml-0.5.6}/tests/unit/cloudbender/test_regions_unit.py +0 -0
- {trainml-0.5.5 → trainml-0.5.6}/tests/unit/test_auth.py +0 -0
- {trainml-0.5.5 → trainml-0.5.6}/tests/unit/test_checkpoints_unit.py +0 -0
- {trainml-0.5.5 → trainml-0.5.6}/tests/unit/test_connections_unit.py +0 -0
- {trainml-0.5.5 → trainml-0.5.6}/tests/unit/test_datasets_unit.py +0 -0
- {trainml-0.5.5 → trainml-0.5.6}/tests/unit/test_environments_unit.py +0 -0
- {trainml-0.5.5 → trainml-0.5.6}/tests/unit/test_exceptions.py +0 -0
- {trainml-0.5.5 → trainml-0.5.6}/tests/unit/test_gpu_types_unit.py +0 -0
- {trainml-0.5.5 → trainml-0.5.6}/tests/unit/test_jobs_unit.py +0 -0
- {trainml-0.5.5 → trainml-0.5.6}/tests/unit/test_models_unit.py +0 -0
- {trainml-0.5.5 → trainml-0.5.6}/tests/unit/test_trainml.py +0 -0
- {trainml-0.5.5 → trainml-0.5.6}/tests/unit/test_volumes_unit.py +0 -0
- {trainml-0.5.5 → trainml-0.5.6}/trainml/__main__.py +0 -0
- {trainml-0.5.5 → trainml-0.5.6}/trainml/auth.py +0 -0
- {trainml-0.5.5 → trainml-0.5.6}/trainml/checkpoints.py +0 -0
- {trainml-0.5.5 → trainml-0.5.6}/trainml/cli/__init__.py +0 -0
- {trainml-0.5.5 → trainml-0.5.6}/trainml/cli/checkpoint.py +0 -0
- {trainml-0.5.5 → trainml-0.5.6}/trainml/cli/cloudbender/datastore.py +0 -0
- {trainml-0.5.5 → trainml-0.5.6}/trainml/cli/cloudbender/device.py +0 -0
- {trainml-0.5.5 → trainml-0.5.6}/trainml/cli/cloudbender/node.py +0 -0
- {trainml-0.5.5 → trainml-0.5.6}/trainml/cli/cloudbender/provider.py +0 -0
- {trainml-0.5.5 → trainml-0.5.6}/trainml/cli/cloudbender/region.py +0 -0
- {trainml-0.5.5 → trainml-0.5.6}/trainml/cli/connection.py +0 -0
- {trainml-0.5.5 → trainml-0.5.6}/trainml/cli/dataset.py +0 -0
- {trainml-0.5.5 → trainml-0.5.6}/trainml/cli/environment.py +0 -0
- {trainml-0.5.5 → trainml-0.5.6}/trainml/cli/gpu.py +0 -0
- {trainml-0.5.5 → trainml-0.5.6}/trainml/cli/job/__init__.py +0 -0
- {trainml-0.5.5 → trainml-0.5.6}/trainml/cli/job/create.py +0 -0
- {trainml-0.5.5 → trainml-0.5.6}/trainml/cli/model.py +0 -0
- {trainml-0.5.5 → trainml-0.5.6}/trainml/cli/volume.py +0 -0
- {trainml-0.5.5 → trainml-0.5.6}/trainml/cloudbender/__init__.py +0 -0
- {trainml-0.5.5 → trainml-0.5.6}/trainml/cloudbender/datastores.py +0 -0
- {trainml-0.5.5 → trainml-0.5.6}/trainml/cloudbender/device_configs.py +0 -0
- {trainml-0.5.5 → trainml-0.5.6}/trainml/cloudbender/devices.py +0 -0
- {trainml-0.5.5 → trainml-0.5.6}/trainml/cloudbender/nodes.py +0 -0
- {trainml-0.5.5 → trainml-0.5.6}/trainml/cloudbender/providers.py +0 -0
- {trainml-0.5.5 → trainml-0.5.6}/trainml/cloudbender/regions.py +0 -0
- {trainml-0.5.5 → trainml-0.5.6}/trainml/connections.py +0 -0
- {trainml-0.5.5 → trainml-0.5.6}/trainml/datasets.py +0 -0
- {trainml-0.5.5 → trainml-0.5.6}/trainml/environments.py +0 -0
- {trainml-0.5.5 → trainml-0.5.6}/trainml/exceptions.py +0 -0
- {trainml-0.5.5 → trainml-0.5.6}/trainml/gpu_types.py +0 -0
- {trainml-0.5.5 → trainml-0.5.6}/trainml/jobs.py +0 -0
- {trainml-0.5.5 → trainml-0.5.6}/trainml/models.py +0 -0
- {trainml-0.5.5 → trainml-0.5.6}/trainml/trainml.py +0 -0
- {trainml-0.5.5 → trainml-0.5.6}/trainml/volumes.py +0 -0
- {trainml-0.5.5 → trainml-0.5.6}/trainml.egg-info/dependency_links.txt +0 -0
- {trainml-0.5.5 → trainml-0.5.6}/trainml.egg-info/entry_points.txt +0 -0
- {trainml-0.5.5 → trainml-0.5.6}/trainml.egg-info/requires.txt +0 -0
- {trainml-0.5.5 → trainml-0.5.6}/trainml.egg-info/top_level.txt +0 -0
|
@@ -21,7 +21,7 @@ markers = [
|
|
|
21
21
|
"nodes: Nodes tests",
|
|
22
22
|
"devices: Devices tests",
|
|
23
23
|
"datastores: Datastores tests",
|
|
24
|
-
"
|
|
24
|
+
"services: Services tests",
|
|
25
25
|
"device_configs: DeviceConfigs tests",
|
|
26
26
|
"unit: All unit tests (no trainML environment required)",
|
|
27
27
|
"integration: All integration tests (trainML environment required)",
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import re
|
|
2
|
+
import json
|
|
3
|
+
import click
|
|
4
|
+
from unittest.mock import AsyncMock, patch
|
|
5
|
+
from pytest import mark, fixture, raises
|
|
6
|
+
|
|
7
|
+
pytestmark = [mark.cli, mark.unit, mark.cloudbender, mark.services]
|
|
8
|
+
|
|
9
|
+
from trainml.cli.cloudbender import service as specimen
|
|
10
|
+
from trainml.cloudbender.services import Service
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def test_list(runner, mock_services):
|
|
14
|
+
with patch("trainml.cli.TrainML", new=AsyncMock) as mock_trainml:
|
|
15
|
+
mock_trainml.cloudbender = AsyncMock()
|
|
16
|
+
mock_trainml.cloudbender.services = AsyncMock()
|
|
17
|
+
mock_trainml.cloudbender.services.list = AsyncMock(return_value=mock_services)
|
|
18
|
+
result = runner.invoke(
|
|
19
|
+
specimen,
|
|
20
|
+
args=["list", "--provider=prov-id-1", "--region=reg-id-1"],
|
|
21
|
+
)
|
|
22
|
+
assert result.exit_code == 0
|
|
23
|
+
mock_trainml.cloudbender.services.list.assert_called_once_with(
|
|
24
|
+
provider_uuid="prov-id-1", region_uuid="reg-id-1"
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def test_list_no_provider(runner, mock_services):
|
|
29
|
+
with patch("trainml.cli.TrainML", new=AsyncMock) as mock_trainml:
|
|
30
|
+
mock_trainml.cloudbender = AsyncMock()
|
|
31
|
+
mock_trainml.cloudbender.services = AsyncMock()
|
|
32
|
+
mock_trainml.cloudbender.services.list = AsyncMock(return_value=mock_services)
|
|
33
|
+
result = runner.invoke(specimen, ["list"])
|
|
34
|
+
assert result.exit_code != 0
|
|
@@ -23,9 +23,7 @@ def test_list(runner, mock_projects):
|
|
|
23
23
|
def test_list_datastores(runner, mock_project_datastores):
|
|
24
24
|
with patch("trainml.cli.TrainML", new=AsyncMock) as mock_trainml:
|
|
25
25
|
mock_project = create_autospec(Project)
|
|
26
|
-
mock_project.list_datastores = AsyncMock(
|
|
27
|
-
return_value=mock_project_datastores
|
|
28
|
-
)
|
|
26
|
+
mock_project.list_datastores = AsyncMock(return_value=mock_project_datastores)
|
|
29
27
|
mock_trainml.projects.get = AsyncMock(return_value=mock_project)
|
|
30
28
|
result = runner.invoke(specimen, ["list-datastores"])
|
|
31
29
|
print(result)
|
|
@@ -33,14 +31,12 @@ def test_list_datastores(runner, mock_project_datastores):
|
|
|
33
31
|
mock_project.list_datastores.assert_called_once()
|
|
34
32
|
|
|
35
33
|
|
|
36
|
-
def
|
|
34
|
+
def test_list_services(runner, mock_project_services):
|
|
37
35
|
with patch("trainml.cli.TrainML", new=AsyncMock) as mock_trainml:
|
|
38
36
|
mock_project = create_autospec(Project)
|
|
39
|
-
mock_project.
|
|
40
|
-
return_value=mock_project_reservations
|
|
41
|
-
)
|
|
37
|
+
mock_project.list_services = AsyncMock(return_value=mock_project_services)
|
|
42
38
|
mock_trainml.projects.get = AsyncMock(return_value=mock_project)
|
|
43
|
-
result = runner.invoke(specimen, ["list-
|
|
39
|
+
result = runner.invoke(specimen, ["list-services"])
|
|
44
40
|
print(result)
|
|
45
41
|
assert result.exit_code == 0
|
|
46
|
-
mock_project.
|
|
42
|
+
mock_project.list_services.assert_called_once()
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import re
|
|
2
|
+
import json
|
|
3
|
+
import logging
|
|
4
|
+
from unittest.mock import AsyncMock, patch
|
|
5
|
+
from pytest import mark, fixture, raises
|
|
6
|
+
from aiohttp import WSMessage, WSMsgType
|
|
7
|
+
|
|
8
|
+
import trainml.cloudbender.services as specimen
|
|
9
|
+
from trainml.exceptions import (
|
|
10
|
+
ApiError,
|
|
11
|
+
SpecificationError,
|
|
12
|
+
TrainMLException,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
pytestmark = [mark.sdk, mark.unit, mark.cloudbender, mark.services]
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@fixture
|
|
19
|
+
def services(mock_trainml):
|
|
20
|
+
yield specimen.Services(mock_trainml)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@fixture
|
|
24
|
+
def service(mock_trainml):
|
|
25
|
+
yield specimen.Service(
|
|
26
|
+
mock_trainml,
|
|
27
|
+
provider_uuid="1",
|
|
28
|
+
region_uuid="a",
|
|
29
|
+
service_id="x",
|
|
30
|
+
name="On-Prem Service",
|
|
31
|
+
public=False,
|
|
32
|
+
hostname="app1.proximl.cloud",
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class RegionsTests:
|
|
37
|
+
@mark.asyncio
|
|
38
|
+
async def test_get_service(
|
|
39
|
+
self,
|
|
40
|
+
services,
|
|
41
|
+
mock_trainml,
|
|
42
|
+
):
|
|
43
|
+
api_response = dict()
|
|
44
|
+
mock_trainml._query = AsyncMock(return_value=api_response)
|
|
45
|
+
await services.get("1234", "5687", "91011")
|
|
46
|
+
mock_trainml._query.assert_called_once_with(
|
|
47
|
+
"/provider/1234/region/5687/service/91011", "GET", {}
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
@mark.asyncio
|
|
51
|
+
async def test_list_services(
|
|
52
|
+
self,
|
|
53
|
+
services,
|
|
54
|
+
mock_trainml,
|
|
55
|
+
):
|
|
56
|
+
api_response = dict()
|
|
57
|
+
mock_trainml._query = AsyncMock(return_value=api_response)
|
|
58
|
+
await services.list("1234", "5687")
|
|
59
|
+
mock_trainml._query.assert_called_once_with(
|
|
60
|
+
"/provider/1234/region/5687/service", "GET", {}
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
@mark.asyncio
|
|
64
|
+
async def test_remove_service(
|
|
65
|
+
self,
|
|
66
|
+
services,
|
|
67
|
+
mock_trainml,
|
|
68
|
+
):
|
|
69
|
+
api_response = dict()
|
|
70
|
+
mock_trainml._query = AsyncMock(return_value=api_response)
|
|
71
|
+
await services.remove("1234", "4567", "8910")
|
|
72
|
+
mock_trainml._query.assert_called_once_with(
|
|
73
|
+
"/provider/1234/region/4567/service/8910", "DELETE", {}
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
@mark.asyncio
|
|
77
|
+
async def test_create_service(self, services, mock_trainml):
|
|
78
|
+
requested_config = dict(
|
|
79
|
+
provider_uuid="provider-id-1",
|
|
80
|
+
region_uuid="region-id-1",
|
|
81
|
+
name="On-Prem Service",
|
|
82
|
+
public=False,
|
|
83
|
+
)
|
|
84
|
+
expected_payload = dict(
|
|
85
|
+
name="On-Prem Service",
|
|
86
|
+
public=False,
|
|
87
|
+
)
|
|
88
|
+
api_response = {
|
|
89
|
+
"provider_uuid": "provider-id-1",
|
|
90
|
+
"region_uuid": "region-id-1",
|
|
91
|
+
"service_id": "service-id-1",
|
|
92
|
+
"name": "On-Prem Service",
|
|
93
|
+
"public": False,
|
|
94
|
+
"hostname": "app1.proximl.cloud",
|
|
95
|
+
"createdAt": "2020-12-31T23:59:59.000Z",
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
mock_trainml._query = AsyncMock(return_value=api_response)
|
|
99
|
+
response = await services.create(**requested_config)
|
|
100
|
+
mock_trainml._query.assert_called_once_with(
|
|
101
|
+
"/provider/provider-id-1/region/region-id-1/service",
|
|
102
|
+
"POST",
|
|
103
|
+
None,
|
|
104
|
+
expected_payload,
|
|
105
|
+
)
|
|
106
|
+
assert response.id == "service-id-1"
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
class serviceTests:
|
|
110
|
+
def test_service_properties(self, service):
|
|
111
|
+
assert isinstance(service.id, str)
|
|
112
|
+
assert isinstance(service.provider_uuid, str)
|
|
113
|
+
assert isinstance(service.region_uuid, str)
|
|
114
|
+
assert isinstance(service.public, bool)
|
|
115
|
+
assert isinstance(service.name, str)
|
|
116
|
+
assert isinstance(service.hostname, str)
|
|
117
|
+
|
|
118
|
+
def test_service_str(self, service):
|
|
119
|
+
string = str(service)
|
|
120
|
+
regex = r"^{.*\"service_id\": \"" + service.id + r"\".*}$"
|
|
121
|
+
assert isinstance(string, str)
|
|
122
|
+
assert re.match(regex, string)
|
|
123
|
+
|
|
124
|
+
def test_service_repr(self, service):
|
|
125
|
+
string = repr(service)
|
|
126
|
+
regex = r"^Service\( trainml , \*\*{.*'service_id': '" + service.id + r"'.*}\)$"
|
|
127
|
+
assert isinstance(string, str)
|
|
128
|
+
assert re.match(regex, string)
|
|
129
|
+
|
|
130
|
+
def test_service_bool(self, service, mock_trainml):
|
|
131
|
+
empty_service = specimen.Service(mock_trainml)
|
|
132
|
+
assert bool(service)
|
|
133
|
+
assert not bool(empty_service)
|
|
134
|
+
|
|
135
|
+
@mark.asyncio
|
|
136
|
+
async def test_service_remove(self, service, mock_trainml):
|
|
137
|
+
api_response = dict()
|
|
138
|
+
mock_trainml._query = AsyncMock(return_value=api_response)
|
|
139
|
+
await service.remove()
|
|
140
|
+
mock_trainml._query.assert_called_once_with(
|
|
141
|
+
"/provider/1/region/a/service/x", "DELETE"
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
@mark.asyncio
|
|
145
|
+
async def test_service_refresh(self, service, mock_trainml):
|
|
146
|
+
api_response = {
|
|
147
|
+
"provider_uuid": "provider-id-1",
|
|
148
|
+
"region_uuid": "region-id-1",
|
|
149
|
+
"service_id": "service-id-1",
|
|
150
|
+
"name": "On-Prem Service",
|
|
151
|
+
"public": False,
|
|
152
|
+
"hostname": "app1.proximl.cloud",
|
|
153
|
+
"createdAt": "2020-12-31T23:59:59.000Z",
|
|
154
|
+
}
|
|
155
|
+
mock_trainml._query = AsyncMock(return_value=api_response)
|
|
156
|
+
response = await service.refresh()
|
|
157
|
+
mock_trainml._query.assert_called_once_with(
|
|
158
|
+
f"/provider/1/region/a/service/x", "GET"
|
|
159
|
+
)
|
|
160
|
+
assert service.id == "service-id-1"
|
|
161
|
+
assert response.id == "service-id-1"
|
|
@@ -17,7 +17,7 @@ from trainml.projects import (
|
|
|
17
17
|
Projects,
|
|
18
18
|
Project,
|
|
19
19
|
ProjectDatastore,
|
|
20
|
-
|
|
20
|
+
ProjectService,
|
|
21
21
|
)
|
|
22
22
|
from trainml.cloudbender import Cloudbender
|
|
23
23
|
from trainml.cloudbender.providers import Provider, Providers
|
|
@@ -25,7 +25,7 @@ from trainml.cloudbender.regions import Region, Regions
|
|
|
25
25
|
from trainml.cloudbender.nodes import Node, Nodes
|
|
26
26
|
from trainml.cloudbender.devices import Device, Devices
|
|
27
27
|
from trainml.cloudbender.datastores import Datastore, Datastores
|
|
28
|
-
from trainml.cloudbender.
|
|
28
|
+
from trainml.cloudbender.services import Service, Services
|
|
29
29
|
from trainml.cloudbender.device_configs import DeviceConfig, DeviceConfigs
|
|
30
30
|
|
|
31
31
|
|
|
@@ -887,27 +887,27 @@ def mock_project_datastores():
|
|
|
887
887
|
|
|
888
888
|
|
|
889
889
|
@fixture(scope="session")
|
|
890
|
-
def
|
|
890
|
+
def mock_services():
|
|
891
891
|
trainml = Mock()
|
|
892
892
|
yield [
|
|
893
|
-
|
|
893
|
+
Service(
|
|
894
894
|
trainml,
|
|
895
895
|
**{
|
|
896
896
|
"provider_uuid": "prov-id-1",
|
|
897
897
|
"region_uuid": "reg-id-1",
|
|
898
|
-
"
|
|
898
|
+
"service_id": "res-id-1",
|
|
899
899
|
"type": "port",
|
|
900
900
|
"name": "On-Prem Service A",
|
|
901
901
|
"resource": "8001",
|
|
902
902
|
"hostname": "service-a.local",
|
|
903
903
|
},
|
|
904
904
|
),
|
|
905
|
-
|
|
905
|
+
Service(
|
|
906
906
|
trainml,
|
|
907
907
|
**{
|
|
908
908
|
"provider_uuid": "prov-id-2",
|
|
909
909
|
"region_uuid": "reg-id-2",
|
|
910
|
-
"
|
|
910
|
+
"service_id": "res-id-2",
|
|
911
911
|
"type": "port",
|
|
912
912
|
"name": "Cloud Service B",
|
|
913
913
|
"resource": "8001",
|
|
@@ -918,10 +918,10 @@ def mock_reservations():
|
|
|
918
918
|
|
|
919
919
|
|
|
920
920
|
@fixture(scope="session")
|
|
921
|
-
def
|
|
921
|
+
def mock_project_services():
|
|
922
922
|
trainml = Mock()
|
|
923
923
|
yield [
|
|
924
|
-
|
|
924
|
+
ProjectService(
|
|
925
925
|
trainml,
|
|
926
926
|
**{
|
|
927
927
|
"project_uuid": "proj-id-1",
|
|
@@ -933,7 +933,7 @@ def mock_project_reservations():
|
|
|
933
933
|
"hostname": "service-a.local",
|
|
934
934
|
},
|
|
935
935
|
),
|
|
936
|
-
|
|
936
|
+
ProjectService(
|
|
937
937
|
trainml,
|
|
938
938
|
**{
|
|
939
939
|
"project_uuid": "proj-id-1",
|
|
@@ -990,7 +990,7 @@ def mock_trainml(
|
|
|
990
990
|
mock_nodes,
|
|
991
991
|
mock_devices,
|
|
992
992
|
mock_datastores,
|
|
993
|
-
|
|
993
|
+
mock_services,
|
|
994
994
|
mock_device_configs,
|
|
995
995
|
):
|
|
996
996
|
trainml = create_autospec(TrainML)
|
|
@@ -1028,8 +1028,8 @@ def mock_trainml(
|
|
|
1028
1028
|
trainml.cloudbender.devices.list = AsyncMock(return_value=mock_devices)
|
|
1029
1029
|
trainml.cloudbender.datastores = create_autospec(Datastores)
|
|
1030
1030
|
trainml.cloudbender.datastores.list = AsyncMock(return_value=mock_datastores)
|
|
1031
|
-
trainml.cloudbender.
|
|
1032
|
-
trainml.cloudbender.
|
|
1031
|
+
trainml.cloudbender.services = create_autospec(Services)
|
|
1032
|
+
trainml.cloudbender.services.list = AsyncMock(return_value=mock_services)
|
|
1033
1033
|
trainml.cloudbender.device_configs = create_autospec(DeviceConfigs)
|
|
1034
1034
|
trainml.cloudbender.device_configs.list = AsyncMock(
|
|
1035
1035
|
return_value=mock_device_configs
|
|
@@ -49,11 +49,11 @@ def project_datastore(mock_trainml):
|
|
|
49
49
|
|
|
50
50
|
|
|
51
51
|
@fixture
|
|
52
|
-
def
|
|
53
|
-
yield specimen.
|
|
52
|
+
def project_service(mock_trainml):
|
|
53
|
+
yield specimen.ProjectService(
|
|
54
54
|
mock_trainml,
|
|
55
55
|
id="res-id-1",
|
|
56
|
-
name="
|
|
56
|
+
name="service 1",
|
|
57
57
|
project_uuid="proj-id-1",
|
|
58
58
|
region_uuid="reg-id-1",
|
|
59
59
|
type="port",
|
|
@@ -72,9 +72,7 @@ class ProjectsTests:
|
|
|
72
72
|
api_response = dict()
|
|
73
73
|
mock_trainml._query = AsyncMock(return_value=api_response)
|
|
74
74
|
await projects.get("1234")
|
|
75
|
-
mock_trainml._query.assert_called_once_with(
|
|
76
|
-
"/project/1234", "GET", dict()
|
|
77
|
-
)
|
|
75
|
+
mock_trainml._query.assert_called_once_with("/project/1234", "GET", dict())
|
|
78
76
|
|
|
79
77
|
@mark.asyncio
|
|
80
78
|
async def test_list_projects(
|
|
@@ -96,9 +94,7 @@ class ProjectsTests:
|
|
|
96
94
|
api_response = dict()
|
|
97
95
|
mock_trainml._query = AsyncMock(return_value=api_response)
|
|
98
96
|
await projects.remove("4567")
|
|
99
|
-
mock_trainml._query.assert_called_once_with(
|
|
100
|
-
"/project/4567", "DELETE", dict()
|
|
101
|
-
)
|
|
97
|
+
mock_trainml._query.assert_called_once_with("/project/4567", "DELETE", dict())
|
|
102
98
|
|
|
103
99
|
@mark.asyncio
|
|
104
100
|
async def test_create_project_simple(self, projects, mock_trainml):
|
|
@@ -156,36 +152,36 @@ class ProjectDatastoreTests:
|
|
|
156
152
|
assert not bool(empty_project_datastore)
|
|
157
153
|
|
|
158
154
|
|
|
159
|
-
class
|
|
160
|
-
def
|
|
161
|
-
assert isinstance(
|
|
162
|
-
assert isinstance(
|
|
163
|
-
assert isinstance(
|
|
164
|
-
assert isinstance(
|
|
165
|
-
assert isinstance(
|
|
166
|
-
assert isinstance(
|
|
167
|
-
assert isinstance(
|
|
155
|
+
class ProjectServiceTests:
|
|
156
|
+
def test_project_service_properties(self, project_service):
|
|
157
|
+
assert isinstance(project_service.id, str)
|
|
158
|
+
assert isinstance(project_service.name, str)
|
|
159
|
+
assert isinstance(project_service.project_uuid, str)
|
|
160
|
+
assert isinstance(project_service.type, str)
|
|
161
|
+
assert isinstance(project_service.hostname, str)
|
|
162
|
+
assert isinstance(project_service.resource, str)
|
|
163
|
+
assert isinstance(project_service.region_uuid, str)
|
|
168
164
|
|
|
169
|
-
def
|
|
170
|
-
string = str(
|
|
171
|
-
regex = r"^{.*\"id\": \"" +
|
|
165
|
+
def test_project_service_str(self, project_service):
|
|
166
|
+
string = str(project_service)
|
|
167
|
+
regex = r"^{.*\"id\": \"" + project_service.id + r"\".*}$"
|
|
172
168
|
assert isinstance(string, str)
|
|
173
169
|
assert re.match(regex, string)
|
|
174
170
|
|
|
175
|
-
def
|
|
176
|
-
string = repr(
|
|
171
|
+
def test_project_service_repr(self, project_service):
|
|
172
|
+
string = repr(project_service)
|
|
177
173
|
regex = (
|
|
178
|
-
r"^
|
|
179
|
-
+
|
|
174
|
+
r"^ProjectService\( trainml , \*\*{.*'id': '"
|
|
175
|
+
+ project_service.id
|
|
180
176
|
+ r"'.*}\)$"
|
|
181
177
|
)
|
|
182
178
|
assert isinstance(string, str)
|
|
183
179
|
assert re.match(regex, string)
|
|
184
180
|
|
|
185
|
-
def
|
|
186
|
-
|
|
187
|
-
assert bool(
|
|
188
|
-
assert not bool(
|
|
181
|
+
def test_project_service_bool(self, project_service, mock_trainml):
|
|
182
|
+
empty_project_service = specimen.ProjectService(mock_trainml)
|
|
183
|
+
assert bool(project_service)
|
|
184
|
+
assert not bool(empty_project_service)
|
|
189
185
|
|
|
190
186
|
|
|
191
187
|
class ProjectTests:
|
|
@@ -203,9 +199,7 @@ class ProjectTests:
|
|
|
203
199
|
|
|
204
200
|
def test_project_repr(self, project):
|
|
205
201
|
string = repr(project)
|
|
206
|
-
regex = (
|
|
207
|
-
r"^Project\( trainml , \*\*{.*'id': '" + project.id + r"'.*}\)$"
|
|
208
|
-
)
|
|
202
|
+
regex = r"^Project\( trainml , \*\*{.*'id': '" + project.id + r"'.*}\)$"
|
|
209
203
|
assert isinstance(string, str)
|
|
210
204
|
assert re.match(regex, string)
|
|
211
205
|
|
|
@@ -226,18 +220,14 @@ class ProjectTests:
|
|
|
226
220
|
api_response = dict()
|
|
227
221
|
mock_trainml._query = AsyncMock(return_value=api_response)
|
|
228
222
|
await project.refresh_datastores()
|
|
229
|
-
mock_trainml._query.assert_called_once_with(
|
|
230
|
-
"/project/1/datastores", "PATCH"
|
|
231
|
-
)
|
|
223
|
+
mock_trainml._query.assert_called_once_with("/project/1/datastores", "PATCH")
|
|
232
224
|
|
|
233
225
|
@mark.asyncio
|
|
234
|
-
async def
|
|
226
|
+
async def test_project_refresh_services(self, project, mock_trainml):
|
|
235
227
|
api_response = dict()
|
|
236
228
|
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
|
-
)
|
|
229
|
+
await project.refresh_services()
|
|
230
|
+
mock_trainml._query.assert_called_once_with("/project/1/services", "PATCH")
|
|
241
231
|
|
|
242
232
|
@mark.asyncio
|
|
243
233
|
async def test_project_list_datastores(self, project, mock_trainml):
|
|
@@ -259,13 +249,11 @@ class ProjectTests:
|
|
|
259
249
|
]
|
|
260
250
|
mock_trainml._query = AsyncMock(return_value=api_response)
|
|
261
251
|
resp = await project.list_datastores()
|
|
262
|
-
mock_trainml._query.assert_called_once_with(
|
|
263
|
-
"/project/1/datastores", "GET"
|
|
264
|
-
)
|
|
252
|
+
mock_trainml._query.assert_called_once_with("/project/1/datastores", "GET")
|
|
265
253
|
assert len(resp) == 2
|
|
266
254
|
|
|
267
255
|
@mark.asyncio
|
|
268
|
-
async def
|
|
256
|
+
async def test_project_list_services(self, project, mock_trainml):
|
|
269
257
|
api_response = [
|
|
270
258
|
{
|
|
271
259
|
"project_uuid": "proj-id-1",
|
|
@@ -287,8 +275,6 @@ class ProjectTests:
|
|
|
287
275
|
},
|
|
288
276
|
]
|
|
289
277
|
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
|
-
)
|
|
278
|
+
resp = await project.list_services()
|
|
279
|
+
mock_trainml._query.assert_called_once_with("/project/1/services", "GET")
|
|
294
280
|
assert len(resp) == 2
|
|
@@ -15,4 +15,4 @@ 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.service import service
|
|
@@ -0,0 +1,129 @@
|
|
|
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
|
+
"--public/--no-public",
|
|
79
|
+
default=True,
|
|
80
|
+
show_default=True,
|
|
81
|
+
help="Service should be accessible from the public internet.",
|
|
82
|
+
)
|
|
83
|
+
@click.argument("name", type=click.STRING, required=True)
|
|
84
|
+
@pass_config
|
|
85
|
+
def create(config, provider, region, public, name):
|
|
86
|
+
"""
|
|
87
|
+
Creates a service.
|
|
88
|
+
"""
|
|
89
|
+
return config.trainml.run(
|
|
90
|
+
config.trainml.client.cloudbender.services.create(
|
|
91
|
+
provider_uuid=provider, region_uuid=region, name=name, public=public
|
|
92
|
+
)
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
@service.command()
|
|
97
|
+
@click.option(
|
|
98
|
+
"--provider",
|
|
99
|
+
"-p",
|
|
100
|
+
type=click.STRING,
|
|
101
|
+
required=True,
|
|
102
|
+
help="The provider ID of the region.",
|
|
103
|
+
)
|
|
104
|
+
@click.option(
|
|
105
|
+
"--region",
|
|
106
|
+
"-r",
|
|
107
|
+
type=click.STRING,
|
|
108
|
+
required=True,
|
|
109
|
+
help="The region ID to remove the service from.",
|
|
110
|
+
)
|
|
111
|
+
@click.argument("service", type=click.STRING)
|
|
112
|
+
@pass_config
|
|
113
|
+
def remove(config, provider, region, service):
|
|
114
|
+
"""
|
|
115
|
+
Remove a service.
|
|
116
|
+
|
|
117
|
+
RESERVATION may be specified by name or ID, but ID is preferred.
|
|
118
|
+
"""
|
|
119
|
+
services = config.trainml.run(
|
|
120
|
+
config.trainml.client.cloudbender.services.list(
|
|
121
|
+
provider_uuid=provider, region_uuid=region
|
|
122
|
+
)
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
found = search_by_id_name(service, services)
|
|
126
|
+
if None is found:
|
|
127
|
+
raise click.UsageError("Cannot find specified service.")
|
|
128
|
+
|
|
129
|
+
return config.trainml.run(found.remove())
|