thestage 0.5.47__py3-none-any.whl → 0.5.49__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/.env +5 -5
- thestage/__init__.py +3 -4
- thestage/__main__.py +9 -9
- thestage/cli_command.py +56 -56
- thestage/cli_command_helper.py +51 -51
- thestage/color_scheme/color_scheme.py +7 -7
- thestage/config/__init__.py +18 -18
- thestage/config/config_storage.py +5 -3
- thestage/config/env_base.py +7 -7
- thestage/controllers/__init__.py +0 -0
- thestage/controllers/base_controller.py +67 -67
- thestage/controllers/config_controller.py +137 -137
- thestage/controllers/container_controller.py +389 -389
- thestage/controllers/instance_controller.py +183 -183
- thestage/controllers/project_controller.py +802 -783
- thestage/controllers/utils_controller.py +32 -32
- thestage/debug_main.dist.py +28 -28
- thestage/entities/__init__.py +0 -0
- thestage/entities/container.py +17 -17
- thestage/entities/enums/__init__.py +0 -0
- thestage/entities/enums/order_direction_type.py +6 -6
- thestage/entities/enums/shell_type.py +7 -7
- thestage/entities/enums/tail_output_type.py +6 -6
- thestage/entities/enums/yes_no_response.py +7 -7
- thestage/entities/file_item.py +27 -27
- thestage/entities/project_inference_simulator.py +18 -18
- thestage/entities/project_inference_simulator_model.py +16 -16
- thestage/entities/project_task.py +19 -19
- thestage/entities/rented_instance.py +19 -19
- thestage/entities/self_hosted_instance.py +18 -18
- thestage/exceptions/__init__.py +0 -0
- thestage/exceptions/auth_exception.py +6 -6
- thestage/exceptions/base_exception.py +13 -13
- thestage/exceptions/business_logic_exception.py +6 -6
- thestage/exceptions/config_exception.py +6 -6
- thestage/exceptions/file_system_exception.py +6 -6
- thestage/exceptions/git_access_exception.py +17 -17
- thestage/exceptions/remote_server_exception.py +24 -24
- thestage/git/ProgressPrinter.py +22 -22
- thestage/helpers/__init__.py +0 -0
- thestage/helpers/error_handler.py +115 -115
- thestage/helpers/exception_hook.py +14 -14
- thestage/helpers/logger/__init__.py +0 -0
- thestage/helpers/logger/app_logger.py +50 -50
- thestage/helpers/ssh_util.py +38 -38
- thestage/i18n/en_GB/messages.po +947 -947
- thestage/i18n/translation.py +9 -9
- thestage/main.py +36 -36
- thestage/services/.env +6 -6
- thestage/services/__init__.py +0 -0
- thestage/services/abstract_mapper.py +9 -9
- thestage/services/abstract_service.py +87 -87
- thestage/services/app_config_service.py +52 -52
- thestage/services/clients/__init__.py +0 -0
- thestage/services/clients/git/__init__.py +0 -0
- thestage/services/clients/git/git_client.py +433 -331
- thestage/services/clients/thestage_api/__init__.py +0 -0
- thestage/services/clients/thestage_api/api_client.py +718 -720
- thestage/services/clients/thestage_api/core/api_client_core.py +108 -108
- thestage/services/clients/thestage_api/core/http_client_exception.py +12 -12
- thestage/services/clients/thestage_api/dtos/__init__.py +0 -0
- thestage/services/clients/thestage_api/dtos/base_response.py +13 -13
- thestage/services/clients/thestage_api/dtos/cloud_provider_region.py +19 -19
- thestage/services/clients/thestage_api/dtos/container_param_request.py +11 -11
- thestage/services/clients/thestage_api/dtos/container_response.py +67 -67
- thestage/services/clients/thestage_api/dtos/docker_container_assigned_device.py +10 -10
- thestage/services/clients/thestage_api/dtos/docker_container_controller/docker_container_list_request.py +13 -13
- thestage/services/clients/thestage_api/dtos/docker_container_controller/docker_container_list_response.py +13 -13
- thestage/services/clients/thestage_api/dtos/docker_container_mapping.py +10 -10
- thestage/services/clients/thestage_api/dtos/entity_filter_request.py +14 -14
- thestage/services/clients/thestage_api/dtos/enums/__init__.py +0 -0
- thestage/services/clients/thestage_api/dtos/enums/container_pending_action.py +10 -10
- thestage/services/clients/thestage_api/dtos/enums/container_status.py +17 -17
- thestage/services/clients/thestage_api/dtos/enums/cpu_type.py +8 -8
- thestage/services/clients/thestage_api/dtos/enums/currency_type.py +10 -10
- thestage/services/clients/thestage_api/dtos/enums/daemon_status.py +9 -9
- thestage/services/clients/thestage_api/dtos/enums/disk_type.py +7 -7
- thestage/services/clients/thestage_api/dtos/enums/drive_type.py +7 -7
- thestage/services/clients/thestage_api/dtos/enums/gpu_name.py +8 -8
- thestage/services/clients/thestage_api/dtos/enums/inference_model_status.py +9 -9
- thestage/services/clients/thestage_api/dtos/enums/inference_simulator_status.py +15 -15
- thestage/services/clients/thestage_api/dtos/enums/instance_rented_status.py +17 -17
- thestage/services/clients/thestage_api/dtos/enums/instance_type.py +7 -7
- thestage/services/clients/thestage_api/dtos/enums/location_region.py +11 -11
- thestage/services/clients/thestage_api/dtos/enums/power_status.py +10 -10
- thestage/services/clients/thestage_api/dtos/enums/provider_name.py +11 -11
- thestage/services/clients/thestage_api/dtos/enums/selfhosted_status.py +10 -10
- thestage/services/clients/thestage_api/dtos/enums/task_execution_status.py +12 -12
- thestage/services/clients/thestage_api/dtos/enums/task_status.py +12 -12
- thestage/services/clients/thestage_api/dtos/frontend_status.py +10 -10
- thestage/services/clients/thestage_api/dtos/inference_controller/deploy_inference_model_to_instance_request.py +13 -13
- thestage/services/clients/thestage_api/dtos/inference_controller/deploy_inference_model_to_instance_response.py +13 -13
- thestage/services/clients/thestage_api/dtos/inference_controller/deploy_inference_model_to_sagemaker_request.py +12 -12
- thestage/services/clients/thestage_api/dtos/inference_controller/deploy_inference_model_to_sagemaker_response.py +12 -12
- thestage/services/clients/thestage_api/dtos/inference_controller/get_inference_simulator_request.py +10 -10
- thestage/services/clients/thestage_api/dtos/inference_controller/get_inference_simulator_response.py +13 -13
- thestage/services/clients/thestage_api/dtos/inference_controller/inference_simulator_list_for_project_request.py +14 -14
- thestage/services/clients/thestage_api/dtos/inference_controller/inference_simulator_list_for_project_response.py +12 -12
- thestage/services/clients/thestage_api/dtos/inference_controller/inference_simulator_model_list_for_project_request.py +12 -12
- thestage/services/clients/thestage_api/dtos/inference_controller/inference_simulator_model_list_for_project_response.py +13 -13
- thestage/services/clients/thestage_api/dtos/inference_simulator_model_response.py +11 -11
- thestage/services/clients/thestage_api/dtos/inference_simulator_response.py +11 -11
- thestage/services/clients/thestage_api/dtos/installed_service.py +17 -17
- thestage/services/clients/thestage_api/dtos/instance_detected_gpus.py +20 -20
- thestage/services/clients/thestage_api/dtos/instance_rented_response.py +71 -71
- thestage/services/clients/thestage_api/dtos/logging_controller/docker_container_log_stream_request.py +7 -7
- thestage/services/clients/thestage_api/dtos/logging_controller/log_polling_request.py +13 -13
- thestage/services/clients/thestage_api/dtos/logging_controller/log_polling_response.py +14 -14
- thestage/services/clients/thestage_api/dtos/logging_controller/task_log_stream_request.py +7 -7
- thestage/services/clients/thestage_api/dtos/logging_controller/user_logs_query_request.py +21 -21
- thestage/services/clients/thestage_api/dtos/logging_controller/user_logs_query_response.py +14 -14
- thestage/services/clients/thestage_api/dtos/paginated_entity_list.py +11 -11
- thestage/services/clients/thestage_api/dtos/pagination_data.py +10 -10
- thestage/services/clients/thestage_api/dtos/price_definition.py +14 -14
- thestage/services/clients/thestage_api/dtos/project_controller/project_get_deploy_ssh_key_request.py +7 -7
- thestage/services/clients/thestage_api/dtos/project_controller/project_get_deploy_ssh_key_response.py +10 -10
- thestage/services/clients/thestage_api/dtos/project_controller/project_push_inference_simulator_model_request.py +8 -8
- thestage/services/clients/thestage_api/dtos/project_controller/project_push_inference_simulator_model_response.py +6 -6
- thestage/services/clients/thestage_api/dtos/project_controller/project_run_task_request.py +15 -15
- thestage/services/clients/thestage_api/dtos/project_controller/project_run_task_response.py +10 -10
- thestage/services/clients/thestage_api/dtos/project_controller/project_start_inference_simulator_request.py +13 -14
- thestage/services/clients/thestage_api/dtos/project_controller/project_start_inference_simulator_response.py +10 -10
- thestage/services/clients/thestage_api/dtos/project_response.py +32 -32
- thestage/services/clients/thestage_api/dtos/selfhosted_instance_response.py +56 -56
- thestage/services/clients/thestage_api/dtos/sftp_path_helper.py +13 -13
- thestage/services/clients/thestage_api/dtos/ssh_key_controller/add_ssh_key_to_user_request.py +8 -8
- thestage/services/clients/thestage_api/dtos/ssh_key_controller/add_ssh_key_to_user_response.py +11 -11
- thestage/services/clients/thestage_api/dtos/ssh_key_controller/add_ssh_public_key_to_instance_request.py +8 -8
- thestage/services/clients/thestage_api/dtos/ssh_key_controller/add_ssh_public_key_to_instance_response.py +11 -11
- thestage/services/clients/thestage_api/dtos/ssh_key_controller/is_user_has_public_ssh_key_request.py +7 -7
- thestage/services/clients/thestage_api/dtos/ssh_key_controller/is_user_has_public_ssh_key_response.py +12 -12
- thestage/services/clients/thestage_api/dtos/task_controller/task_list_for_project_request.py +10 -10
- thestage/services/clients/thestage_api/dtos/task_controller/task_list_for_project_response.py +12 -12
- thestage/services/clients/thestage_api/dtos/task_controller/task_status_localized_map_response.py +9 -9
- thestage/services/clients/thestage_api/dtos/task_controller/task_view_response.py +12 -12
- thestage/services/clients/thestage_api/dtos/user_controller/user_profile.py +12 -12
- thestage/services/clients/thestage_api/dtos/validate_token_response.py +11 -11
- thestage/services/config_provider/__init__.py +0 -0
- thestage/services/config_provider/config_provider.py +237 -237
- thestage/services/connect/connect_service.py +196 -196
- thestage/services/connect/dto/remote_server_config.py +9 -9
- thestage/services/container/__init__.py +0 -0
- thestage/services/container/container_service.py +374 -374
- thestage/services/container/mapper/__init__.py +0 -0
- thestage/services/container/mapper/container_mapper.py +30 -30
- thestage/services/core_files/config_entity.py +26 -26
- thestage/services/filesystem_service.py +133 -133
- thestage/services/instance/__init__.py +0 -0
- thestage/services/instance/instance_service.py +303 -303
- thestage/services/instance/mapper/__init__.py +0 -0
- thestage/services/instance/mapper/instance_mapper.py +24 -24
- thestage/services/instance/mapper/selfhosted_mapper.py +33 -33
- thestage/services/logging/byte_print_style.py +5 -5
- thestage/services/logging/dto/log_message.py +15 -15
- thestage/services/logging/dto/log_type.py +6 -6
- thestage/services/logging/exception/log_polling_exception.py +6 -6
- thestage/services/logging/logging_constants.py +3 -3
- thestage/services/logging/logging_service.py +367 -367
- thestage/services/project/__init__.py +0 -0
- thestage/services/project/dto/inference_simulator_dto.py +22 -22
- thestage/services/project/dto/inference_simulator_model_dto.py +20 -20
- thestage/services/project/dto/project_config.py +14 -14
- thestage/services/project/mapper/__init__.py +0 -0
- thestage/services/project/mapper/project_inference_simulator_mapper.py +21 -21
- thestage/services/project/mapper/project_inference_simulator_model_mapper.py +21 -21
- thestage/services/project/mapper/project_task_mapper.py +22 -22
- thestage/services/project/project_service.py +1260 -1241
- thestage/services/remote_server_service.py +609 -609
- thestage/services/service_factory.py +97 -97
- thestage/services/task/dto/task_dto.py +40 -40
- thestage/services/validation_service.py +61 -61
- {thestage-0.5.47.dist-info → thestage-0.5.49.dist-info}/LICENSE.txt +12 -12
- {thestage-0.5.47.dist-info → thestage-0.5.49.dist-info}/METADATA +1 -1
- thestage-0.5.49.dist-info/RECORD +176 -0
- {thestage-0.5.47.dist-info → thestage-0.5.49.dist-info}/WHEEL +1 -1
- thestage/debug_tests.py +0 -12
- thestage/services/clients/.DS_Store +0 -0
- thestage-0.5.47.dist-info/RECORD +0 -178
- {thestage-0.5.47.dist-info → thestage-0.5.49.dist-info}/entry_points.txt +0 -0
|
@@ -1,374 +1,374 @@
|
|
|
1
|
-
from pathlib import Path
|
|
2
|
-
from typing import List, Tuple, Optional, Dict
|
|
3
|
-
|
|
4
|
-
import typer
|
|
5
|
-
from thestage.entities.container import DockerContainerEntity
|
|
6
|
-
from thestage.services.clients.thestage_api.dtos.container_param_request import DockerContainerActionRequestDto
|
|
7
|
-
from thestage.services.clients.thestage_api.dtos.enums.container_pending_action import DockerContainerAction
|
|
8
|
-
from thestage.services.clients.thestage_api.dtos.enums.container_status import DockerContainerStatus
|
|
9
|
-
from thestage.entities.enums.shell_type import ShellType
|
|
10
|
-
from thestage.services.clients.thestage_api.dtos.paginated_entity_list import PaginatedEntityList
|
|
11
|
-
from thestage.services.container.mapper.container_mapper import ContainerMapper
|
|
12
|
-
from thestage.services.filesystem_service import FileSystemService
|
|
13
|
-
from thestage.services.remote_server_service import RemoteServerService
|
|
14
|
-
from thestage.i18n.translation import __
|
|
15
|
-
from thestage.services.abstract_service import AbstractService
|
|
16
|
-
from thestage.services.clients.thestage_api.dtos.container_response import DockerContainerDto
|
|
17
|
-
from thestage.helpers.error_handler import error_handler
|
|
18
|
-
from thestage.services.clients.thestage_api.api_client import TheStageApiClient
|
|
19
|
-
from thestage.services.config_provider.config_provider import ConfigProvider
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
class ContainerService(AbstractService):
|
|
23
|
-
|
|
24
|
-
__thestage_api_client: TheStageApiClient = None
|
|
25
|
-
__config_provider: ConfigProvider = None
|
|
26
|
-
|
|
27
|
-
def __init__(
|
|
28
|
-
self,
|
|
29
|
-
thestage_api_client: TheStageApiClient,
|
|
30
|
-
config_provider: ConfigProvider,
|
|
31
|
-
remote_server_service: RemoteServerService,
|
|
32
|
-
file_system_service: FileSystemService,
|
|
33
|
-
):
|
|
34
|
-
self.__config_provider = config_provider
|
|
35
|
-
self.__thestage_api_client = thestage_api_client
|
|
36
|
-
self.__remote_server_service = remote_server_service
|
|
37
|
-
self.__file_system_service = file_system_service
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
@error_handler()
|
|
41
|
-
def print_container_list(
|
|
42
|
-
self,
|
|
43
|
-
row: int,
|
|
44
|
-
page: int,
|
|
45
|
-
project_uid: Optional[str],
|
|
46
|
-
statuses: List[str],
|
|
47
|
-
):
|
|
48
|
-
container_status_map = self.__thestage_api_client.get_container_business_status_map()
|
|
49
|
-
|
|
50
|
-
if not statuses:
|
|
51
|
-
statuses = ({key: container_status_map[key] for key in [
|
|
52
|
-
DockerContainerStatus.RUNNING,
|
|
53
|
-
DockerContainerStatus.STARTING,
|
|
54
|
-
]}).values()
|
|
55
|
-
|
|
56
|
-
if "all" in statuses:
|
|
57
|
-
statuses = container_status_map.values()
|
|
58
|
-
|
|
59
|
-
for input_status_item in statuses:
|
|
60
|
-
if input_status_item not in container_status_map.values():
|
|
61
|
-
typer.echo(__("'%invalid_status%' is not one of %valid_statuses%", {
|
|
62
|
-
'invalid_status': input_status_item,
|
|
63
|
-
'valid_statuses': str(list(container_status_map.values()))
|
|
64
|
-
}))
|
|
65
|
-
raise typer.Exit(1)
|
|
66
|
-
|
|
67
|
-
typer.echo(__(
|
|
68
|
-
"Listing containers with the following statuses: %statuses%. To list all containers, use --status all",
|
|
69
|
-
placeholders={
|
|
70
|
-
'statuses': ', '.join([input_status_item for input_status_item in statuses])
|
|
71
|
-
}))
|
|
72
|
-
|
|
73
|
-
backend_statuses: List[str] = [key for key, value in container_status_map.items() if value in statuses]
|
|
74
|
-
|
|
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
|
-
self.print(
|
|
81
|
-
func_get_data=self.get_list,
|
|
82
|
-
func_special_params={
|
|
83
|
-
'statuses': backend_statuses,
|
|
84
|
-
'project_id': project_id,
|
|
85
|
-
},
|
|
86
|
-
mapper=ContainerMapper(),
|
|
87
|
-
headers=list(map(lambda x: x.alias, DockerContainerEntity.model_fields.values())),
|
|
88
|
-
row=row,
|
|
89
|
-
page=page,
|
|
90
|
-
max_col_width=[15, 20, 25],
|
|
91
|
-
show_index="never",
|
|
92
|
-
)
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
@error_handler()
|
|
96
|
-
def get_list(
|
|
97
|
-
self,
|
|
98
|
-
statuses: List[str],
|
|
99
|
-
row: int = 5,
|
|
100
|
-
page: int = 1,
|
|
101
|
-
project_id: Optional[int] = None,
|
|
102
|
-
) -> PaginatedEntityList[DockerContainerDto]:
|
|
103
|
-
|
|
104
|
-
list = self.__thestage_api_client.get_container_list(
|
|
105
|
-
statuses=statuses,
|
|
106
|
-
page=page,
|
|
107
|
-
limit=row,
|
|
108
|
-
project_id=project_id,
|
|
109
|
-
)
|
|
110
|
-
|
|
111
|
-
return list
|
|
112
|
-
|
|
113
|
-
@error_handler()
|
|
114
|
-
def get_container(
|
|
115
|
-
self,
|
|
116
|
-
container_id: Optional[int] = None,
|
|
117
|
-
container_slug: Optional[str] = None,
|
|
118
|
-
) -> Optional[DockerContainerDto]:
|
|
119
|
-
return self.__thestage_api_client.get_container(
|
|
120
|
-
container_id=container_id,
|
|
121
|
-
container_slug=container_slug,
|
|
122
|
-
)
|
|
123
|
-
|
|
124
|
-
def get_server_auth(
|
|
125
|
-
self,
|
|
126
|
-
container: DockerContainerDto,
|
|
127
|
-
username_param: Optional[str],
|
|
128
|
-
private_key_path_override: Optional[str],
|
|
129
|
-
) -> Tuple[str, str, Optional[str]]:
|
|
130
|
-
username = None
|
|
131
|
-
if container.instance_rented:
|
|
132
|
-
username = container.instance_rented.host_username
|
|
133
|
-
ip_address = container.instance_rented.ip_address
|
|
134
|
-
elif container.selfhosted_instance:
|
|
135
|
-
ip_address = container.selfhosted_instance.ip_address
|
|
136
|
-
else:
|
|
137
|
-
typer.echo(__("Neither rented nor self-hosted server instance found to connect to"))
|
|
138
|
-
raise typer.Exit(1)
|
|
139
|
-
|
|
140
|
-
if username_param:
|
|
141
|
-
username = username_param
|
|
142
|
-
|
|
143
|
-
if not username:
|
|
144
|
-
username = 'root'
|
|
145
|
-
typer.echo(__("No remote server username provided, using 'root' as username"))
|
|
146
|
-
|
|
147
|
-
private_key_path = private_key_path_override
|
|
148
|
-
if not private_key_path:
|
|
149
|
-
private_key_path = self.__config_provider.get_valid_private_key_path_by_ip_address(ip_address)
|
|
150
|
-
if private_key_path:
|
|
151
|
-
typer.echo(f'Using configured private key for {ip_address}: {private_key_path}')
|
|
152
|
-
else:
|
|
153
|
-
typer.echo(f'Using SSH agent to connect to {ip_address}')
|
|
154
|
-
else:
|
|
155
|
-
self.__config_provider.update_remote_server_config_entry(ip_address, Path(private_key_path))
|
|
156
|
-
typer.echo(f'Updated private key path for {ip_address}: {private_key_path}')
|
|
157
|
-
|
|
158
|
-
return username, ip_address, private_key_path
|
|
159
|
-
|
|
160
|
-
@error_handler()
|
|
161
|
-
def connect_to_container(
|
|
162
|
-
self,
|
|
163
|
-
container_uid: str,
|
|
164
|
-
username: Optional[str],
|
|
165
|
-
input_ssh_key_path: Optional[str],
|
|
166
|
-
):
|
|
167
|
-
container: Optional[DockerContainerDto] = self.get_container(
|
|
168
|
-
container_slug=container_uid,
|
|
169
|
-
)
|
|
170
|
-
|
|
171
|
-
if not container:
|
|
172
|
-
typer.echo(f"Container with UID '{container_uid}' not found")
|
|
173
|
-
raise typer.Exit(1)
|
|
174
|
-
|
|
175
|
-
self.check_if_container_running(
|
|
176
|
-
container=container
|
|
177
|
-
)
|
|
178
|
-
|
|
179
|
-
if not container.system_name:
|
|
180
|
-
typer.echo(__("Unable to connect to container: container system_name is missing"))
|
|
181
|
-
raise typer.Exit(1)
|
|
182
|
-
|
|
183
|
-
starting_directory: str = '/'
|
|
184
|
-
workspace_mappings = {v for v in container.mappings.directory_mappings.values() if v.startswith('/workspace/') or v == '/workspace'}
|
|
185
|
-
if len(workspace_mappings) > 0:
|
|
186
|
-
starting_directory = '/workspace'
|
|
187
|
-
|
|
188
|
-
inference_mappings = {v for v in container.mappings.directory_mappings.values() if v.startswith('/opt/') or v == '/opt'}
|
|
189
|
-
if len(inference_mappings) > 0:
|
|
190
|
-
starting_directory = '/opt/project'
|
|
191
|
-
|
|
192
|
-
username, ip_address, private_key_path = self.get_server_auth(
|
|
193
|
-
container=container,
|
|
194
|
-
username_param=username,
|
|
195
|
-
private_key_path_override=input_ssh_key_path
|
|
196
|
-
)
|
|
197
|
-
|
|
198
|
-
shell: Optional[ShellType] = self.__remote_server_service.get_shell_from_container(
|
|
199
|
-
ip_address=ip_address,
|
|
200
|
-
username=username,
|
|
201
|
-
container_name=container.system_name,
|
|
202
|
-
private_key_path=private_key_path
|
|
203
|
-
)
|
|
204
|
-
|
|
205
|
-
if not shell:
|
|
206
|
-
typer.echo(f"Failed to start shell (bash, sh) in container: ensure user '{username}' has Docker access and compatible shell is available")
|
|
207
|
-
raise typer.Exit(1)
|
|
208
|
-
|
|
209
|
-
self.__remote_server_service.connect_to_container(
|
|
210
|
-
ip_address=ip_address,
|
|
211
|
-
username=username,
|
|
212
|
-
docker_name=container.system_name,
|
|
213
|
-
starting_directory=starting_directory,
|
|
214
|
-
shell=shell,
|
|
215
|
-
private_key_path=private_key_path
|
|
216
|
-
)
|
|
217
|
-
|
|
218
|
-
@error_handler()
|
|
219
|
-
def check_if_container_stopped(
|
|
220
|
-
self,
|
|
221
|
-
container: DockerContainerDto,
|
|
222
|
-
) -> DockerContainerDto:
|
|
223
|
-
if container.frontend_status.status_key not in [
|
|
224
|
-
DockerContainerStatus.STOPPED.value,
|
|
225
|
-
]:
|
|
226
|
-
typer.echo(__(f'Container is not stopped (status: \'{container.frontend_status.status_translation}\')'))
|
|
227
|
-
raise typer.Exit(1)
|
|
228
|
-
|
|
229
|
-
return container
|
|
230
|
-
|
|
231
|
-
@error_handler()
|
|
232
|
-
def check_if_container_running(
|
|
233
|
-
self,
|
|
234
|
-
container: DockerContainerDto,
|
|
235
|
-
):
|
|
236
|
-
if container.frontend_status.status_key not in [
|
|
237
|
-
DockerContainerStatus.RUNNING.value,
|
|
238
|
-
DockerContainerStatus.BUSY.value,
|
|
239
|
-
]:
|
|
240
|
-
typer.echo(__(f'Container is not running (status: \'{container.frontend_status.status_translation}\')'))
|
|
241
|
-
raise typer.Exit(1)
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
@staticmethod
|
|
245
|
-
def _get_new_path_from_mapping(
|
|
246
|
-
directory_mapping: Dict[str, str],
|
|
247
|
-
destination_path: str,
|
|
248
|
-
) -> Tuple[Optional[str], Optional[str]]:
|
|
249
|
-
|
|
250
|
-
instance_path: Optional[str] = None
|
|
251
|
-
container_path: Optional[str] = None
|
|
252
|
-
|
|
253
|
-
for instance_mapping, container_mapping in directory_mapping.items():
|
|
254
|
-
if destination_path.startswith(f"{container_mapping}/") or destination_path == container_mapping:
|
|
255
|
-
instance_path = destination_path.replace(container_mapping, instance_mapping)
|
|
256
|
-
container_path = destination_path
|
|
257
|
-
# dont break, check all mapping list
|
|
258
|
-
|
|
259
|
-
if instance_path and container_path:
|
|
260
|
-
return instance_path, container_path
|
|
261
|
-
else:
|
|
262
|
-
return None, None
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
@error_handler()
|
|
266
|
-
def put_file_to_container(
|
|
267
|
-
self,
|
|
268
|
-
container: DockerContainerDto,
|
|
269
|
-
src_path: str,
|
|
270
|
-
copy_only_folder_contents: bool,
|
|
271
|
-
destination_path: Optional[str] = None,
|
|
272
|
-
username_param: Optional[str] = None,
|
|
273
|
-
):
|
|
274
|
-
if not self.__file_system_service.check_if_path_exist(file=src_path):
|
|
275
|
-
typer.echo(__("File not found at specified path"))
|
|
276
|
-
raise typer.Exit(1)
|
|
277
|
-
|
|
278
|
-
username, ip_address, private_key_path = self.get_server_auth(
|
|
279
|
-
container=container,
|
|
280
|
-
username_param=username_param,
|
|
281
|
-
private_key_path_override=None
|
|
282
|
-
)
|
|
283
|
-
|
|
284
|
-
if not container.mappings or not container.mappings.directory_mappings:
|
|
285
|
-
typer.echo(__("Mapping folders not found"))
|
|
286
|
-
raise typer.Exit(1)
|
|
287
|
-
|
|
288
|
-
instance_path, container_path = self._get_new_path_from_mapping(
|
|
289
|
-
directory_mapping=container.mappings.directory_mappings,
|
|
290
|
-
destination_path=destination_path,
|
|
291
|
-
)
|
|
292
|
-
|
|
293
|
-
if not instance_path and not container_path:
|
|
294
|
-
typer.echo(__("Cannot find matching container volume mapping for specified file path"))
|
|
295
|
-
raise typer.Exit(1)
|
|
296
|
-
|
|
297
|
-
self.__remote_server_service.upload_data_to_container(
|
|
298
|
-
ip_address=ip_address,
|
|
299
|
-
username=username,
|
|
300
|
-
src_path=src_path,
|
|
301
|
-
dest_path=destination_path,
|
|
302
|
-
instance_path=instance_path,
|
|
303
|
-
container_path=container_path,
|
|
304
|
-
copy_only_folder_contents=copy_only_folder_contents,
|
|
305
|
-
private_key_path=private_key_path,
|
|
306
|
-
)
|
|
307
|
-
|
|
308
|
-
@error_handler()
|
|
309
|
-
def get_file_from_container(
|
|
310
|
-
self,
|
|
311
|
-
container: DockerContainerDto,
|
|
312
|
-
src_path: str,
|
|
313
|
-
copy_only_folder_contents: bool,
|
|
314
|
-
destination_path: Optional[str] = None,
|
|
315
|
-
username_param: Optional[str] = None,
|
|
316
|
-
):
|
|
317
|
-
username, ip_address, private_key_path = self.get_server_auth(
|
|
318
|
-
container=container,
|
|
319
|
-
username_param=username_param,
|
|
320
|
-
private_key_path_override=None,
|
|
321
|
-
)
|
|
322
|
-
|
|
323
|
-
if not container.mappings or not container.mappings.directory_mappings:
|
|
324
|
-
typer.echo(__("Mapping folders not found"))
|
|
325
|
-
raise typer.Exit(1)
|
|
326
|
-
|
|
327
|
-
instance_path, container_path = self._get_new_path_from_mapping(
|
|
328
|
-
directory_mapping=container.mappings.directory_mappings,
|
|
329
|
-
destination_path=src_path,
|
|
330
|
-
)
|
|
331
|
-
|
|
332
|
-
if not instance_path and not container_path:
|
|
333
|
-
typer.echo(__("Cannot find matching container volume mapping for specified file path"))
|
|
334
|
-
raise typer.Exit(1)
|
|
335
|
-
|
|
336
|
-
self.__remote_server_service.download_data_from_container(
|
|
337
|
-
ip_address=ip_address,
|
|
338
|
-
username=username,
|
|
339
|
-
dest_path=destination_path,
|
|
340
|
-
instance_path=instance_path,
|
|
341
|
-
copy_only_folder_contents=copy_only_folder_contents,
|
|
342
|
-
private_key_path=private_key_path,
|
|
343
|
-
)
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
@error_handler()
|
|
347
|
-
def request_docker_container_action(
|
|
348
|
-
self,
|
|
349
|
-
container_uid: str,
|
|
350
|
-
action: DockerContainerAction,
|
|
351
|
-
):
|
|
352
|
-
container: Optional[DockerContainerDto] = self.get_container(
|
|
353
|
-
container_slug=container_uid,
|
|
354
|
-
)
|
|
355
|
-
if not container:
|
|
356
|
-
typer.echo(f"Container with unique ID '{container_uid}' not found")
|
|
357
|
-
raise typer.Exit(1)
|
|
358
|
-
|
|
359
|
-
if action == DockerContainerAction.START:
|
|
360
|
-
self.check_if_container_stopped(container=container)
|
|
361
|
-
|
|
362
|
-
if action in [DockerContainerAction.STOP, DockerContainerAction.RESTART]:
|
|
363
|
-
self.check_if_container_running(container=container)
|
|
364
|
-
|
|
365
|
-
request_params = DockerContainerActionRequestDto(
|
|
366
|
-
dockerContainerId=container.id,
|
|
367
|
-
action=action,
|
|
368
|
-
)
|
|
369
|
-
result = self.__thestage_api_client.container_action(
|
|
370
|
-
request_param=request_params,
|
|
371
|
-
)
|
|
372
|
-
|
|
373
|
-
if result.is_success:
|
|
374
|
-
typer.echo(f'Docker container action scheduled: {action.value}')
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
from typing import List, Tuple, Optional, Dict
|
|
3
|
+
|
|
4
|
+
import typer
|
|
5
|
+
from thestage.entities.container import DockerContainerEntity
|
|
6
|
+
from thestage.services.clients.thestage_api.dtos.container_param_request import DockerContainerActionRequestDto
|
|
7
|
+
from thestage.services.clients.thestage_api.dtos.enums.container_pending_action import DockerContainerAction
|
|
8
|
+
from thestage.services.clients.thestage_api.dtos.enums.container_status import DockerContainerStatus
|
|
9
|
+
from thestage.entities.enums.shell_type import ShellType
|
|
10
|
+
from thestage.services.clients.thestage_api.dtos.paginated_entity_list import PaginatedEntityList
|
|
11
|
+
from thestage.services.container.mapper.container_mapper import ContainerMapper
|
|
12
|
+
from thestage.services.filesystem_service import FileSystemService
|
|
13
|
+
from thestage.services.remote_server_service import RemoteServerService
|
|
14
|
+
from thestage.i18n.translation import __
|
|
15
|
+
from thestage.services.abstract_service import AbstractService
|
|
16
|
+
from thestage.services.clients.thestage_api.dtos.container_response import DockerContainerDto
|
|
17
|
+
from thestage.helpers.error_handler import error_handler
|
|
18
|
+
from thestage.services.clients.thestage_api.api_client import TheStageApiClient
|
|
19
|
+
from thestage.services.config_provider.config_provider import ConfigProvider
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class ContainerService(AbstractService):
|
|
23
|
+
|
|
24
|
+
__thestage_api_client: TheStageApiClient = None
|
|
25
|
+
__config_provider: ConfigProvider = None
|
|
26
|
+
|
|
27
|
+
def __init__(
|
|
28
|
+
self,
|
|
29
|
+
thestage_api_client: TheStageApiClient,
|
|
30
|
+
config_provider: ConfigProvider,
|
|
31
|
+
remote_server_service: RemoteServerService,
|
|
32
|
+
file_system_service: FileSystemService,
|
|
33
|
+
):
|
|
34
|
+
self.__config_provider = config_provider
|
|
35
|
+
self.__thestage_api_client = thestage_api_client
|
|
36
|
+
self.__remote_server_service = remote_server_service
|
|
37
|
+
self.__file_system_service = file_system_service
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@error_handler()
|
|
41
|
+
def print_container_list(
|
|
42
|
+
self,
|
|
43
|
+
row: int,
|
|
44
|
+
page: int,
|
|
45
|
+
project_uid: Optional[str],
|
|
46
|
+
statuses: List[str],
|
|
47
|
+
):
|
|
48
|
+
container_status_map = self.__thestage_api_client.get_container_business_status_map()
|
|
49
|
+
|
|
50
|
+
if not statuses:
|
|
51
|
+
statuses = ({key: container_status_map[key] for key in [
|
|
52
|
+
DockerContainerStatus.RUNNING,
|
|
53
|
+
DockerContainerStatus.STARTING,
|
|
54
|
+
]}).values()
|
|
55
|
+
|
|
56
|
+
if "all" in statuses:
|
|
57
|
+
statuses = container_status_map.values()
|
|
58
|
+
|
|
59
|
+
for input_status_item in statuses:
|
|
60
|
+
if input_status_item not in container_status_map.values():
|
|
61
|
+
typer.echo(__("'%invalid_status%' is not one of %valid_statuses%", {
|
|
62
|
+
'invalid_status': input_status_item,
|
|
63
|
+
'valid_statuses': str(list(container_status_map.values()))
|
|
64
|
+
}))
|
|
65
|
+
raise typer.Exit(1)
|
|
66
|
+
|
|
67
|
+
typer.echo(__(
|
|
68
|
+
"Listing containers with the following statuses: %statuses%. To list all containers, use --status all",
|
|
69
|
+
placeholders={
|
|
70
|
+
'statuses': ', '.join([input_status_item for input_status_item in statuses])
|
|
71
|
+
}))
|
|
72
|
+
|
|
73
|
+
backend_statuses: List[str] = [key for key, value in container_status_map.items() if value in statuses]
|
|
74
|
+
|
|
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
|
+
self.print(
|
|
81
|
+
func_get_data=self.get_list,
|
|
82
|
+
func_special_params={
|
|
83
|
+
'statuses': backend_statuses,
|
|
84
|
+
'project_id': project_id,
|
|
85
|
+
},
|
|
86
|
+
mapper=ContainerMapper(),
|
|
87
|
+
headers=list(map(lambda x: x.alias, DockerContainerEntity.model_fields.values())),
|
|
88
|
+
row=row,
|
|
89
|
+
page=page,
|
|
90
|
+
max_col_width=[15, 20, 25],
|
|
91
|
+
show_index="never",
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
@error_handler()
|
|
96
|
+
def get_list(
|
|
97
|
+
self,
|
|
98
|
+
statuses: List[str],
|
|
99
|
+
row: int = 5,
|
|
100
|
+
page: int = 1,
|
|
101
|
+
project_id: Optional[int] = None,
|
|
102
|
+
) -> PaginatedEntityList[DockerContainerDto]:
|
|
103
|
+
|
|
104
|
+
list = self.__thestage_api_client.get_container_list(
|
|
105
|
+
statuses=statuses,
|
|
106
|
+
page=page,
|
|
107
|
+
limit=row,
|
|
108
|
+
project_id=project_id,
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
return list
|
|
112
|
+
|
|
113
|
+
@error_handler()
|
|
114
|
+
def get_container(
|
|
115
|
+
self,
|
|
116
|
+
container_id: Optional[int] = None,
|
|
117
|
+
container_slug: Optional[str] = None,
|
|
118
|
+
) -> Optional[DockerContainerDto]:
|
|
119
|
+
return self.__thestage_api_client.get_container(
|
|
120
|
+
container_id=container_id,
|
|
121
|
+
container_slug=container_slug,
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
def get_server_auth(
|
|
125
|
+
self,
|
|
126
|
+
container: DockerContainerDto,
|
|
127
|
+
username_param: Optional[str],
|
|
128
|
+
private_key_path_override: Optional[str],
|
|
129
|
+
) -> Tuple[str, str, Optional[str]]:
|
|
130
|
+
username = None
|
|
131
|
+
if container.instance_rented:
|
|
132
|
+
username = container.instance_rented.host_username
|
|
133
|
+
ip_address = container.instance_rented.ip_address
|
|
134
|
+
elif container.selfhosted_instance:
|
|
135
|
+
ip_address = container.selfhosted_instance.ip_address
|
|
136
|
+
else:
|
|
137
|
+
typer.echo(__("Neither rented nor self-hosted server instance found to connect to"))
|
|
138
|
+
raise typer.Exit(1)
|
|
139
|
+
|
|
140
|
+
if username_param:
|
|
141
|
+
username = username_param
|
|
142
|
+
|
|
143
|
+
if not username:
|
|
144
|
+
username = 'root'
|
|
145
|
+
typer.echo(__("No remote server username provided, using 'root' as username"))
|
|
146
|
+
|
|
147
|
+
private_key_path = private_key_path_override
|
|
148
|
+
if not private_key_path:
|
|
149
|
+
private_key_path = self.__config_provider.get_valid_private_key_path_by_ip_address(ip_address)
|
|
150
|
+
if private_key_path:
|
|
151
|
+
typer.echo(f'Using configured private key for {ip_address}: {private_key_path}')
|
|
152
|
+
else:
|
|
153
|
+
typer.echo(f'Using SSH agent to connect to {ip_address}')
|
|
154
|
+
else:
|
|
155
|
+
self.__config_provider.update_remote_server_config_entry(ip_address, Path(private_key_path))
|
|
156
|
+
typer.echo(f'Updated private key path for {ip_address}: {private_key_path}')
|
|
157
|
+
|
|
158
|
+
return username, ip_address, private_key_path
|
|
159
|
+
|
|
160
|
+
@error_handler()
|
|
161
|
+
def connect_to_container(
|
|
162
|
+
self,
|
|
163
|
+
container_uid: str,
|
|
164
|
+
username: Optional[str],
|
|
165
|
+
input_ssh_key_path: Optional[str],
|
|
166
|
+
):
|
|
167
|
+
container: Optional[DockerContainerDto] = self.get_container(
|
|
168
|
+
container_slug=container_uid,
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
if not container:
|
|
172
|
+
typer.echo(f"Container with UID '{container_uid}' not found")
|
|
173
|
+
raise typer.Exit(1)
|
|
174
|
+
|
|
175
|
+
self.check_if_container_running(
|
|
176
|
+
container=container
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
if not container.system_name:
|
|
180
|
+
typer.echo(__("Unable to connect to container: container system_name is missing"))
|
|
181
|
+
raise typer.Exit(1)
|
|
182
|
+
|
|
183
|
+
starting_directory: str = '/'
|
|
184
|
+
workspace_mappings = {v for v in container.mappings.directory_mappings.values() if v.startswith('/workspace/') or v == '/workspace'}
|
|
185
|
+
if len(workspace_mappings) > 0:
|
|
186
|
+
starting_directory = '/workspace'
|
|
187
|
+
|
|
188
|
+
inference_mappings = {v for v in container.mappings.directory_mappings.values() if v.startswith('/opt/') or v == '/opt'}
|
|
189
|
+
if len(inference_mappings) > 0:
|
|
190
|
+
starting_directory = '/opt/project'
|
|
191
|
+
|
|
192
|
+
username, ip_address, private_key_path = self.get_server_auth(
|
|
193
|
+
container=container,
|
|
194
|
+
username_param=username,
|
|
195
|
+
private_key_path_override=input_ssh_key_path
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
shell: Optional[ShellType] = self.__remote_server_service.get_shell_from_container(
|
|
199
|
+
ip_address=ip_address,
|
|
200
|
+
username=username,
|
|
201
|
+
container_name=container.system_name,
|
|
202
|
+
private_key_path=private_key_path
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
if not shell:
|
|
206
|
+
typer.echo(f"Failed to start shell (bash, sh) in container: ensure user '{username}' has Docker access and compatible shell is available")
|
|
207
|
+
raise typer.Exit(1)
|
|
208
|
+
|
|
209
|
+
self.__remote_server_service.connect_to_container(
|
|
210
|
+
ip_address=ip_address,
|
|
211
|
+
username=username,
|
|
212
|
+
docker_name=container.system_name,
|
|
213
|
+
starting_directory=starting_directory,
|
|
214
|
+
shell=shell,
|
|
215
|
+
private_key_path=private_key_path
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
@error_handler()
|
|
219
|
+
def check_if_container_stopped(
|
|
220
|
+
self,
|
|
221
|
+
container: DockerContainerDto,
|
|
222
|
+
) -> DockerContainerDto:
|
|
223
|
+
if container.frontend_status.status_key not in [
|
|
224
|
+
DockerContainerStatus.STOPPED.value,
|
|
225
|
+
]:
|
|
226
|
+
typer.echo(__(f'Container is not stopped (status: \'{container.frontend_status.status_translation}\')'))
|
|
227
|
+
raise typer.Exit(1)
|
|
228
|
+
|
|
229
|
+
return container
|
|
230
|
+
|
|
231
|
+
@error_handler()
|
|
232
|
+
def check_if_container_running(
|
|
233
|
+
self,
|
|
234
|
+
container: DockerContainerDto,
|
|
235
|
+
):
|
|
236
|
+
if container.frontend_status.status_key not in [
|
|
237
|
+
DockerContainerStatus.RUNNING.value,
|
|
238
|
+
DockerContainerStatus.BUSY.value,
|
|
239
|
+
]:
|
|
240
|
+
typer.echo(__(f'Container is not running (status: \'{container.frontend_status.status_translation}\')'))
|
|
241
|
+
raise typer.Exit(1)
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
@staticmethod
|
|
245
|
+
def _get_new_path_from_mapping(
|
|
246
|
+
directory_mapping: Dict[str, str],
|
|
247
|
+
destination_path: str,
|
|
248
|
+
) -> Tuple[Optional[str], Optional[str]]:
|
|
249
|
+
|
|
250
|
+
instance_path: Optional[str] = None
|
|
251
|
+
container_path: Optional[str] = None
|
|
252
|
+
|
|
253
|
+
for instance_mapping, container_mapping in directory_mapping.items():
|
|
254
|
+
if destination_path.startswith(f"{container_mapping}/") or destination_path == container_mapping:
|
|
255
|
+
instance_path = destination_path.replace(container_mapping, instance_mapping)
|
|
256
|
+
container_path = destination_path
|
|
257
|
+
# dont break, check all mapping list
|
|
258
|
+
|
|
259
|
+
if instance_path and container_path:
|
|
260
|
+
return instance_path, container_path
|
|
261
|
+
else:
|
|
262
|
+
return None, None
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
@error_handler()
|
|
266
|
+
def put_file_to_container(
|
|
267
|
+
self,
|
|
268
|
+
container: DockerContainerDto,
|
|
269
|
+
src_path: str,
|
|
270
|
+
copy_only_folder_contents: bool,
|
|
271
|
+
destination_path: Optional[str] = None,
|
|
272
|
+
username_param: Optional[str] = None,
|
|
273
|
+
):
|
|
274
|
+
if not self.__file_system_service.check_if_path_exist(file=src_path):
|
|
275
|
+
typer.echo(__("File not found at specified path"))
|
|
276
|
+
raise typer.Exit(1)
|
|
277
|
+
|
|
278
|
+
username, ip_address, private_key_path = self.get_server_auth(
|
|
279
|
+
container=container,
|
|
280
|
+
username_param=username_param,
|
|
281
|
+
private_key_path_override=None
|
|
282
|
+
)
|
|
283
|
+
|
|
284
|
+
if not container.mappings or not container.mappings.directory_mappings:
|
|
285
|
+
typer.echo(__("Mapping folders not found"))
|
|
286
|
+
raise typer.Exit(1)
|
|
287
|
+
|
|
288
|
+
instance_path, container_path = self._get_new_path_from_mapping(
|
|
289
|
+
directory_mapping=container.mappings.directory_mappings,
|
|
290
|
+
destination_path=destination_path,
|
|
291
|
+
)
|
|
292
|
+
|
|
293
|
+
if not instance_path and not container_path:
|
|
294
|
+
typer.echo(__("Cannot find matching container volume mapping for specified file path"))
|
|
295
|
+
raise typer.Exit(1)
|
|
296
|
+
|
|
297
|
+
self.__remote_server_service.upload_data_to_container(
|
|
298
|
+
ip_address=ip_address,
|
|
299
|
+
username=username,
|
|
300
|
+
src_path=src_path,
|
|
301
|
+
dest_path=destination_path,
|
|
302
|
+
instance_path=instance_path,
|
|
303
|
+
container_path=container_path,
|
|
304
|
+
copy_only_folder_contents=copy_only_folder_contents,
|
|
305
|
+
private_key_path=private_key_path,
|
|
306
|
+
)
|
|
307
|
+
|
|
308
|
+
@error_handler()
|
|
309
|
+
def get_file_from_container(
|
|
310
|
+
self,
|
|
311
|
+
container: DockerContainerDto,
|
|
312
|
+
src_path: str,
|
|
313
|
+
copy_only_folder_contents: bool,
|
|
314
|
+
destination_path: Optional[str] = None,
|
|
315
|
+
username_param: Optional[str] = None,
|
|
316
|
+
):
|
|
317
|
+
username, ip_address, private_key_path = self.get_server_auth(
|
|
318
|
+
container=container,
|
|
319
|
+
username_param=username_param,
|
|
320
|
+
private_key_path_override=None,
|
|
321
|
+
)
|
|
322
|
+
|
|
323
|
+
if not container.mappings or not container.mappings.directory_mappings:
|
|
324
|
+
typer.echo(__("Mapping folders not found"))
|
|
325
|
+
raise typer.Exit(1)
|
|
326
|
+
|
|
327
|
+
instance_path, container_path = self._get_new_path_from_mapping(
|
|
328
|
+
directory_mapping=container.mappings.directory_mappings,
|
|
329
|
+
destination_path=src_path,
|
|
330
|
+
)
|
|
331
|
+
|
|
332
|
+
if not instance_path and not container_path:
|
|
333
|
+
typer.echo(__("Cannot find matching container volume mapping for specified file path"))
|
|
334
|
+
raise typer.Exit(1)
|
|
335
|
+
|
|
336
|
+
self.__remote_server_service.download_data_from_container(
|
|
337
|
+
ip_address=ip_address,
|
|
338
|
+
username=username,
|
|
339
|
+
dest_path=destination_path,
|
|
340
|
+
instance_path=instance_path,
|
|
341
|
+
copy_only_folder_contents=copy_only_folder_contents,
|
|
342
|
+
private_key_path=private_key_path,
|
|
343
|
+
)
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
@error_handler()
|
|
347
|
+
def request_docker_container_action(
|
|
348
|
+
self,
|
|
349
|
+
container_uid: str,
|
|
350
|
+
action: DockerContainerAction,
|
|
351
|
+
):
|
|
352
|
+
container: Optional[DockerContainerDto] = self.get_container(
|
|
353
|
+
container_slug=container_uid,
|
|
354
|
+
)
|
|
355
|
+
if not container:
|
|
356
|
+
typer.echo(f"Container with unique ID '{container_uid}' not found")
|
|
357
|
+
raise typer.Exit(1)
|
|
358
|
+
|
|
359
|
+
if action == DockerContainerAction.START:
|
|
360
|
+
self.check_if_container_stopped(container=container)
|
|
361
|
+
|
|
362
|
+
if action in [DockerContainerAction.STOP, DockerContainerAction.RESTART]:
|
|
363
|
+
self.check_if_container_running(container=container)
|
|
364
|
+
|
|
365
|
+
request_params = DockerContainerActionRequestDto(
|
|
366
|
+
dockerContainerId=container.id,
|
|
367
|
+
action=action,
|
|
368
|
+
)
|
|
369
|
+
result = self.__thestage_api_client.container_action(
|
|
370
|
+
request_param=request_params,
|
|
371
|
+
)
|
|
372
|
+
|
|
373
|
+
if result.is_success:
|
|
374
|
+
typer.echo(f'Docker container action scheduled: {action.value}')
|