thestage 0.6.5__py3-none-any.whl → 0.6.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.
- thestage/__init__.py +1 -1
- thestage/color_scheme/color_scheme.py +1 -0
- thestage/controllers/base_controller.py +4 -3
- thestage/controllers/config_controller.py +16 -4
- thestage/controllers/container_controller.py +151 -106
- thestage/controllers/instance_controller.py +35 -9
- thestage/controllers/project_controller.py +335 -89
- thestage/entities/container.py +5 -3
- thestage/entities/project_inference_simulator.py +2 -1
- thestage/entities/project_inference_simulator_model.py +2 -2
- thestage/entities/project_task.py +2 -3
- thestage/entities/rented_instance.py +2 -2
- thestage/entities/self_hosted_instance.py +2 -2
- thestage/helpers/error_handler.py +1 -1
- thestage/main.py +1 -1
- thestage/services/clients/git/git_client.py +1 -1
- thestage/services/clients/thestage_api/api_client.py +142 -109
- thestage/services/clients/thestage_api/dtos/base_controller/connect_resolve_response.py +22 -0
- thestage/services/clients/thestage_api/dtos/container_param_request.py +1 -1
- thestage/services/clients/thestage_api/dtos/container_response.py +1 -21
- thestage/services/clients/thestage_api/dtos/docker_container_controller/docker_container_list_request.py +2 -1
- thestage/services/clients/thestage_api/dtos/inference_controller/deploy_inference_model_to_instance_request.py +5 -1
- thestage/services/clients/thestage_api/dtos/inference_controller/deploy_inference_model_to_instance_response.py +2 -1
- thestage/services/clients/thestage_api/dtos/inference_controller/deploy_inference_model_to_sagemaker_request.py +1 -0
- thestage/services/clients/thestage_api/dtos/inference_controller/get_inference_simulator_request.py +2 -1
- thestage/services/clients/thestage_api/dtos/inference_controller/{inference_simulator_list_for_project_request.py → inference_simulator_list_request.py} +3 -2
- thestage/services/clients/thestage_api/dtos/inference_controller/{inference_simulator_list_for_project_response.py → inference_simulator_list_response.py} +1 -1
- thestage/services/clients/thestage_api/dtos/inference_controller/inference_simulator_model_list_for_project_request.py +2 -1
- thestage/services/clients/thestage_api/dtos/instance_rented_response.py +4 -37
- thestage/services/clients/thestage_api/dtos/logging_controller/log_polling_request.py +3 -3
- thestage/services/clients/thestage_api/dtos/logging_controller/user_logs_query_request.py +3 -11
- thestage/services/clients/thestage_api/dtos/project_controller/project_get_deploy_ssh_key_request.py +1 -0
- thestage/services/clients/thestage_api/dtos/project_controller/project_push_inference_simulator_model_request.py +2 -1
- thestage/services/clients/thestage_api/dtos/project_controller/project_run_task_request.py +2 -4
- thestage/services/clients/thestage_api/dtos/project_controller/project_run_task_response.py +3 -0
- thestage/services/clients/thestage_api/dtos/project_controller/project_start_inference_simulator_request.py +5 -3
- thestage/services/clients/thestage_api/dtos/project_response.py +3 -15
- thestage/services/clients/thestage_api/dtos/selfhosted_instance_response.py +2 -20
- thestage/services/clients/thestage_api/dtos/ssh_key_controller/add_ssh_key_to_user_response.py +1 -1
- thestage/services/clients/thestage_api/dtos/ssh_key_controller/add_ssh_public_key_to_instance_request.py +4 -2
- thestage/services/clients/thestage_api/dtos/ssh_key_controller/is_user_has_public_ssh_key_response.py +1 -1
- thestage/services/clients/thestage_api/dtos/task_controller/task_list_for_project_request.py +4 -1
- thestage/services/clients/thestage_api/dtos/task_controller/task_view_response.py +0 -2
- thestage/services/config_provider/config_provider.py +2 -2
- thestage/services/connect/connect_service.py +77 -74
- thestage/services/container/container_service.py +120 -41
- thestage/services/container/mapper/container_mapper.py +2 -1
- thestage/services/instance/instance_service.py +13 -20
- thestage/services/instance/mapper/instance_mapper.py +1 -3
- thestage/services/instance/mapper/selfhosted_mapper.py +3 -4
- thestage/services/logging/logging_service.py +45 -48
- thestage/services/project/dto/inference_simulator_dto.py +1 -10
- thestage/services/project/dto/inference_simulator_model_dto.py +2 -10
- thestage/services/project/dto/project_config.py +2 -2
- thestage/services/project/mapper/project_inference_simulator_mapper.py +1 -0
- thestage/services/project/mapper/project_inference_simulator_model_mapper.py +2 -2
- thestage/services/project/mapper/project_task_mapper.py +2 -3
- thestage/services/project/project_service.py +174 -140
- thestage/services/remote_server_service.py +1 -0
- thestage/services/task/dto/task_dto.py +3 -23
- {thestage-0.6.5.dist-info → thestage-0.6.7.dist-info}/METADATA +4 -2
- {thestage-0.6.5.dist-info → thestage-0.6.7.dist-info}/RECORD +65 -74
- {thestage-0.6.5.dist-info → thestage-0.6.7.dist-info}/WHEEL +1 -1
- thestage/services/clients/thestage_api/dtos/cloud_provider_region.py +0 -19
- thestage/services/clients/thestage_api/dtos/docker_container_assigned_device.py +0 -10
- thestage/services/clients/thestage_api/dtos/enums/currency_type.py +0 -10
- thestage/services/clients/thestage_api/dtos/enums/daemon_status.py +0 -9
- thestage/services/clients/thestage_api/dtos/enums/disk_type.py +0 -7
- thestage/services/clients/thestage_api/dtos/enums/drive_type.py +0 -7
- thestage/services/clients/thestage_api/dtos/enums/instance_type.py +0 -7
- thestage/services/clients/thestage_api/dtos/enums/location_region.py +0 -11
- thestage/services/clients/thestage_api/dtos/enums/power_status.py +0 -10
- thestage/services/clients/thestage_api/dtos/price_definition.py +0 -14
- {thestage-0.6.5.dist-info → thestage-0.6.7.dist-info}/entry_points.txt +0 -0
- {thestage-0.6.5.dist-info → thestage-0.6.7.dist-info/licenses}/LICENSE.txt +0 -0
|
@@ -3,21 +3,18 @@ import typer
|
|
|
3
3
|
|
|
4
4
|
from thestage.cli_command import CliCommand
|
|
5
5
|
from thestage.cli_command_helper import check_command_permission
|
|
6
|
+
from thestage.color_scheme.color_scheme import ColorScheme
|
|
6
7
|
from thestage.i18n.translation import __
|
|
7
8
|
from thestage.services.clients.thestage_api.core.http_client_exception import HttpClientException
|
|
8
|
-
from thestage.services.clients.thestage_api.dtos.enums.container_status import DockerContainerStatus
|
|
9
|
-
from thestage.services.clients.thestage_api.dtos.enums.selfhosted_status import SelfhostedBusinessStatus
|
|
10
9
|
from thestage.services.clients.thestage_api.dtos.enums.instance_rented_status import InstanceRentedBusinessStatus
|
|
11
10
|
from thestage.services.abstract_service import AbstractService
|
|
12
11
|
from thestage.helpers.error_handler import error_handler
|
|
13
12
|
from thestage.services.clients.thestage_api.api_client import TheStageApiClient
|
|
14
|
-
from thestage.services.clients.thestage_api.dtos.enums.task_status import TaskStatus
|
|
15
13
|
from thestage.services.clients.thestage_api.dtos.instance_rented_response import InstanceRentedDto
|
|
16
14
|
from thestage.services.container.container_service import ContainerService
|
|
17
15
|
from thestage.services.instance.instance_service import InstanceService
|
|
18
16
|
from thestage.services.logging.logging_service import LoggingService
|
|
19
|
-
from
|
|
20
|
-
from thestage.helpers.logger.app_logger import app_logger
|
|
17
|
+
from rich import print
|
|
21
18
|
|
|
22
19
|
|
|
23
20
|
class ConnectService(AbstractService):
|
|
@@ -44,113 +41,119 @@ class ConnectService(AbstractService):
|
|
|
44
41
|
@error_handler()
|
|
45
42
|
def connect_to_entity(
|
|
46
43
|
self,
|
|
47
|
-
|
|
44
|
+
input_entity_identifier: str,
|
|
48
45
|
username: Optional[str],
|
|
49
46
|
private_key_path: Optional[str],
|
|
50
47
|
):
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
if
|
|
48
|
+
resolved_options = self.__thestage_api_client.resolve_user_input(entity_identifier=input_entity_identifier)
|
|
49
|
+
entities_available_for_connect_count = 0
|
|
50
|
+
task_presence = False
|
|
51
|
+
container_presence = False
|
|
52
|
+
rented_presence = False
|
|
53
|
+
selfhosted_presence = False
|
|
54
|
+
resolved_entity_public_id = None
|
|
55
|
+
|
|
56
|
+
if resolved_options.taskMatchData:
|
|
57
|
+
for task_item in resolved_options.taskMatchData:
|
|
58
|
+
message = f"Found a task with with matching {task_item.matchedField} in status: '{task_item.frontendStatus.status_translation}' (ID: {task_item.publicId})"
|
|
59
|
+
line_color = ColorScheme.SUCCESS.value if task_item.canConnect else 'default'
|
|
60
|
+
print(f"[{line_color}]{message}[{line_color}]")
|
|
61
|
+
if task_item.canConnect:
|
|
62
|
+
task_presence = True
|
|
63
|
+
resolved_entity_public_id = task_item.publicId
|
|
64
|
+
entities_available_for_connect_count += 1
|
|
65
|
+
|
|
66
|
+
if resolved_options.dockerContainerMatchData:
|
|
67
|
+
for container_item in resolved_options.dockerContainerMatchData:
|
|
68
|
+
message = f"Found a container with matching {container_item.matchedField} in status: '{container_item.frontendStatus.status_translation}' (ID: {container_item.publicId})"
|
|
69
|
+
line_color = ColorScheme.SUCCESS.value if container_item.canConnect else 'default'
|
|
70
|
+
print(f"[{line_color}]{message}[{line_color}]")
|
|
71
|
+
if container_item.canConnect:
|
|
72
|
+
container_presence = True
|
|
73
|
+
resolved_entity_public_id = container_item.publicId
|
|
74
|
+
entities_available_for_connect_count += 1
|
|
75
|
+
|
|
76
|
+
if resolved_options.instanceRentedMatchData:
|
|
77
|
+
for instance_rented_item in resolved_options.instanceRentedMatchData:
|
|
78
|
+
message = f"Found a rented instance with matching {instance_rented_item.matchedField} in status: '{instance_rented_item.frontendStatus.status_translation}' (ID: {instance_rented_item.publicId})"
|
|
79
|
+
line_color = ColorScheme.SUCCESS.value if instance_rented_item.canConnect else 'default'
|
|
80
|
+
print(f"[{line_color}]{message}[{line_color}]")
|
|
81
|
+
|
|
82
|
+
if instance_rented_item.canConnect:
|
|
83
|
+
rented_presence = True
|
|
84
|
+
resolved_entity_public_id = instance_rented_item.publicId
|
|
85
|
+
entities_available_for_connect_count += 1
|
|
86
|
+
|
|
87
|
+
if resolved_options.selfhostedInstanceMatchData:
|
|
88
|
+
for selfhosted_item in resolved_options.selfhostedInstanceMatchData:
|
|
89
|
+
message = f"Found a self-hosted instance with matching {selfhosted_item.matchedField} in status: '{selfhosted_item.frontendStatus.status_translation}' (ID: {selfhosted_item.publicId})"
|
|
90
|
+
line_color = ColorScheme.SUCCESS.value if selfhosted_item.canConnect else 'default'
|
|
91
|
+
print(f"[{line_color}]{message}[{line_color}]")
|
|
92
|
+
|
|
93
|
+
if selfhosted_item.canConnect:
|
|
94
|
+
selfhosted_presence = True
|
|
95
|
+
resolved_entity_public_id = selfhosted_item.publicId
|
|
96
|
+
entities_available_for_connect_count += 1
|
|
97
|
+
|
|
98
|
+
if entities_available_for_connect_count > 1:
|
|
102
99
|
typer.echo("Provided identifier caused ambiguity")
|
|
103
100
|
typer.echo("Consider running a dedicated command to connect to the entity you need")
|
|
104
101
|
raise typer.Exit(code=1)
|
|
105
102
|
|
|
106
|
-
if
|
|
103
|
+
if entities_available_for_connect_count == 0:
|
|
107
104
|
typer.echo("There is nothing to connect to with the provided identifier")
|
|
108
105
|
raise typer.Exit(code=1)
|
|
109
106
|
|
|
110
107
|
if rented_presence:
|
|
111
108
|
check_command_permission(CliCommand.INSTANCE_RENTED_CONNECT)
|
|
112
|
-
typer.echo("Connecting to rented instance...")
|
|
109
|
+
typer.echo(f"Connecting to rented instance '{resolved_entity_public_id}'...")
|
|
113
110
|
self.__instance_service.connect_to_rented_instance(
|
|
114
|
-
|
|
111
|
+
instance_rented_public_id=resolved_entity_public_id,
|
|
112
|
+
instance_rented_slug=None,
|
|
115
113
|
input_ssh_key_path=private_key_path
|
|
116
114
|
)
|
|
117
115
|
|
|
118
116
|
if container_presence:
|
|
119
117
|
check_command_permission(CliCommand.CONTAINER_CONNECT)
|
|
120
|
-
typer.echo("Connecting to docker container...")
|
|
118
|
+
typer.echo(f"Connecting to docker container '{resolved_entity_public_id}'...")
|
|
121
119
|
self.__container_service.connect_to_container(
|
|
122
|
-
|
|
120
|
+
container_public_id=resolved_entity_public_id,
|
|
121
|
+
container_slug=None,
|
|
123
122
|
username=username,
|
|
124
123
|
input_ssh_key_path=private_key_path
|
|
125
124
|
)
|
|
126
125
|
|
|
127
126
|
if selfhosted_presence:
|
|
128
127
|
check_command_permission(CliCommand.INSTANCE_SELF_HOSTED_CONNECT)
|
|
129
|
-
typer.echo("Connecting to self-hosted instance...")
|
|
128
|
+
typer.echo(f"Connecting to self-hosted instance '{resolved_entity_public_id}'...")
|
|
130
129
|
|
|
131
130
|
self.__instance_service.connect_to_selfhosted_instance(
|
|
132
|
-
|
|
131
|
+
selfhosted_instance_public_id=resolved_entity_public_id,
|
|
132
|
+
selfhosted_instance_slug=None,
|
|
133
133
|
username=username,
|
|
134
134
|
input_ssh_key_path=private_key_path
|
|
135
135
|
)
|
|
136
136
|
|
|
137
137
|
if task_presence:
|
|
138
|
-
typer.echo(
|
|
139
|
-
self.__logging_service.stream_task_logs_with_controls(
|
|
138
|
+
typer.echo(f"Connecting to task '{resolved_entity_public_id}'...")
|
|
139
|
+
self.__logging_service.stream_task_logs_with_controls(task_public_id=resolved_entity_public_id)
|
|
140
140
|
|
|
141
141
|
|
|
142
142
|
@error_handler()
|
|
143
|
-
def upload_ssh_key(self, public_key_contents: str, instance_slug: Optional[str]):
|
|
143
|
+
def upload_ssh_key(self, public_key_contents: str, instance_public_id: Optional[str], instance_slug: Optional[str]):
|
|
144
144
|
instance_rented: Optional[InstanceRentedDto] = None
|
|
145
|
-
if instance_slug:
|
|
145
|
+
if instance_slug or instance_public_id:
|
|
146
146
|
try:
|
|
147
|
-
instance_rented = self.__thestage_api_client.get_rented_instance(
|
|
147
|
+
instance_rented = self.__thestage_api_client.get_rented_instance(
|
|
148
|
+
instance_public_id=instance_public_id,
|
|
149
|
+
instance_slug=instance_slug
|
|
150
|
+
)
|
|
148
151
|
except HttpClientException as e:
|
|
149
152
|
instance_rented = None
|
|
150
153
|
|
|
151
154
|
# if no instances found - exit 1
|
|
152
155
|
if instance_rented is None:
|
|
153
|
-
typer.echo(f"No rented instance found with matching
|
|
156
|
+
typer.echo(f"No rented instance found with matching identifier")
|
|
154
157
|
raise typer.Exit(1)
|
|
155
158
|
|
|
156
159
|
note_to_send: Optional[str] = None
|
|
@@ -159,7 +162,7 @@ class ConnectService(AbstractService):
|
|
|
159
162
|
public_key=public_key_contents
|
|
160
163
|
)
|
|
161
164
|
|
|
162
|
-
|
|
165
|
+
ssh_key_pair_public_id = is_user_already_has_key_response.sshKeyPairPublicId
|
|
163
166
|
is_adding_key_to_user = not is_user_already_has_key_response.isUserHasPublicKey
|
|
164
167
|
|
|
165
168
|
if is_adding_key_to_user and not note_to_send:
|
|
@@ -179,12 +182,12 @@ class ConnectService(AbstractService):
|
|
|
179
182
|
note=note_to_send
|
|
180
183
|
)
|
|
181
184
|
typer.echo(f"Public key '{note_to_send}' added to your profile")
|
|
182
|
-
|
|
185
|
+
ssh_key_pair_public_id = add_ssh_key_to_user_response.sshKeyPairPublicId
|
|
183
186
|
|
|
184
187
|
if instance_rented:
|
|
185
188
|
self.__thestage_api_client.add_public_ssh_key_to_instance_rented(
|
|
186
|
-
|
|
187
|
-
|
|
189
|
+
instance_rented_public_id=instance_rented.public_id,
|
|
190
|
+
ssh_key_pair_public_id=ssh_key_pair_public_id
|
|
188
191
|
)
|
|
189
192
|
|
|
190
193
|
if instance_rented.frontend_status.status_key != InstanceRentedBusinessStatus.ONLINE:
|
|
@@ -1,7 +1,11 @@
|
|
|
1
|
+
import re
|
|
1
2
|
from pathlib import Path
|
|
2
3
|
from typing import List, Tuple, Optional, Dict
|
|
4
|
+
from rich import print
|
|
3
5
|
|
|
4
6
|
import typer
|
|
7
|
+
|
|
8
|
+
from thestage.color_scheme.color_scheme import ColorScheme
|
|
5
9
|
from thestage.entities.container import DockerContainerEntity
|
|
6
10
|
from thestage.services.clients.thestage_api.dtos.container_param_request import DockerContainerActionRequestDto
|
|
7
11
|
from thestage.services.clients.thestage_api.dtos.enums.container_pending_action import DockerContainerAction
|
|
@@ -42,7 +46,8 @@ class ContainerService(AbstractService):
|
|
|
42
46
|
self,
|
|
43
47
|
row: int,
|
|
44
48
|
page: int,
|
|
45
|
-
|
|
49
|
+
project_public_id: Optional[str],
|
|
50
|
+
project_slug: Optional[str],
|
|
46
51
|
statuses: List[str],
|
|
47
52
|
):
|
|
48
53
|
container_status_map = self.__thestage_api_client.get_container_business_status_map()
|
|
@@ -72,22 +77,18 @@ class ContainerService(AbstractService):
|
|
|
72
77
|
|
|
73
78
|
backend_statuses: List[str] = [key for key, value in container_status_map.items() if value in statuses]
|
|
74
79
|
|
|
75
|
-
project_id: Optional[int] = None
|
|
76
|
-
if project_uid:
|
|
77
|
-
project = self.__thestage_api_client.get_project_by_slug(slug=project_uid)
|
|
78
|
-
project_id = project.id
|
|
79
|
-
|
|
80
80
|
self.print(
|
|
81
81
|
func_get_data=self.get_list,
|
|
82
82
|
func_special_params={
|
|
83
83
|
'statuses': backend_statuses,
|
|
84
|
-
'
|
|
84
|
+
'project_slug': project_slug,
|
|
85
|
+
'project_public_id': project_public_id,
|
|
85
86
|
},
|
|
86
87
|
mapper=ContainerMapper(),
|
|
87
88
|
headers=list(map(lambda x: x.alias, DockerContainerEntity.model_fields.values())),
|
|
88
89
|
row=row,
|
|
89
90
|
page=page,
|
|
90
|
-
max_col_width=[
|
|
91
|
+
max_col_width=[35, 20, 25],
|
|
91
92
|
show_index="never",
|
|
92
93
|
)
|
|
93
94
|
|
|
@@ -98,26 +99,29 @@ class ContainerService(AbstractService):
|
|
|
98
99
|
statuses: List[str],
|
|
99
100
|
row: int = 5,
|
|
100
101
|
page: int = 1,
|
|
101
|
-
|
|
102
|
+
project_public_id: Optional[str] = None,
|
|
103
|
+
project_slug: Optional[str] = None,
|
|
102
104
|
) -> PaginatedEntityList[DockerContainerDto]:
|
|
103
105
|
|
|
104
106
|
list = self.__thestage_api_client.get_container_list(
|
|
105
107
|
statuses=statuses,
|
|
106
108
|
page=page,
|
|
107
109
|
limit=row,
|
|
108
|
-
|
|
110
|
+
project_public_id=project_public_id,
|
|
111
|
+
project_slug=project_slug
|
|
109
112
|
)
|
|
110
113
|
|
|
111
114
|
return list
|
|
112
115
|
|
|
116
|
+
# TODO delete this proxy method
|
|
113
117
|
@error_handler()
|
|
114
118
|
def get_container(
|
|
115
119
|
self,
|
|
116
|
-
|
|
120
|
+
container_public_id: Optional[str] = None,
|
|
117
121
|
container_slug: Optional[str] = None,
|
|
118
122
|
) -> Optional[DockerContainerDto]:
|
|
119
123
|
return self.__thestage_api_client.get_container(
|
|
120
|
-
|
|
124
|
+
container_public_id=container_public_id,
|
|
121
125
|
container_slug=container_slug,
|
|
122
126
|
)
|
|
123
127
|
|
|
@@ -150,7 +154,7 @@ class ContainerService(AbstractService):
|
|
|
150
154
|
if private_key_path:
|
|
151
155
|
typer.echo(f'Using configured private key for {ip_address}: {private_key_path}')
|
|
152
156
|
else:
|
|
153
|
-
typer.echo(f'Using SSH agent to connect to {ip_address}')
|
|
157
|
+
typer.echo(f'Using SSH agent to connect to {ip_address} as {username}')
|
|
154
158
|
else:
|
|
155
159
|
self.__config_provider.update_remote_server_config_entry(ip_address, Path(private_key_path))
|
|
156
160
|
typer.echo(f'Updated private key path for {ip_address}: {private_key_path}')
|
|
@@ -160,16 +164,18 @@ class ContainerService(AbstractService):
|
|
|
160
164
|
@error_handler()
|
|
161
165
|
def connect_to_container(
|
|
162
166
|
self,
|
|
163
|
-
|
|
167
|
+
container_public_id: Optional[str],
|
|
168
|
+
container_slug: Optional[str],
|
|
164
169
|
username: Optional[str],
|
|
165
170
|
input_ssh_key_path: Optional[str],
|
|
166
171
|
):
|
|
167
172
|
container: Optional[DockerContainerDto] = self.get_container(
|
|
168
|
-
|
|
173
|
+
container_public_id=container_public_id,
|
|
174
|
+
container_slug=container_slug,
|
|
169
175
|
)
|
|
170
176
|
|
|
171
177
|
if not container:
|
|
172
|
-
typer.echo(f"Container
|
|
178
|
+
typer.echo(f"Container not found")
|
|
173
179
|
raise typer.Exit(1)
|
|
174
180
|
|
|
175
181
|
self.check_if_container_running(
|
|
@@ -265,22 +271,49 @@ class ContainerService(AbstractService):
|
|
|
265
271
|
@error_handler()
|
|
266
272
|
def put_file_to_container(
|
|
267
273
|
self,
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
destination_path: Optional[str] = None,
|
|
272
|
-
username_param: Optional[str] = None,
|
|
274
|
+
source_path: str,
|
|
275
|
+
destination: str,
|
|
276
|
+
username_param: Optional[str],
|
|
273
277
|
):
|
|
274
|
-
|
|
275
|
-
|
|
278
|
+
container_args = re.match(r"^([\w\W]+?):([\w\W]+)$", destination)
|
|
279
|
+
if container_args is None:
|
|
280
|
+
typer.echo(__('Container name and source file path are required as the second argument'))
|
|
281
|
+
typer.echo(__('Example: container_name:/path/to/file'))
|
|
276
282
|
raise typer.Exit(1)
|
|
283
|
+
container_identifier = container_args.groups()[0]
|
|
284
|
+
destination_path = container_args.groups()[1].rstrip("/")
|
|
277
285
|
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
286
|
+
if not container_identifier:
|
|
287
|
+
typer.echo('Container identifier (container_id_or_name) is required')
|
|
288
|
+
raise typer.Exit(1)
|
|
289
|
+
|
|
290
|
+
resolved_options = self.__thestage_api_client.resolve_user_input(entity_identifier=container_identifier)
|
|
291
|
+
container_public_id = None
|
|
292
|
+
valid_container_count = 0
|
|
293
|
+
for container_item in resolved_options.dockerContainerMatchData:
|
|
294
|
+
message = f"Found a container with matching {container_item.matchedField} in status: '{container_item.frontendStatus.status_translation}' (ID: {container_item.publicId})"
|
|
295
|
+
line_color = ColorScheme.SUCCESS.value if container_item.canDownloadUploadOnContainer else 'default'
|
|
296
|
+
print(f"[{line_color}]{message}[{line_color}]")
|
|
297
|
+
if container_item.canDownloadUploadOnContainer:
|
|
298
|
+
valid_container_count += 1
|
|
299
|
+
container_public_id = container_item.publicId
|
|
300
|
+
|
|
301
|
+
if valid_container_count != 1:
|
|
302
|
+
typer.echo(f"Failed to resolve the container by provided identifier, as total of {valid_container_count} containers are valid options")
|
|
303
|
+
raise typer.Exit(1)
|
|
304
|
+
|
|
305
|
+
container: Optional[DockerContainerDto] = self.__thestage_api_client.get_container(
|
|
306
|
+
container_public_id=container_public_id,
|
|
282
307
|
)
|
|
283
308
|
|
|
309
|
+
if not container:
|
|
310
|
+
typer.echo(f"Unexpected error: container '{container_public_id}' not found")
|
|
311
|
+
raise typer.Exit(1)
|
|
312
|
+
|
|
313
|
+
if not self.__file_system_service.check_if_path_exist(file=source_path):
|
|
314
|
+
typer.echo(__("File not found at specified path"))
|
|
315
|
+
raise typer.Exit(1)
|
|
316
|
+
|
|
284
317
|
if not container.mappings or not container.mappings.directory_mappings:
|
|
285
318
|
typer.echo(__("Mapping folders not found"))
|
|
286
319
|
raise typer.Exit(1)
|
|
@@ -294,10 +327,18 @@ class ContainerService(AbstractService):
|
|
|
294
327
|
typer.echo(__("Cannot find matching container volume mapping for specified file path"))
|
|
295
328
|
raise typer.Exit(1)
|
|
296
329
|
|
|
330
|
+
username, ip_address, private_key_path = self.get_server_auth(
|
|
331
|
+
container=container,
|
|
332
|
+
username_param=username_param,
|
|
333
|
+
private_key_path_override=None
|
|
334
|
+
)
|
|
335
|
+
|
|
336
|
+
copy_only_folder_contents = source_path.endswith("/")
|
|
337
|
+
|
|
297
338
|
self.__remote_server_service.upload_data_to_container(
|
|
298
339
|
ip_address=ip_address,
|
|
299
340
|
username=username,
|
|
300
|
-
src_path=
|
|
341
|
+
src_path=source_path,
|
|
301
342
|
dest_path=destination_path,
|
|
302
343
|
instance_path=instance_path,
|
|
303
344
|
container_path=container_path,
|
|
@@ -308,31 +349,67 @@ class ContainerService(AbstractService):
|
|
|
308
349
|
@error_handler()
|
|
309
350
|
def get_file_from_container(
|
|
310
351
|
self,
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
copy_only_folder_contents: bool,
|
|
314
|
-
destination_path: Optional[str] = None,
|
|
352
|
+
source: str,
|
|
353
|
+
destination_path: str,
|
|
315
354
|
username_param: Optional[str] = None,
|
|
316
355
|
):
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
356
|
+
container_args = re.match(r"^([\w\W]+?):([\w\W]+)$", source)
|
|
357
|
+
|
|
358
|
+
if container_args is None:
|
|
359
|
+
typer.echo(__('Container name and source directory path are required as the first argument'))
|
|
360
|
+
typer.echo(__('Example: container_name:/path/to/file'))
|
|
361
|
+
raise typer.Exit(1)
|
|
362
|
+
container_identifier = container_args.groups()[0]
|
|
363
|
+
source_path = container_args.groups()[1]
|
|
364
|
+
|
|
365
|
+
if not container_identifier:
|
|
366
|
+
typer.echo('Container identifier (container_id_or_name) is required')
|
|
367
|
+
raise typer.Exit(1)
|
|
368
|
+
|
|
369
|
+
resolved_options = self.__thestage_api_client.resolve_user_input(entity_identifier=container_identifier)
|
|
370
|
+
container_public_id = None
|
|
371
|
+
valid_container_count = 0
|
|
372
|
+
for container_item in resolved_options.dockerContainerMatchData:
|
|
373
|
+
message = f"Found a container with matching {container_item.matchedField} in status: '{container_item.frontendStatus.status_translation}' (ID: {container_item.publicId})"
|
|
374
|
+
line_color = ColorScheme.SUCCESS.value if container_item.canDownloadUploadOnContainer else 'default'
|
|
375
|
+
print(f"[{line_color}]{message}[{line_color}]")
|
|
376
|
+
if container_item.canDownloadUploadOnContainer:
|
|
377
|
+
valid_container_count += 1
|
|
378
|
+
container_public_id = container_item.publicId
|
|
379
|
+
|
|
380
|
+
if valid_container_count != 1:
|
|
381
|
+
typer.echo(f"Failed to resolve the container by provided identifier, as total of {valid_container_count} containers are valid options")
|
|
382
|
+
raise typer.Exit(1)
|
|
383
|
+
|
|
384
|
+
container: Optional[DockerContainerDto] = self.__thestage_api_client.get_container(
|
|
385
|
+
container_public_id=container_public_id,
|
|
321
386
|
)
|
|
322
387
|
|
|
388
|
+
if not container:
|
|
389
|
+
typer.echo(f"Unexpected error: container '{container_public_id}' not found")
|
|
390
|
+
raise typer.Exit(1)
|
|
391
|
+
|
|
323
392
|
if not container.mappings or not container.mappings.directory_mappings:
|
|
324
393
|
typer.echo(__("Mapping folders not found"))
|
|
325
394
|
raise typer.Exit(1)
|
|
326
395
|
|
|
327
396
|
instance_path, container_path = self._get_new_path_from_mapping(
|
|
328
397
|
directory_mapping=container.mappings.directory_mappings,
|
|
329
|
-
destination_path=
|
|
398
|
+
destination_path=source_path,
|
|
330
399
|
)
|
|
331
400
|
|
|
332
401
|
if not instance_path and not container_path:
|
|
333
402
|
typer.echo(__("Cannot find matching container volume mapping for specified file path"))
|
|
334
403
|
raise typer.Exit(1)
|
|
335
404
|
|
|
405
|
+
username, ip_address, private_key_path = self.get_server_auth(
|
|
406
|
+
container=container,
|
|
407
|
+
username_param=username_param,
|
|
408
|
+
private_key_path_override=None,
|
|
409
|
+
)
|
|
410
|
+
|
|
411
|
+
copy_only_folder_contents=source_path.endswith("/")
|
|
412
|
+
|
|
336
413
|
self.__remote_server_service.download_data_from_container(
|
|
337
414
|
ip_address=ip_address,
|
|
338
415
|
username=username,
|
|
@@ -346,14 +423,16 @@ class ContainerService(AbstractService):
|
|
|
346
423
|
@error_handler()
|
|
347
424
|
def request_docker_container_action(
|
|
348
425
|
self,
|
|
349
|
-
|
|
426
|
+
container_public_id: Optional[str],
|
|
427
|
+
container_slug: Optional[str],
|
|
350
428
|
action: DockerContainerAction,
|
|
351
429
|
):
|
|
352
430
|
container: Optional[DockerContainerDto] = self.get_container(
|
|
353
|
-
|
|
431
|
+
container_public_id=container_public_id,
|
|
432
|
+
container_slug=container_slug,
|
|
354
433
|
)
|
|
355
434
|
if not container:
|
|
356
|
-
typer.echo(f"Container
|
|
435
|
+
typer.echo(f"Container not found")
|
|
357
436
|
raise typer.Exit(1)
|
|
358
437
|
|
|
359
438
|
if action == DockerContainerAction.START:
|
|
@@ -363,7 +442,7 @@ class ContainerService(AbstractService):
|
|
|
363
442
|
self.check_if_container_running(container=container)
|
|
364
443
|
|
|
365
444
|
request_params = DockerContainerActionRequestDto(
|
|
366
|
-
|
|
445
|
+
dockerContainerPublicId=container.public_id,
|
|
367
446
|
action=action,
|
|
368
447
|
)
|
|
369
448
|
result = self.__thestage_api_client.container_action(
|
|
@@ -22,8 +22,9 @@ class ContainerMapper(AbstractMapper):
|
|
|
22
22
|
|
|
23
23
|
return DockerContainerEntity(
|
|
24
24
|
status=item.frontend_status.status_translation if item.frontend_status else '',
|
|
25
|
+
public_id=item.public_id or '',
|
|
25
26
|
slug=item.slug or '',
|
|
26
|
-
|
|
27
|
+
project_slug=item.project.slug if item.project else '',
|
|
27
28
|
instance_type=instance_type,
|
|
28
29
|
instance_slug=instance_slug,
|
|
29
30
|
docker_image=item.docker_image or '',
|
|
@@ -35,21 +35,6 @@ class InstanceService(AbstractService):
|
|
|
35
35
|
self.__remote_server_service = remote_server_service
|
|
36
36
|
self.__config_provider = config_provider
|
|
37
37
|
|
|
38
|
-
def get_rented_instance(
|
|
39
|
-
self,
|
|
40
|
-
instance_slug: str,
|
|
41
|
-
) -> Optional[InstanceRentedDto]:
|
|
42
|
-
return self.__thestage_api_client.get_rented_instance(
|
|
43
|
-
instance_slug=instance_slug,
|
|
44
|
-
)
|
|
45
|
-
|
|
46
|
-
def get_self_hosted_instance(
|
|
47
|
-
self,
|
|
48
|
-
instance_slug: str,
|
|
49
|
-
) -> Optional[SelfHostedInstanceDto]:
|
|
50
|
-
return self.__thestage_api_client.get_selfhosted_instance(
|
|
51
|
-
instance_slug=instance_slug,
|
|
52
|
-
)
|
|
53
38
|
|
|
54
39
|
@error_handler()
|
|
55
40
|
def check_instance_status_to_connect(
|
|
@@ -116,10 +101,14 @@ class InstanceService(AbstractService):
|
|
|
116
101
|
@error_handler()
|
|
117
102
|
def connect_to_rented_instance(
|
|
118
103
|
self,
|
|
119
|
-
|
|
104
|
+
instance_rented_public_id: Optional[str],
|
|
105
|
+
instance_rented_slug: Optional[str],
|
|
120
106
|
input_ssh_key_path: Optional[str]
|
|
121
107
|
):
|
|
122
|
-
instance = self.get_rented_instance(
|
|
108
|
+
instance = self.__thestage_api_client.get_rented_instance(
|
|
109
|
+
instance_public_id=instance_rented_public_id,
|
|
110
|
+
instance_slug=instance_rented_slug,
|
|
111
|
+
)
|
|
123
112
|
|
|
124
113
|
if instance:
|
|
125
114
|
self.check_instance_status_to_connect(
|
|
@@ -151,7 +140,8 @@ class InstanceService(AbstractService):
|
|
|
151
140
|
@error_handler()
|
|
152
141
|
def connect_to_selfhosted_instance(
|
|
153
142
|
self,
|
|
154
|
-
|
|
143
|
+
selfhosted_instance_public_id: Optional[str],
|
|
144
|
+
selfhosted_instance_slug: Optional[str],
|
|
155
145
|
username: str,
|
|
156
146
|
input_ssh_key_path: Optional[str],
|
|
157
147
|
):
|
|
@@ -159,7 +149,10 @@ class InstanceService(AbstractService):
|
|
|
159
149
|
username = 'root'
|
|
160
150
|
typer.echo(__("No remote server username provided, using 'root' as username"))
|
|
161
151
|
|
|
162
|
-
instance = self.
|
|
152
|
+
instance = self.__thestage_api_client.get_selfhosted_instance(
|
|
153
|
+
instance_public_id=selfhosted_instance_public_id,
|
|
154
|
+
instance_slug=selfhosted_instance_slug,
|
|
155
|
+
)
|
|
163
156
|
|
|
164
157
|
if instance:
|
|
165
158
|
self.check_selfhosted_status_to_connect(
|
|
@@ -184,7 +177,7 @@ class InstanceService(AbstractService):
|
|
|
184
177
|
if input_ssh_key_path:
|
|
185
178
|
self.__config_provider.update_remote_server_config_entry(ip_address=instance.ip_address, ssh_key_path=Path(input_ssh_key_path))
|
|
186
179
|
else:
|
|
187
|
-
typer.echo(
|
|
180
|
+
typer.echo("Self-hosted instance not found")
|
|
188
181
|
|
|
189
182
|
|
|
190
183
|
@error_handler()
|
|
@@ -13,12 +13,10 @@ class InstanceMapper(AbstractMapper):
|
|
|
13
13
|
|
|
14
14
|
return RentedInstanceEntity(
|
|
15
15
|
slug=item.slug if item.slug else '',
|
|
16
|
-
|
|
16
|
+
public_id=item.public_id if item.public_id else '',
|
|
17
17
|
cpu_type=item.cpu_type if item.cpu_type else '',
|
|
18
18
|
gpu_type=item.gpu_type if item.gpu_type else '',
|
|
19
19
|
cpu_cores=str(item.cpu_cores) if item.cpu_cores else '',
|
|
20
20
|
ip_address=item.ip_address if item.ip_address else '',
|
|
21
21
|
status=item.frontend_status.status_translation if item.frontend_status else '',
|
|
22
|
-
created_at=str(item.created_at.strftime("%Y-%m-%d %H:%M:%S")) if item.created_at else '',
|
|
23
|
-
updated_at=str(item.updated_at.strftime("%Y-%m-%d %H:%M:%S")) if item.updated_at else '',
|
|
24
22
|
)
|
|
@@ -3,6 +3,7 @@ from typing import Optional, Any, Tuple
|
|
|
3
3
|
|
|
4
4
|
from thestage.entities.rented_instance import RentedInstanceEntity
|
|
5
5
|
from thestage.entities.self_hosted_instance import SelfHostedInstanceEntity
|
|
6
|
+
from thestage.services.clients.thestage_api.dtos.enums.gpu_name import InstanceGpuType
|
|
6
7
|
from thestage.services.clients.thestage_api.dtos.selfhosted_instance_response import SelfHostedInstanceDto
|
|
7
8
|
from thestage.services.abstract_mapper import AbstractMapper
|
|
8
9
|
|
|
@@ -18,16 +19,14 @@ class SelfHostedMapper(AbstractMapper):
|
|
|
18
19
|
gpus = [item.type.value for item in item.detected_gpus.gpus]
|
|
19
20
|
|
|
20
21
|
if len(gpus) == 0:
|
|
21
|
-
gpus = [
|
|
22
|
+
gpus = [InstanceGpuType.NO_GPU]
|
|
22
23
|
|
|
23
24
|
return SelfHostedInstanceEntity(
|
|
24
25
|
slug=item.slug,
|
|
25
|
-
|
|
26
|
+
public_id=item.public_id,
|
|
26
27
|
cpu_type=item.cpu_type,
|
|
27
28
|
cpu_cores=item.cpu_cores,
|
|
28
29
|
gpu_type=', '.join(gpus),
|
|
29
30
|
ip_address=item.ip_address,
|
|
30
31
|
status=item.frontend_status.status_translation if item.frontend_status else None,
|
|
31
|
-
created_at=item.created_at.strftime("%Y-%m-%d %H:%M:%S") if item.created_at else '',
|
|
32
|
-
updated_at=item.updated_at.strftime("%Y-%m-%d %H:%M:%S") if item.updated_at else '',
|
|
33
32
|
)
|