thestage 0.5.471__py3-none-any.whl → 0.6.2__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 (179) hide show
  1. thestage/.env +5 -5
  2. thestage/__init__.py +3 -4
  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 -783
  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 -331
  57. thestage/services/clients/thestage_api/__init__.py +0 -0
  58. thestage/services/clients/thestage_api/api_client.py +718 -720
  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 -14
  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 +196 -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 -1241
  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.5.471.dist-info → thestage-0.6.2.dist-info}/LICENSE.txt +12 -12
  173. {thestage-0.5.471.dist-info → thestage-0.6.2.dist-info}/METADATA +1 -1
  174. thestage-0.6.2.dist-info/RECORD +176 -0
  175. {thestage-0.5.471.dist-info → thestage-0.6.2.dist-info}/WHEEL +1 -1
  176. thestage/debug_tests.py +0 -12
  177. thestage/services/clients/.DS_Store +0 -0
  178. thestage-0.5.471.dist-info/RECORD +0 -178
  179. {thestage-0.5.471.dist-info → thestage-0.6.2.dist-info}/entry_points.txt +0 -0
@@ -1,609 +1,609 @@
1
- import os
2
- import stat
3
- from pathlib import Path
4
- from time import sleep
5
- from typing import Optional, List, Tuple
6
-
7
- import math
8
- import paramiko
9
- import typer
10
- from click import Abort
11
- from paramiko.client import SSHClient
12
- from paramiko.pkey import PKey
13
- from paramiko.sftp_client import SFTPClient
14
-
15
- from thestage.entities.file_item import FileItemEntity
16
- from thestage.services.core_files.config_entity import ConfigEntity
17
- from thestage.exceptions.remote_server_exception import RemoteServerException
18
- from thestage.helpers.logger.app_logger import app_logger
19
- from thestage.entities.enums.shell_type import ShellType
20
- from thestage.helpers.ssh_util import parse_private_key
21
- from thestage.i18n.translation import __
22
- from thestage.services.clients.thestage_api.dtos.sftp_path_helper import SftpFileItemEntity
23
- from thestage.services.config_provider.config_provider import ConfigProvider
24
- from thestage.services.filesystem_service import FileSystemService
25
-
26
- old_value: int = 0
27
-
28
-
29
- class RemoteServerService:
30
- __config_provider: ConfigProvider = None
31
- __file_system_service: FileSystemService = None
32
-
33
- def __init__(
34
- self,
35
- file_system_service: FileSystemService,
36
- config_provider: ConfigProvider,
37
- ):
38
- self.__file_system_service = file_system_service
39
- self.__config_provider = config_provider
40
-
41
- def __get_client(
42
- self,
43
- ip_address: str,
44
- username: str,
45
- private_key_path: Optional[str],
46
- ) -> Optional[SSHClient]:
47
- pkey: Optional[PKey] = None
48
- if private_key_path:
49
- pkey = parse_private_key(private_key_path)
50
- if pkey is None:
51
- typer.echo("Could not identify provided private key (expected RSA, ECDSA, ED25519)")
52
- raise typer.Exit(1)
53
-
54
- client = SSHClient()
55
- try:
56
- client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
57
- client.connect(
58
- hostname=ip_address,
59
- username=username,
60
- timeout=6,
61
- # key_filename=private_key_path, # key_filename is buggy for certain algorithms/systems apparently
62
- pkey=pkey,
63
- allow_agent=private_key_path is None,
64
- look_for_keys=private_key_path is None,
65
- )
66
- return client
67
-
68
- except Exception as ex:
69
- if client:
70
- client.close()
71
- typer.echo(f"Error connecting to {ip_address} as {username} ({ex})")
72
- app_logger.error(f"Error connecting to {ip_address} as {username} ({ex})")
73
- raise RemoteServerException(
74
- message=__("Unable to connect to remote server"),
75
- ip_address=ip_address,
76
- username=username,
77
- )
78
-
79
-
80
- def get_shell_from_container(
81
- self,
82
- ip_address: str,
83
- username: str,
84
- container_name: str,
85
- private_key_path: Optional[str],
86
- ) -> Optional[ShellType]:
87
- client: Optional[SSHClient] = self.__get_client(
88
- ip_address=ip_address,
89
- username=username,
90
- private_key_path=private_key_path,
91
- )
92
- stdin, stdout, stderr = client.exec_command(f'docker exec -it {container_name} cat /etc/shells', get_pty=True)
93
- shell: Optional[ShellType] = None
94
- stdout_lines: List[str] = []
95
-
96
- for line in stdout.readlines():
97
- stdout_lines.append(line.rstrip())
98
- if 'bin/bash' in line:
99
- shell = ShellType.BASH
100
- break
101
- if 'bin/sh' in line:
102
- shell = ShellType.SH
103
- break
104
- client.close()
105
-
106
- if shell is None:
107
- for line_item in stdout_lines:
108
- typer.echo(line_item)
109
-
110
- return shell
111
-
112
-
113
- def connect_to_instance(
114
- self,
115
- ip_address: str,
116
- username: str,
117
- private_key_path: Optional[str],
118
- ):
119
- try:
120
- if private_key_path:
121
- os.system(f"ssh -o PreferredAuthentications=publickey -o 'IdentitiesOnly=yes' -i {private_key_path} {username}@{ip_address}")
122
- else:
123
- os.system(f"ssh -o PreferredAuthentications=publickey {username}@{ip_address}")
124
- except Abort as e1:
125
- return
126
- except Exception as ex:
127
- app_logger.error(f"Error connecting to {ip_address} as {username} ({ex})")
128
- raise RemoteServerException(
129
- message=__("Unable to connect to remote server"),
130
- ip_address=ip_address,
131
- username=username,
132
- )
133
-
134
-
135
- def connect_to_container(
136
- self,
137
- ip_address: str,
138
- username: str,
139
- docker_name: str,
140
- starting_directory: str,
141
- shell: ShellType,
142
- private_key_path: Optional[str],
143
- ):
144
- try:
145
- if private_key_path:
146
- os.system(f"ssh -tt -o PreferredAuthentications=publickey -o 'IdentitiesOnly=yes' -i {private_key_path} {username}@{ip_address} 'docker exec -it {docker_name} sh -c \"cd {starting_directory} && {shell.value}\"'")
147
- else:
148
- os.system(f"ssh -tt {username}@{ip_address} 'docker exec -it {docker_name} sh -c \"cd {starting_directory} && {shell.value}\"'")
149
- except Exception as ex:
150
- app_logger.exception(f"Error connecting to {ip_address} as {username} ({ex})")
151
- raise RemoteServerException(
152
- message=__("Unable to connect to remote server"),
153
- ip_address=ip_address,
154
- username=username,
155
- )
156
-
157
-
158
- def __upload_one_file(
159
- self,
160
- sftp: SFTPClient,
161
- src_path: str,
162
- dest_path: str,
163
- file_name: str,
164
- container_path: str,
165
- file_size: [int] = 100,
166
- ) -> bool:
167
- has_error = False
168
- try:
169
- with typer.progressbar(length=file_size, label=__("Uploading %file_name% (%file_size%)", {'file_name': file_name, 'file_size': self.__convert_size(file_size)})) as progress:
170
- def __show_result_copy(size: int, full_size: int):
171
- global old_value
172
- progress.update(size - (old_value or 0))
173
- old_value = size
174
- if old_value == full_size:
175
- old_value = 0
176
- sftp.put(localpath=src_path, remotepath=f"{dest_path}", callback=__show_result_copy)
177
- typer.echo(__('Uploaded to container as %file_path%', {'file_path': container_path}))
178
- except FileNotFoundError as err:
179
- app_logger.exception(f"Error uploading file {file_name} to container (file not found): {err}")
180
- typer.echo(__("Error uploading file: file not found on server"))
181
- has_error = True
182
- except Exception as err2:
183
- typer.echo(err2)
184
- app_logger.exception(f"Error uploading file {file_name} to container: {err2}")
185
- typer.echo(__("Error uploading file: undefined server error"))
186
- has_error = True
187
-
188
- return has_error
189
-
190
- def __make_dirs_by_sftp(
191
- self,
192
- sftp: SFTPClient,
193
- path: str,
194
- ):
195
- full_path = ''
196
- for item in path.split('/'):
197
- if item == '':
198
- continue
199
- try:
200
- full_path += f'/{item}'
201
- sftp.chdir(full_path) # Test if remote_path exists
202
- except IOError:
203
- sftp.mkdir(full_path) # Create remote_path
204
- sftp.chdir(full_path)
205
-
206
-
207
- def __upload_list_files(
208
- self,
209
- sftp: SFTPClient,
210
- src_item: SftpFileItemEntity,
211
- ):
212
- if src_item.is_file:
213
-
214
- get_parent_path = '/'.join(src_item.instance_path.split('/')[0:-1])
215
- self.__make_dirs_by_sftp(sftp=sftp, path=get_parent_path)
216
-
217
- self.__upload_one_file(
218
- sftp=sftp,
219
- src_path=src_item.path,
220
- dest_path=src_item.instance_path,
221
- file_name=src_item.name,
222
- file_size=src_item.file_size,
223
- container_path=src_item.container_path
224
- )
225
- elif src_item.is_folder:
226
- self.__make_dirs_by_sftp(sftp=sftp, path=src_item.instance_path)
227
- for item in src_item.children:
228
- self.__upload_list_files(
229
- sftp=sftp,
230
- src_item=item,
231
- )
232
-
233
-
234
- def __convert_size(self, size_bytes):
235
- if size_bytes == 0:
236
- return "0B"
237
- size_name = ("B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB")
238
- i = int(math.floor(math.log(size_bytes, 1024)))
239
- p = math.pow(1024, i)
240
- s = round(size_bytes / p, 2)
241
- return "%s %s" % (s, size_name[i])
242
-
243
-
244
- def __download_one_file(
245
- self,
246
- sftp: SFTPClient,
247
- src_path: str,
248
- dest_path: str,
249
- file_name: str,
250
- file_size: [int] = 100
251
- ) -> bool:
252
- has_error = False
253
- try:
254
- with typer.progressbar(length=file_size, label=__("Downloading %file_name% (%file_size%)", {'file_name': file_name, 'file_size': self.__convert_size(file_size)})) as progress:
255
- def __show_result_copy(size: int, full_size: int):
256
- global old_value
257
- progress.update(size - (old_value or 0))
258
- old_value = size
259
- if old_value == full_size:
260
- old_value = 0
261
- sftp.get(remotepath=src_path, localpath=f"{dest_path}", callback=__show_result_copy)
262
- typer.echo(__('Downloaded as %file_path%', {'file_path': dest_path}))
263
- except FileNotFoundError as err:
264
- app_logger.exception(f"Error retrieving file {file_name} from container (file not found): {err}")
265
- typer.echo(__("Error retrieving file: file not found on server"))
266
- has_error = True
267
- except Exception as err2:
268
- typer.echo(err2)
269
- app_logger.exception(f"Error retrieving file {file_name} from container: {err2}")
270
- typer.echo(__("Error retrieving file: undefined server error"))
271
- has_error = True
272
- return has_error
273
-
274
-
275
- def __download_list_files(
276
- self,
277
- sftp: SFTPClient,
278
- src_item: SftpFileItemEntity,
279
- ):
280
- if src_item.is_file:
281
- self.__file_system_service.get_path('/'.join(src_item.dest_path.split('/')[0:-1]), auto_create=True)
282
- self.__download_one_file(
283
- sftp=sftp,
284
- src_path=src_item.path,
285
- dest_path=src_item.dest_path,
286
- file_name=src_item.name,
287
- file_size=src_item.file_size,
288
- )
289
- elif src_item.is_folder:
290
- self.__file_system_service.get_path(str(Path(src_item.dest_path)), auto_create=True)
291
- for item in src_item.children:
292
- self.__download_list_files(
293
- sftp=sftp,
294
- src_item=item,
295
- #dest_path=src_item.dest_path,
296
- )
297
-
298
- @staticmethod
299
- def find_sftp_server_path(
300
- client: SSHClient,
301
- ) -> Optional[str]:
302
- stdin, stdout, stderr = client.exec_command(f'whereis sftp-server', get_pty=True)
303
- for line in stdout.readlines():
304
- pre_line = line.replace('sftp-server:', '')
305
- for command in pre_line.strip().split(' '):
306
- tmp = command.strip()
307
- if tmp:
308
- if tmp.endswith('/sftp-server'):
309
- return tmp
310
- return None
311
-
312
- def copy_data_on_container(
313
- self,
314
- client: SSHClient,
315
- docker_name: str,
316
- src_path: str,
317
- dest_path: str,
318
- is_recursive: bool = False,
319
- ):
320
- self.start_command_on_container(
321
- client=client,
322
- docker_name=docker_name,
323
- command=['cp ' + ('-R' if is_recursive else '') + f' {src_path}' + f' {dest_path}'],
324
- )
325
- # TODO: dont now how, need check for copy end!!!!
326
- sleep(3)
327
-
328
- @staticmethod
329
- def start_command_on_container(
330
- client: SSHClient,
331
- docker_name: str,
332
- command: List[str],
333
- is_bash: bool = False,
334
- ):
335
- if is_bash:
336
- stdin, stdout, stderr = client.exec_command(f'docker exec -it {docker_name} /bin/bash -c "{";".join(command)}"', get_pty=True)
337
- else:
338
- stdin, stdout, stderr = client.exec_command(f'docker exec -it {docker_name} {command[0]}', get_pty=True)
339
- for line in stdout.readlines():
340
- pass
341
-
342
-
343
- def __build_sftp_client(
344
- self,
345
- ip_address: str,
346
- username: str,
347
- private_key_path: Optional[str],
348
- ) -> Tuple[SSHClient, SFTPClient]:
349
- client: Optional[SSHClient] = self.__get_client(ip_address=ip_address, username=username, private_key_path=private_key_path)
350
- sftp_server_path = self.find_sftp_server_path(client=client)
351
-
352
- if not sftp_server_path:
353
- typer.echo(__('SFTP server is not installed on the server instance'))
354
- raise typer.Exit(1)
355
-
356
- chan = client.get_transport().open_session()
357
- # chan.exec_command("sudo su -c /usr/lib/openssh/sftp-server")
358
- chan.exec_command(f"sudo su -c {sftp_server_path}")
359
- sftp = paramiko.SFTPClient(chan)
360
-
361
- return client, sftp
362
-
363
- # TODO what the fuck does this method do?
364
- @staticmethod
365
- def _check_if_file_name_in_path(path: str, file_template: Optional[str] = None) -> bool:
366
- # strange logic
367
- file_name = path.split('/')[-1] if path else None
368
- if file_name and '.' in file_name:
369
- if file_template and '.' in file_template:
370
- extension = file_template.split('.')[-1]
371
- if extension in file_name:
372
- return True
373
- else:
374
- return True
375
- return False
376
-
377
- @staticmethod
378
- def _get_parent_from_path(path: str) -> str:
379
- pre_path = '/'.join(path.split('/')[0:-1])
380
- if not pre_path:
381
- return '/'
382
- else:
383
- return pre_path
384
-
385
-
386
- def __build_path_mapping_for_upload(
387
- self,
388
- files: List[FileItemEntity],
389
- instance_path: str,
390
- container_path: str,
391
- copy_only_folder_contents: bool,
392
- has_parent: bool = False,
393
- ) -> List[SftpFileItemEntity]:
394
- result = []
395
- for item in files:
396
- elem = SftpFileItemEntity.model_validate(item.model_dump())
397
- if item.is_file:
398
- # TODO god knows what is happening here
399
- # if uploading a single file without extension: has_file_name must be true if destination is not a folder
400
- has_file_name = self._check_if_file_name_in_path(
401
- path=instance_path,
402
- file_template=item.name,
403
- )
404
-
405
- if not has_parent and has_file_name:
406
- elem.instance_path = instance_path
407
- elem.container_path = container_path
408
- else:
409
- elem.instance_path = f"{instance_path}/{item.name}"
410
- elem.container_path = f"{container_path}/{item.name}"
411
-
412
- elem.dest_path = elem.instance_path
413
-
414
- else:
415
- if not has_parent and copy_only_folder_contents:
416
- elem.instance_path = instance_path
417
- elem.container_path = container_path
418
- else:
419
- elem.instance_path = f"{instance_path}/{item.name}"
420
- elem.container_path = f"{container_path}/{item.name}"
421
-
422
- elem.dest_path = elem.instance_path
423
-
424
- if len(item.children) > 0:
425
- elem.children = []
426
- elem.children.extend(self.__build_path_mapping_for_upload(
427
- files=item.children,
428
- instance_path=elem.instance_path,
429
- container_path=elem.container_path,
430
- copy_only_folder_contents=copy_only_folder_contents,
431
- has_parent=True,
432
- ))
433
-
434
- result.append(elem)
435
-
436
- return result
437
-
438
- def upload_data_to_container(
439
- self,
440
- ip_address: str,
441
- username: str,
442
- src_path: str,
443
- dest_path: str,
444
- instance_path: str,
445
- container_path: str,
446
- copy_only_folder_contents: bool,
447
- private_key_path: Optional[str],
448
- ):
449
- has_error = False
450
- client, sftp = self.__build_sftp_client(username=username, ip_address=ip_address, private_key_path=private_key_path)
451
-
452
- origin_files: List[FileItemEntity] = self.__file_system_service.get_path_items(src_path)
453
-
454
- files: List[SftpFileItemEntity] = self.__build_path_mapping_for_upload(
455
- files=origin_files,
456
- instance_path=instance_path,
457
- container_path=container_path,
458
- copy_only_folder_contents=copy_only_folder_contents
459
- )
460
-
461
- try:
462
- for item in files:
463
- self.__upload_list_files(
464
- sftp=sftp,
465
- src_item=item,
466
- )
467
-
468
- if len(files) == 0:
469
- typer.echo(__("No source files could be found on the server"))
470
- raise typer.Exit(1)
471
-
472
- if len(files[0].children) == 0 and files[0].is_folder:
473
- typer.echo(__("Source directory is empty"))
474
- raise typer.Exit(1)
475
-
476
- except FileNotFoundError as err:
477
- app_logger.error(f"Error uploading file to container {ip_address}, user {username} (file not found): {err}")
478
- typer.echo(__("Error uploading file: file not found on server"))
479
- has_error = True
480
- finally:
481
- sftp.close()
482
-
483
- client.close()
484
- if has_error:
485
- raise typer.Exit(1)
486
-
487
-
488
- def __read_remote_path_items_for_download(
489
- self,
490
- sftp: SFTPClient,
491
- current_path: str,
492
- dest_path: str,
493
- instance_path: str,
494
- copy_only_folder_contents: bool,
495
- depth: int = 0,
496
- ) -> List[SftpFileItemEntity]:
497
- config = self.__config_provider.get_config()
498
- path_items = []
499
- try:
500
- root_stat = sftp.stat(current_path)
501
- parent = SftpFileItemEntity(
502
- name=current_path.split('/')[-1],
503
- path=current_path,
504
- is_file=stat.S_ISREG(root_stat.st_mode),
505
- is_folder=stat.S_ISDIR(root_stat.st_mode),
506
- file_size=root_stat.st_size,
507
- instance_path=instance_path,
508
- dest_path=dest_path,
509
- )
510
-
511
- if depth == 0 and not copy_only_folder_contents and parent.is_folder:
512
- parent.dest_path = parent.dest_path.rstrip("/") + "/" + parent.name
513
- path_items.append(parent)
514
- if parent.is_file:
515
- # must be true if destination is file
516
- # must be false if destination is dir
517
- # if destination does not exist, destination is file (true)
518
- treat_as_file = True
519
- dest_fullpath = Path(config.runtime.working_directory + '/' + dest_path)
520
- if dest_fullpath.exists() and dest_fullpath.is_dir():
521
- treat_as_file = False
522
-
523
- if not treat_as_file:
524
- parent.dest_path += f"{parent.name}" if parent.dest_path.endswith('/') else f"/{parent.name}"
525
-
526
- elif parent.is_folder:
527
- sftp.chdir(current_path)
528
- if depth > 0:
529
- parent.dest_path += f"{parent.name}" if parent.dest_path.endswith('/') else f"/{parent.name}"
530
- for item in sftp.listdir_attr():
531
- next_path = f'{current_path}/{item.filename}'
532
- is_dir = stat.S_ISDIR(item.st_mode)
533
- is_file = stat.S_ISREG(item.st_mode)
534
- if is_file:
535
- parent.children.append(SftpFileItemEntity(
536
- name=item.filename,
537
- path=next_path,
538
- is_file=is_file,
539
- is_folder=is_dir,
540
- file_size=item.st_size,
541
- instance_path=f'{instance_path}/{item.filename}',
542
- dest_path=f'{parent.dest_path}/{item.filename}',
543
- ))
544
- elif is_dir:
545
- parent.children.extend(self.__read_remote_path_items_for_download(
546
- sftp=sftp,
547
- current_path=next_path,
548
- dest_path=parent.dest_path,
549
- instance_path=parent.instance_path,
550
- depth=depth + 1,
551
- copy_only_folder_contents=copy_only_folder_contents,
552
- ))
553
- return path_items
554
- except FileNotFoundError as ex:
555
- app_logger.exception(f"Unable to read remote file list: {ex}")
556
- typer.echo(__("Could not find the requested object on remote instance: %path%", {'path': current_path}))
557
- raise typer.Exit(1)
558
- except Exception as ex:
559
- app_logger.exception(f"Error occurred: {ex}")
560
- typer.echo(__('Error occurred while processing the file'))
561
- raise typer.Exit(1)
562
-
563
-
564
- def download_data_from_container(
565
- self,
566
- ip_address: str,
567
- username: str,
568
- dest_path: str,
569
- instance_path: str,
570
- copy_only_folder_contents: bool,
571
- private_key_path: Optional[str],
572
- ):
573
- has_error = False
574
-
575
- client, sftp = self.__build_sftp_client(username=username, ip_address=ip_address, private_key_path=private_key_path)
576
-
577
- try:
578
- files: List[SftpFileItemEntity] = self.__read_remote_path_items_for_download(
579
- sftp=sftp,
580
- current_path=instance_path,
581
- instance_path=instance_path,
582
- dest_path=dest_path,
583
- copy_only_folder_contents=copy_only_folder_contents,
584
- )
585
-
586
- if len(files) == 0:
587
- typer.echo(__("No source files could be found on the server"))
588
- raise typer.Exit(1)
589
-
590
- if len(files[0].children) == 0 and files[0].is_folder:
591
- typer.echo(__("Source directory is empty"))
592
- raise typer.Exit(1)
593
-
594
- for item in files:
595
- self.__download_list_files(
596
- sftp=sftp,
597
- src_item=item,
598
- )
599
- except FileNotFoundError as err:
600
- print(err)
601
- app_logger.error(f"Error uploading file to container {ip_address} for user {username} (file not found): {err}")
602
- typer.echo(__("Error uploading file: file not found on server"))
603
- has_error = True
604
- finally:
605
- sftp.close()
606
-
607
- client.close()
608
- if has_error:
609
- raise typer.Exit(1)
1
+ import os
2
+ import stat
3
+ from pathlib import Path
4
+ from time import sleep
5
+ from typing import Optional, List, Tuple
6
+
7
+ import math
8
+ import paramiko
9
+ import typer
10
+ from click import Abort
11
+ from paramiko.client import SSHClient
12
+ from paramiko.pkey import PKey
13
+ from paramiko.sftp_client import SFTPClient
14
+
15
+ from thestage.entities.file_item import FileItemEntity
16
+ from thestage.services.core_files.config_entity import ConfigEntity
17
+ from thestage.exceptions.remote_server_exception import RemoteServerException
18
+ from thestage.helpers.logger.app_logger import app_logger
19
+ from thestage.entities.enums.shell_type import ShellType
20
+ from thestage.helpers.ssh_util import parse_private_key
21
+ from thestage.i18n.translation import __
22
+ from thestage.services.clients.thestage_api.dtos.sftp_path_helper import SftpFileItemEntity
23
+ from thestage.services.config_provider.config_provider import ConfigProvider
24
+ from thestage.services.filesystem_service import FileSystemService
25
+
26
+ old_value: int = 0
27
+
28
+
29
+ class RemoteServerService:
30
+ __config_provider: ConfigProvider = None
31
+ __file_system_service: FileSystemService = None
32
+
33
+ def __init__(
34
+ self,
35
+ file_system_service: FileSystemService,
36
+ config_provider: ConfigProvider,
37
+ ):
38
+ self.__file_system_service = file_system_service
39
+ self.__config_provider = config_provider
40
+
41
+ def __get_client(
42
+ self,
43
+ ip_address: str,
44
+ username: str,
45
+ private_key_path: Optional[str],
46
+ ) -> Optional[SSHClient]:
47
+ pkey: Optional[PKey] = None
48
+ if private_key_path:
49
+ pkey = parse_private_key(private_key_path)
50
+ if pkey is None:
51
+ typer.echo("Could not identify provided private key (expected RSA, ECDSA, ED25519)")
52
+ raise typer.Exit(1)
53
+
54
+ client = SSHClient()
55
+ try:
56
+ client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
57
+ client.connect(
58
+ hostname=ip_address,
59
+ username=username,
60
+ timeout=6,
61
+ # key_filename=private_key_path, # key_filename is buggy for certain algorithms/systems apparently
62
+ pkey=pkey,
63
+ allow_agent=private_key_path is None,
64
+ look_for_keys=private_key_path is None,
65
+ )
66
+ return client
67
+
68
+ except Exception as ex:
69
+ if client:
70
+ client.close()
71
+ typer.echo(f"Error connecting to {ip_address} as {username} ({ex})")
72
+ app_logger.error(f"Error connecting to {ip_address} as {username} ({ex})")
73
+ raise RemoteServerException(
74
+ message=__("Unable to connect to remote server"),
75
+ ip_address=ip_address,
76
+ username=username,
77
+ )
78
+
79
+
80
+ def get_shell_from_container(
81
+ self,
82
+ ip_address: str,
83
+ username: str,
84
+ container_name: str,
85
+ private_key_path: Optional[str],
86
+ ) -> Optional[ShellType]:
87
+ client: Optional[SSHClient] = self.__get_client(
88
+ ip_address=ip_address,
89
+ username=username,
90
+ private_key_path=private_key_path,
91
+ )
92
+ stdin, stdout, stderr = client.exec_command(f'docker exec -it {container_name} cat /etc/shells', get_pty=True)
93
+ shell: Optional[ShellType] = None
94
+ stdout_lines: List[str] = []
95
+
96
+ for line in stdout.readlines():
97
+ stdout_lines.append(line.rstrip())
98
+ if 'bin/bash' in line:
99
+ shell = ShellType.BASH
100
+ break
101
+ if 'bin/sh' in line:
102
+ shell = ShellType.SH
103
+ break
104
+ client.close()
105
+
106
+ if shell is None:
107
+ for line_item in stdout_lines:
108
+ typer.echo(line_item)
109
+
110
+ return shell
111
+
112
+
113
+ def connect_to_instance(
114
+ self,
115
+ ip_address: str,
116
+ username: str,
117
+ private_key_path: Optional[str],
118
+ ):
119
+ try:
120
+ if private_key_path:
121
+ os.system(f"ssh -o PreferredAuthentications=publickey -o 'IdentitiesOnly=yes' -i {private_key_path} {username}@{ip_address}")
122
+ else:
123
+ os.system(f"ssh -o PreferredAuthentications=publickey {username}@{ip_address}")
124
+ except Abort as e1:
125
+ return
126
+ except Exception as ex:
127
+ app_logger.error(f"Error connecting to {ip_address} as {username} ({ex})")
128
+ raise RemoteServerException(
129
+ message=__("Unable to connect to remote server"),
130
+ ip_address=ip_address,
131
+ username=username,
132
+ )
133
+
134
+
135
+ def connect_to_container(
136
+ self,
137
+ ip_address: str,
138
+ username: str,
139
+ docker_name: str,
140
+ starting_directory: str,
141
+ shell: ShellType,
142
+ private_key_path: Optional[str],
143
+ ):
144
+ try:
145
+ if private_key_path:
146
+ os.system(f"ssh -tt -o PreferredAuthentications=publickey -o 'IdentitiesOnly=yes' -i {private_key_path} {username}@{ip_address} 'docker exec -it {docker_name} sh -c \"cd {starting_directory} && {shell.value}\"'")
147
+ else:
148
+ os.system(f"ssh -tt {username}@{ip_address} 'docker exec -it {docker_name} sh -c \"cd {starting_directory} && {shell.value}\"'")
149
+ except Exception as ex:
150
+ app_logger.exception(f"Error connecting to {ip_address} as {username} ({ex})")
151
+ raise RemoteServerException(
152
+ message=__("Unable to connect to remote server"),
153
+ ip_address=ip_address,
154
+ username=username,
155
+ )
156
+
157
+
158
+ def __upload_one_file(
159
+ self,
160
+ sftp: SFTPClient,
161
+ src_path: str,
162
+ dest_path: str,
163
+ file_name: str,
164
+ container_path: str,
165
+ file_size: [int] = 100,
166
+ ) -> bool:
167
+ has_error = False
168
+ try:
169
+ with typer.progressbar(length=file_size, label=__("Uploading %file_name% (%file_size%)", {'file_name': file_name, 'file_size': self.__convert_size(file_size)})) as progress:
170
+ def __show_result_copy(size: int, full_size: int):
171
+ global old_value
172
+ progress.update(size - (old_value or 0))
173
+ old_value = size
174
+ if old_value == full_size:
175
+ old_value = 0
176
+ sftp.put(localpath=src_path, remotepath=f"{dest_path}", callback=__show_result_copy)
177
+ typer.echo(__('Uploaded to container as %file_path%', {'file_path': container_path}))
178
+ except FileNotFoundError as err:
179
+ app_logger.exception(f"Error uploading file {file_name} to container (file not found): {err}")
180
+ typer.echo(__("Error uploading file: file not found on server"))
181
+ has_error = True
182
+ except Exception as err2:
183
+ typer.echo(err2)
184
+ app_logger.exception(f"Error uploading file {file_name} to container: {err2}")
185
+ typer.echo(__("Error uploading file: undefined server error"))
186
+ has_error = True
187
+
188
+ return has_error
189
+
190
+ def __make_dirs_by_sftp(
191
+ self,
192
+ sftp: SFTPClient,
193
+ path: str,
194
+ ):
195
+ full_path = ''
196
+ for item in path.split('/'):
197
+ if item == '':
198
+ continue
199
+ try:
200
+ full_path += f'/{item}'
201
+ sftp.chdir(full_path) # Test if remote_path exists
202
+ except IOError:
203
+ sftp.mkdir(full_path) # Create remote_path
204
+ sftp.chdir(full_path)
205
+
206
+
207
+ def __upload_list_files(
208
+ self,
209
+ sftp: SFTPClient,
210
+ src_item: SftpFileItemEntity,
211
+ ):
212
+ if src_item.is_file:
213
+
214
+ get_parent_path = '/'.join(src_item.instance_path.split('/')[0:-1])
215
+ self.__make_dirs_by_sftp(sftp=sftp, path=get_parent_path)
216
+
217
+ self.__upload_one_file(
218
+ sftp=sftp,
219
+ src_path=src_item.path,
220
+ dest_path=src_item.instance_path,
221
+ file_name=src_item.name,
222
+ file_size=src_item.file_size,
223
+ container_path=src_item.container_path
224
+ )
225
+ elif src_item.is_folder:
226
+ self.__make_dirs_by_sftp(sftp=sftp, path=src_item.instance_path)
227
+ for item in src_item.children:
228
+ self.__upload_list_files(
229
+ sftp=sftp,
230
+ src_item=item,
231
+ )
232
+
233
+
234
+ def __convert_size(self, size_bytes):
235
+ if size_bytes == 0:
236
+ return "0B"
237
+ size_name = ("B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB")
238
+ i = int(math.floor(math.log(size_bytes, 1024)))
239
+ p = math.pow(1024, i)
240
+ s = round(size_bytes / p, 2)
241
+ return "%s %s" % (s, size_name[i])
242
+
243
+
244
+ def __download_one_file(
245
+ self,
246
+ sftp: SFTPClient,
247
+ src_path: str,
248
+ dest_path: str,
249
+ file_name: str,
250
+ file_size: [int] = 100
251
+ ) -> bool:
252
+ has_error = False
253
+ try:
254
+ with typer.progressbar(length=file_size, label=__("Downloading %file_name% (%file_size%)", {'file_name': file_name, 'file_size': self.__convert_size(file_size)})) as progress:
255
+ def __show_result_copy(size: int, full_size: int):
256
+ global old_value
257
+ progress.update(size - (old_value or 0))
258
+ old_value = size
259
+ if old_value == full_size:
260
+ old_value = 0
261
+ sftp.get(remotepath=src_path, localpath=f"{dest_path}", callback=__show_result_copy)
262
+ typer.echo(__('Downloaded as %file_path%', {'file_path': dest_path}))
263
+ except FileNotFoundError as err:
264
+ app_logger.exception(f"Error retrieving file {file_name} from container (file not found): {err}")
265
+ typer.echo(__("Error retrieving file: file not found on server"))
266
+ has_error = True
267
+ except Exception as err2:
268
+ typer.echo(err2)
269
+ app_logger.exception(f"Error retrieving file {file_name} from container: {err2}")
270
+ typer.echo(__("Error retrieving file: undefined server error"))
271
+ has_error = True
272
+ return has_error
273
+
274
+
275
+ def __download_list_files(
276
+ self,
277
+ sftp: SFTPClient,
278
+ src_item: SftpFileItemEntity,
279
+ ):
280
+ if src_item.is_file:
281
+ self.__file_system_service.get_path('/'.join(src_item.dest_path.split('/')[0:-1]), auto_create=True)
282
+ self.__download_one_file(
283
+ sftp=sftp,
284
+ src_path=src_item.path,
285
+ dest_path=src_item.dest_path,
286
+ file_name=src_item.name,
287
+ file_size=src_item.file_size,
288
+ )
289
+ elif src_item.is_folder:
290
+ self.__file_system_service.get_path(str(Path(src_item.dest_path)), auto_create=True)
291
+ for item in src_item.children:
292
+ self.__download_list_files(
293
+ sftp=sftp,
294
+ src_item=item,
295
+ #dest_path=src_item.dest_path,
296
+ )
297
+
298
+ @staticmethod
299
+ def find_sftp_server_path(
300
+ client: SSHClient,
301
+ ) -> Optional[str]:
302
+ stdin, stdout, stderr = client.exec_command(f'whereis sftp-server', get_pty=True)
303
+ for line in stdout.readlines():
304
+ pre_line = line.replace('sftp-server:', '')
305
+ for command in pre_line.strip().split(' '):
306
+ tmp = command.strip()
307
+ if tmp:
308
+ if tmp.endswith('/sftp-server'):
309
+ return tmp
310
+ return None
311
+
312
+ def copy_data_on_container(
313
+ self,
314
+ client: SSHClient,
315
+ docker_name: str,
316
+ src_path: str,
317
+ dest_path: str,
318
+ is_recursive: bool = False,
319
+ ):
320
+ self.start_command_on_container(
321
+ client=client,
322
+ docker_name=docker_name,
323
+ command=['cp ' + ('-R' if is_recursive else '') + f' {src_path}' + f' {dest_path}'],
324
+ )
325
+ # TODO: dont now how, need check for copy end!!!!
326
+ sleep(3)
327
+
328
+ @staticmethod
329
+ def start_command_on_container(
330
+ client: SSHClient,
331
+ docker_name: str,
332
+ command: List[str],
333
+ is_bash: bool = False,
334
+ ):
335
+ if is_bash:
336
+ stdin, stdout, stderr = client.exec_command(f'docker exec -it {docker_name} /bin/bash -c "{";".join(command)}"', get_pty=True)
337
+ else:
338
+ stdin, stdout, stderr = client.exec_command(f'docker exec -it {docker_name} {command[0]}', get_pty=True)
339
+ for line in stdout.readlines():
340
+ pass
341
+
342
+
343
+ def __build_sftp_client(
344
+ self,
345
+ ip_address: str,
346
+ username: str,
347
+ private_key_path: Optional[str],
348
+ ) -> Tuple[SSHClient, SFTPClient]:
349
+ client: Optional[SSHClient] = self.__get_client(ip_address=ip_address, username=username, private_key_path=private_key_path)
350
+ sftp_server_path = self.find_sftp_server_path(client=client)
351
+
352
+ if not sftp_server_path:
353
+ typer.echo(__('SFTP server is not installed on the server instance'))
354
+ raise typer.Exit(1)
355
+
356
+ chan = client.get_transport().open_session()
357
+ # chan.exec_command("sudo su -c /usr/lib/openssh/sftp-server")
358
+ chan.exec_command(f"sudo su -c {sftp_server_path}")
359
+ sftp = paramiko.SFTPClient(chan)
360
+
361
+ return client, sftp
362
+
363
+ # TODO what the fuck does this method do?
364
+ @staticmethod
365
+ def _check_if_file_name_in_path(path: str, file_template: Optional[str] = None) -> bool:
366
+ # strange logic
367
+ file_name = path.split('/')[-1] if path else None
368
+ if file_name and '.' in file_name:
369
+ if file_template and '.' in file_template:
370
+ extension = file_template.split('.')[-1]
371
+ if extension in file_name:
372
+ return True
373
+ else:
374
+ return True
375
+ return False
376
+
377
+ @staticmethod
378
+ def _get_parent_from_path(path: str) -> str:
379
+ pre_path = '/'.join(path.split('/')[0:-1])
380
+ if not pre_path:
381
+ return '/'
382
+ else:
383
+ return pre_path
384
+
385
+
386
+ def __build_path_mapping_for_upload(
387
+ self,
388
+ files: List[FileItemEntity],
389
+ instance_path: str,
390
+ container_path: str,
391
+ copy_only_folder_contents: bool,
392
+ has_parent: bool = False,
393
+ ) -> List[SftpFileItemEntity]:
394
+ result = []
395
+ for item in files:
396
+ elem = SftpFileItemEntity.model_validate(item.model_dump())
397
+ if item.is_file:
398
+ # TODO god knows what is happening here
399
+ # if uploading a single file without extension: has_file_name must be true if destination is not a folder
400
+ has_file_name = self._check_if_file_name_in_path(
401
+ path=instance_path,
402
+ file_template=item.name,
403
+ )
404
+
405
+ if not has_parent and has_file_name:
406
+ elem.instance_path = instance_path
407
+ elem.container_path = container_path
408
+ else:
409
+ elem.instance_path = f"{instance_path}/{item.name}"
410
+ elem.container_path = f"{container_path}/{item.name}"
411
+
412
+ elem.dest_path = elem.instance_path
413
+
414
+ else:
415
+ if not has_parent and copy_only_folder_contents:
416
+ elem.instance_path = instance_path
417
+ elem.container_path = container_path
418
+ else:
419
+ elem.instance_path = f"{instance_path}/{item.name}"
420
+ elem.container_path = f"{container_path}/{item.name}"
421
+
422
+ elem.dest_path = elem.instance_path
423
+
424
+ if len(item.children) > 0:
425
+ elem.children = []
426
+ elem.children.extend(self.__build_path_mapping_for_upload(
427
+ files=item.children,
428
+ instance_path=elem.instance_path,
429
+ container_path=elem.container_path,
430
+ copy_only_folder_contents=copy_only_folder_contents,
431
+ has_parent=True,
432
+ ))
433
+
434
+ result.append(elem)
435
+
436
+ return result
437
+
438
+ def upload_data_to_container(
439
+ self,
440
+ ip_address: str,
441
+ username: str,
442
+ src_path: str,
443
+ dest_path: str,
444
+ instance_path: str,
445
+ container_path: str,
446
+ copy_only_folder_contents: bool,
447
+ private_key_path: Optional[str],
448
+ ):
449
+ has_error = False
450
+ client, sftp = self.__build_sftp_client(username=username, ip_address=ip_address, private_key_path=private_key_path)
451
+
452
+ origin_files: List[FileItemEntity] = self.__file_system_service.get_path_items(src_path)
453
+
454
+ files: List[SftpFileItemEntity] = self.__build_path_mapping_for_upload(
455
+ files=origin_files,
456
+ instance_path=instance_path,
457
+ container_path=container_path,
458
+ copy_only_folder_contents=copy_only_folder_contents
459
+ )
460
+
461
+ try:
462
+ for item in files:
463
+ self.__upload_list_files(
464
+ sftp=sftp,
465
+ src_item=item,
466
+ )
467
+
468
+ if len(files) == 0:
469
+ typer.echo(__("No source files could be found on the server"))
470
+ raise typer.Exit(1)
471
+
472
+ if len(files[0].children) == 0 and files[0].is_folder:
473
+ typer.echo(__("Source directory is empty"))
474
+ raise typer.Exit(1)
475
+
476
+ except FileNotFoundError as err:
477
+ app_logger.error(f"Error uploading file to container {ip_address}, user {username} (file not found): {err}")
478
+ typer.echo(__("Error uploading file: file not found on server"))
479
+ has_error = True
480
+ finally:
481
+ sftp.close()
482
+
483
+ client.close()
484
+ if has_error:
485
+ raise typer.Exit(1)
486
+
487
+
488
+ def __read_remote_path_items_for_download(
489
+ self,
490
+ sftp: SFTPClient,
491
+ current_path: str,
492
+ dest_path: str,
493
+ instance_path: str,
494
+ copy_only_folder_contents: bool,
495
+ depth: int = 0,
496
+ ) -> List[SftpFileItemEntity]:
497
+ config = self.__config_provider.get_config()
498
+ path_items = []
499
+ try:
500
+ root_stat = sftp.stat(current_path)
501
+ parent = SftpFileItemEntity(
502
+ name=current_path.split('/')[-1],
503
+ path=current_path,
504
+ is_file=stat.S_ISREG(root_stat.st_mode),
505
+ is_folder=stat.S_ISDIR(root_stat.st_mode),
506
+ file_size=root_stat.st_size,
507
+ instance_path=instance_path,
508
+ dest_path=dest_path,
509
+ )
510
+
511
+ if depth == 0 and not copy_only_folder_contents and parent.is_folder:
512
+ parent.dest_path = parent.dest_path.rstrip("/") + "/" + parent.name
513
+ path_items.append(parent)
514
+ if parent.is_file:
515
+ # must be true if destination is file
516
+ # must be false if destination is dir
517
+ # if destination does not exist, destination is file (true)
518
+ treat_as_file = True
519
+ dest_fullpath = Path(config.runtime.working_directory + '/' + dest_path)
520
+ if dest_fullpath.exists() and dest_fullpath.is_dir():
521
+ treat_as_file = False
522
+
523
+ if not treat_as_file:
524
+ parent.dest_path += f"{parent.name}" if parent.dest_path.endswith('/') else f"/{parent.name}"
525
+
526
+ elif parent.is_folder:
527
+ sftp.chdir(current_path)
528
+ if depth > 0:
529
+ parent.dest_path += f"{parent.name}" if parent.dest_path.endswith('/') else f"/{parent.name}"
530
+ for item in sftp.listdir_attr():
531
+ next_path = f'{current_path}/{item.filename}'
532
+ is_dir = stat.S_ISDIR(item.st_mode)
533
+ is_file = stat.S_ISREG(item.st_mode)
534
+ if is_file:
535
+ parent.children.append(SftpFileItemEntity(
536
+ name=item.filename,
537
+ path=next_path,
538
+ is_file=is_file,
539
+ is_folder=is_dir,
540
+ file_size=item.st_size,
541
+ instance_path=f'{instance_path}/{item.filename}',
542
+ dest_path=f'{parent.dest_path}/{item.filename}',
543
+ ))
544
+ elif is_dir:
545
+ parent.children.extend(self.__read_remote_path_items_for_download(
546
+ sftp=sftp,
547
+ current_path=next_path,
548
+ dest_path=parent.dest_path,
549
+ instance_path=parent.instance_path,
550
+ depth=depth + 1,
551
+ copy_only_folder_contents=copy_only_folder_contents,
552
+ ))
553
+ return path_items
554
+ except FileNotFoundError as ex:
555
+ app_logger.exception(f"Unable to read remote file list: {ex}")
556
+ typer.echo(__("Could not find the requested object on remote instance: %path%", {'path': current_path}))
557
+ raise typer.Exit(1)
558
+ except Exception as ex:
559
+ app_logger.exception(f"Error occurred: {ex}")
560
+ typer.echo(__('Error occurred while processing the file'))
561
+ raise typer.Exit(1)
562
+
563
+
564
+ def download_data_from_container(
565
+ self,
566
+ ip_address: str,
567
+ username: str,
568
+ dest_path: str,
569
+ instance_path: str,
570
+ copy_only_folder_contents: bool,
571
+ private_key_path: Optional[str],
572
+ ):
573
+ has_error = False
574
+
575
+ client, sftp = self.__build_sftp_client(username=username, ip_address=ip_address, private_key_path=private_key_path)
576
+
577
+ try:
578
+ files: List[SftpFileItemEntity] = self.__read_remote_path_items_for_download(
579
+ sftp=sftp,
580
+ current_path=instance_path,
581
+ instance_path=instance_path,
582
+ dest_path=dest_path,
583
+ copy_only_folder_contents=copy_only_folder_contents,
584
+ )
585
+
586
+ if len(files) == 0:
587
+ typer.echo(__("No source files could be found on the server"))
588
+ raise typer.Exit(1)
589
+
590
+ if len(files[0].children) == 0 and files[0].is_folder:
591
+ typer.echo(__("Source directory is empty"))
592
+ raise typer.Exit(1)
593
+
594
+ for item in files:
595
+ self.__download_list_files(
596
+ sftp=sftp,
597
+ src_item=item,
598
+ )
599
+ except FileNotFoundError as err:
600
+ print(err)
601
+ app_logger.error(f"Error uploading file to container {ip_address} for user {username} (file not found): {err}")
602
+ typer.echo(__("Error uploading file: file not found on server"))
603
+ has_error = True
604
+ finally:
605
+ sftp.close()
606
+
607
+ client.close()
608
+ if has_error:
609
+ raise typer.Exit(1)