thestage 0.6.1__py3-none-any.whl → 0.6.3__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (177) hide show
  1. thestage/.env +4 -5
  2. thestage/__init__.py +3 -3
  3. thestage/__main__.py +9 -9
  4. thestage/cli_command.py +56 -56
  5. thestage/cli_command_helper.py +51 -51
  6. thestage/color_scheme/color_scheme.py +7 -7
  7. thestage/config/__init__.py +18 -18
  8. thestage/config/config_storage.py +5 -5
  9. thestage/config/env_base.py +7 -7
  10. thestage/controllers/__init__.py +0 -0
  11. thestage/controllers/base_controller.py +67 -67
  12. thestage/controllers/config_controller.py +137 -137
  13. thestage/controllers/container_controller.py +389 -389
  14. thestage/controllers/instance_controller.py +183 -183
  15. thestage/controllers/project_controller.py +810 -802
  16. thestage/controllers/utils_controller.py +32 -32
  17. thestage/debug_main.dist.py +28 -28
  18. thestage/entities/__init__.py +0 -0
  19. thestage/entities/container.py +17 -17
  20. thestage/entities/enums/__init__.py +0 -0
  21. thestage/entities/enums/order_direction_type.py +6 -6
  22. thestage/entities/enums/shell_type.py +7 -7
  23. thestage/entities/enums/tail_output_type.py +6 -6
  24. thestage/entities/enums/yes_no_response.py +7 -7
  25. thestage/entities/file_item.py +27 -27
  26. thestage/entities/project_inference_simulator.py +18 -18
  27. thestage/entities/project_inference_simulator_model.py +16 -16
  28. thestage/entities/project_task.py +19 -19
  29. thestage/entities/rented_instance.py +19 -19
  30. thestage/entities/self_hosted_instance.py +18 -18
  31. thestage/exceptions/__init__.py +0 -0
  32. thestage/exceptions/auth_exception.py +6 -6
  33. thestage/exceptions/base_exception.py +13 -13
  34. thestage/exceptions/business_logic_exception.py +6 -6
  35. thestage/exceptions/config_exception.py +6 -6
  36. thestage/exceptions/file_system_exception.py +6 -6
  37. thestage/exceptions/git_access_exception.py +17 -17
  38. thestage/exceptions/remote_server_exception.py +24 -24
  39. thestage/git/ProgressPrinter.py +22 -22
  40. thestage/helpers/__init__.py +0 -0
  41. thestage/helpers/error_handler.py +115 -115
  42. thestage/helpers/exception_hook.py +14 -14
  43. thestage/helpers/logger/__init__.py +0 -0
  44. thestage/helpers/logger/app_logger.py +50 -50
  45. thestage/helpers/ssh_util.py +38 -38
  46. thestage/i18n/en_GB/messages.po +947 -947
  47. thestage/i18n/translation.py +9 -9
  48. thestage/main.py +36 -36
  49. thestage/services/.env +6 -6
  50. thestage/services/__init__.py +0 -0
  51. thestage/services/abstract_mapper.py +9 -9
  52. thestage/services/abstract_service.py +87 -87
  53. thestage/services/app_config_service.py +52 -52
  54. thestage/services/clients/__init__.py +0 -0
  55. thestage/services/clients/git/__init__.py +0 -0
  56. thestage/services/clients/git/git_client.py +436 -433
  57. thestage/services/clients/thestage_api/__init__.py +0 -0
  58. thestage/services/clients/thestage_api/api_client.py +718 -718
  59. thestage/services/clients/thestage_api/core/api_client_core.py +108 -108
  60. thestage/services/clients/thestage_api/core/http_client_exception.py +12 -12
  61. thestage/services/clients/thestage_api/dtos/__init__.py +0 -0
  62. thestage/services/clients/thestage_api/dtos/base_response.py +13 -13
  63. thestage/services/clients/thestage_api/dtos/cloud_provider_region.py +19 -19
  64. thestage/services/clients/thestage_api/dtos/container_param_request.py +11 -11
  65. thestage/services/clients/thestage_api/dtos/container_response.py +67 -67
  66. thestage/services/clients/thestage_api/dtos/docker_container_assigned_device.py +10 -10
  67. thestage/services/clients/thestage_api/dtos/docker_container_controller/docker_container_list_request.py +13 -13
  68. thestage/services/clients/thestage_api/dtos/docker_container_controller/docker_container_list_response.py +13 -13
  69. thestage/services/clients/thestage_api/dtos/docker_container_mapping.py +10 -10
  70. thestage/services/clients/thestage_api/dtos/entity_filter_request.py +14 -14
  71. thestage/services/clients/thestage_api/dtos/enums/__init__.py +0 -0
  72. thestage/services/clients/thestage_api/dtos/enums/container_pending_action.py +10 -10
  73. thestage/services/clients/thestage_api/dtos/enums/container_status.py +17 -17
  74. thestage/services/clients/thestage_api/dtos/enums/cpu_type.py +8 -8
  75. thestage/services/clients/thestage_api/dtos/enums/currency_type.py +10 -10
  76. thestage/services/clients/thestage_api/dtos/enums/daemon_status.py +9 -9
  77. thestage/services/clients/thestage_api/dtos/enums/disk_type.py +7 -7
  78. thestage/services/clients/thestage_api/dtos/enums/drive_type.py +7 -7
  79. thestage/services/clients/thestage_api/dtos/enums/gpu_name.py +8 -8
  80. thestage/services/clients/thestage_api/dtos/enums/inference_model_status.py +9 -9
  81. thestage/services/clients/thestage_api/dtos/enums/inference_simulator_status.py +15 -15
  82. thestage/services/clients/thestage_api/dtos/enums/instance_rented_status.py +17 -17
  83. thestage/services/clients/thestage_api/dtos/enums/instance_type.py +7 -7
  84. thestage/services/clients/thestage_api/dtos/enums/location_region.py +11 -11
  85. thestage/services/clients/thestage_api/dtos/enums/power_status.py +10 -10
  86. thestage/services/clients/thestage_api/dtos/enums/provider_name.py +11 -11
  87. thestage/services/clients/thestage_api/dtos/enums/selfhosted_status.py +10 -10
  88. thestage/services/clients/thestage_api/dtos/enums/task_execution_status.py +12 -12
  89. thestage/services/clients/thestage_api/dtos/enums/task_status.py +12 -12
  90. thestage/services/clients/thestage_api/dtos/frontend_status.py +10 -10
  91. thestage/services/clients/thestage_api/dtos/inference_controller/deploy_inference_model_to_instance_request.py +13 -13
  92. thestage/services/clients/thestage_api/dtos/inference_controller/deploy_inference_model_to_instance_response.py +13 -13
  93. thestage/services/clients/thestage_api/dtos/inference_controller/deploy_inference_model_to_sagemaker_request.py +12 -12
  94. thestage/services/clients/thestage_api/dtos/inference_controller/deploy_inference_model_to_sagemaker_response.py +12 -12
  95. thestage/services/clients/thestage_api/dtos/inference_controller/get_inference_simulator_request.py +10 -10
  96. thestage/services/clients/thestage_api/dtos/inference_controller/get_inference_simulator_response.py +13 -13
  97. thestage/services/clients/thestage_api/dtos/inference_controller/inference_simulator_list_for_project_request.py +14 -14
  98. thestage/services/clients/thestage_api/dtos/inference_controller/inference_simulator_list_for_project_response.py +12 -12
  99. thestage/services/clients/thestage_api/dtos/inference_controller/inference_simulator_model_list_for_project_request.py +12 -12
  100. thestage/services/clients/thestage_api/dtos/inference_controller/inference_simulator_model_list_for_project_response.py +13 -13
  101. thestage/services/clients/thestage_api/dtos/inference_simulator_model_response.py +11 -11
  102. thestage/services/clients/thestage_api/dtos/inference_simulator_response.py +11 -11
  103. thestage/services/clients/thestage_api/dtos/installed_service.py +17 -17
  104. thestage/services/clients/thestage_api/dtos/instance_detected_gpus.py +20 -20
  105. thestage/services/clients/thestage_api/dtos/instance_rented_response.py +71 -71
  106. thestage/services/clients/thestage_api/dtos/logging_controller/docker_container_log_stream_request.py +7 -7
  107. thestage/services/clients/thestage_api/dtos/logging_controller/log_polling_request.py +13 -13
  108. thestage/services/clients/thestage_api/dtos/logging_controller/log_polling_response.py +14 -14
  109. thestage/services/clients/thestage_api/dtos/logging_controller/task_log_stream_request.py +7 -7
  110. thestage/services/clients/thestage_api/dtos/logging_controller/user_logs_query_request.py +21 -21
  111. thestage/services/clients/thestage_api/dtos/logging_controller/user_logs_query_response.py +14 -14
  112. thestage/services/clients/thestage_api/dtos/paginated_entity_list.py +11 -11
  113. thestage/services/clients/thestage_api/dtos/pagination_data.py +10 -10
  114. thestage/services/clients/thestage_api/dtos/price_definition.py +14 -14
  115. thestage/services/clients/thestage_api/dtos/project_controller/project_get_deploy_ssh_key_request.py +7 -7
  116. thestage/services/clients/thestage_api/dtos/project_controller/project_get_deploy_ssh_key_response.py +10 -10
  117. thestage/services/clients/thestage_api/dtos/project_controller/project_push_inference_simulator_model_request.py +8 -8
  118. thestage/services/clients/thestage_api/dtos/project_controller/project_push_inference_simulator_model_response.py +6 -6
  119. thestage/services/clients/thestage_api/dtos/project_controller/project_run_task_request.py +15 -15
  120. thestage/services/clients/thestage_api/dtos/project_controller/project_run_task_response.py +10 -10
  121. thestage/services/clients/thestage_api/dtos/project_controller/project_start_inference_simulator_request.py +13 -13
  122. thestage/services/clients/thestage_api/dtos/project_controller/project_start_inference_simulator_response.py +10 -10
  123. thestage/services/clients/thestage_api/dtos/project_response.py +32 -32
  124. thestage/services/clients/thestage_api/dtos/selfhosted_instance_response.py +56 -56
  125. thestage/services/clients/thestage_api/dtos/sftp_path_helper.py +13 -13
  126. thestage/services/clients/thestage_api/dtos/ssh_key_controller/add_ssh_key_to_user_request.py +8 -8
  127. thestage/services/clients/thestage_api/dtos/ssh_key_controller/add_ssh_key_to_user_response.py +11 -11
  128. thestage/services/clients/thestage_api/dtos/ssh_key_controller/add_ssh_public_key_to_instance_request.py +8 -8
  129. thestage/services/clients/thestage_api/dtos/ssh_key_controller/add_ssh_public_key_to_instance_response.py +11 -11
  130. thestage/services/clients/thestage_api/dtos/ssh_key_controller/is_user_has_public_ssh_key_request.py +7 -7
  131. thestage/services/clients/thestage_api/dtos/ssh_key_controller/is_user_has_public_ssh_key_response.py +12 -12
  132. thestage/services/clients/thestage_api/dtos/task_controller/task_list_for_project_request.py +10 -10
  133. thestage/services/clients/thestage_api/dtos/task_controller/task_list_for_project_response.py +12 -12
  134. thestage/services/clients/thestage_api/dtos/task_controller/task_status_localized_map_response.py +9 -9
  135. thestage/services/clients/thestage_api/dtos/task_controller/task_view_response.py +12 -12
  136. thestage/services/clients/thestage_api/dtos/user_controller/user_profile.py +12 -12
  137. thestage/services/clients/thestage_api/dtos/validate_token_response.py +11 -11
  138. thestage/services/config_provider/__init__.py +0 -0
  139. thestage/services/config_provider/config_provider.py +237 -237
  140. thestage/services/connect/connect_service.py +193 -196
  141. thestage/services/connect/dto/remote_server_config.py +9 -9
  142. thestage/services/container/__init__.py +0 -0
  143. thestage/services/container/container_service.py +374 -374
  144. thestage/services/container/mapper/__init__.py +0 -0
  145. thestage/services/container/mapper/container_mapper.py +30 -30
  146. thestage/services/core_files/config_entity.py +26 -26
  147. thestage/services/filesystem_service.py +133 -133
  148. thestage/services/instance/__init__.py +0 -0
  149. thestage/services/instance/instance_service.py +303 -303
  150. thestage/services/instance/mapper/__init__.py +0 -0
  151. thestage/services/instance/mapper/instance_mapper.py +24 -24
  152. thestage/services/instance/mapper/selfhosted_mapper.py +33 -33
  153. thestage/services/logging/byte_print_style.py +5 -5
  154. thestage/services/logging/dto/log_message.py +15 -15
  155. thestage/services/logging/dto/log_type.py +6 -6
  156. thestage/services/logging/exception/log_polling_exception.py +6 -6
  157. thestage/services/logging/logging_constants.py +3 -3
  158. thestage/services/logging/logging_service.py +367 -367
  159. thestage/services/project/__init__.py +0 -0
  160. thestage/services/project/dto/inference_simulator_dto.py +22 -22
  161. thestage/services/project/dto/inference_simulator_model_dto.py +20 -20
  162. thestage/services/project/dto/project_config.py +14 -14
  163. thestage/services/project/mapper/__init__.py +0 -0
  164. thestage/services/project/mapper/project_inference_simulator_mapper.py +21 -21
  165. thestage/services/project/mapper/project_inference_simulator_model_mapper.py +21 -21
  166. thestage/services/project/mapper/project_task_mapper.py +22 -22
  167. thestage/services/project/project_service.py +1253 -1260
  168. thestage/services/remote_server_service.py +609 -609
  169. thestage/services/service_factory.py +97 -97
  170. thestage/services/task/dto/task_dto.py +40 -40
  171. thestage/services/validation_service.py +61 -61
  172. {thestage-0.6.1.dist-info → thestage-0.6.3.dist-info}/LICENSE.txt +12 -12
  173. {thestage-0.6.1.dist-info → thestage-0.6.3.dist-info}/METADATA +3 -2
  174. thestage-0.6.3.dist-info/RECORD +176 -0
  175. {thestage-0.6.1.dist-info → thestage-0.6.3.dist-info}/WHEEL +1 -1
  176. thestage-0.6.1.dist-info/RECORD +0 -176
  177. {thestage-0.6.1.dist-info → thestage-0.6.3.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}')