appmesh 1.3.5__py3-none-any.whl → 1.3.7__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- appmesh/__init__.py +18 -1
- appmesh/app.py +226 -0
- appmesh/app_output.py +26 -0
- appmesh/app_run.py +48 -0
- appmesh/appmesh_client.py +167 -700
- appmesh/appmesh_client_tcp.py +362 -0
- {appmesh-1.3.5.dist-info → appmesh-1.3.7.dist-info}/METADATA +1 -1
- appmesh-1.3.7.dist-info/RECORD +10 -0
- appmesh-1.3.5.dist-info/RECORD +0 -6
- {appmesh-1.3.5.dist-info → appmesh-1.3.7.dist-info}/WHEEL +0 -0
- {appmesh-1.3.5.dist-info → appmesh-1.3.7.dist-info}/top_level.txt +0 -0
appmesh/appmesh_client.py
CHANGED
@@ -1,352 +1,26 @@
|
|
1
|
-
#!/usr/bin/python3
|
2
1
|
"""App Mesh Python SDK"""
|
2
|
+
|
3
|
+
# pylint: disable=broad-exception-raised,line-too-long,broad-exception-caught,too-many-lines, import-outside-toplevel, protected-access
|
4
|
+
|
5
|
+
# Standard library imports
|
3
6
|
import abc
|
4
7
|
import base64
|
5
|
-
from contextlib import contextmanager
|
6
|
-
import copy
|
7
8
|
import json
|
8
9
|
import os
|
9
|
-
import socket
|
10
|
-
import ssl
|
11
|
-
import uuid
|
12
|
-
|
13
|
-
from enum import Enum, unique
|
14
10
|
from datetime import datetime
|
11
|
+
from enum import Enum, unique
|
15
12
|
from http import HTTPStatus
|
16
|
-
from typing import
|
13
|
+
from typing import Tuple, Union
|
17
14
|
from urllib import parse
|
18
15
|
|
16
|
+
# Third-party imports
|
19
17
|
import aniso8601
|
20
18
|
import requests
|
21
19
|
|
22
|
-
#
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
DEFAULT_RUN_APP_LIFECYCLE_SECONDS = "P2DT12H" # 2.5 days
|
27
|
-
|
28
|
-
DEFAULT_SSL_CA_PEM_FILE = "/opt/appmesh/ssl/ca.pem"
|
29
|
-
DEFAULT_SSL_CLIENT_PEM_FILE = "/opt/appmesh/ssl/client.pem"
|
30
|
-
DEFAULT_SSL_CLIENT_PEM_KEY_FILE = "/opt/appmesh/ssl/client-key.pem"
|
31
|
-
|
32
|
-
# TLS-optimized chunk size (slightly less than maximum TLS record size)
|
33
|
-
# leaves some room for TLS overhead (like headers) within the 16 KB limit.
|
34
|
-
TCP_CHUNK_BLOCK_SIZE = 16 * 1024 - 256 # target to 16KB
|
35
|
-
TCP_MESSAGE_HEADER_LENGTH = 4
|
36
|
-
REST_TEXT_MESSAGE_JSON_KEY = "message"
|
37
|
-
MESSAGE_ENCODING_UTF8 = "utf-8"
|
38
|
-
|
39
|
-
HTTP_USER_AGENT = "appmesh/python"
|
40
|
-
HTTP_USER_AGENT_TCP = "appmesh/python/tcp"
|
41
|
-
HTTP_HEADER_KEY_USER_AGENT = "User-Agent"
|
42
|
-
HTTP_HEADER_KEY_X_SEND_FILE_SOCKET = "X-Send-File-Socket"
|
43
|
-
HTTP_HEADER_KEY_X_RECV_FILE_SOCKET = "X-Recv-File-Socket"
|
44
|
-
HTTP_HEADER_KEY_X_TARGET_HOST = "X-Target-Host"
|
45
|
-
|
46
|
-
|
47
|
-
class App(object):
|
48
|
-
"""
|
49
|
-
App object present an application in App Mesh
|
50
|
-
"""
|
51
|
-
|
52
|
-
@staticmethod
|
53
|
-
def _get_str_item(data: dict, key) -> Optional[str]:
|
54
|
-
"""Retrieve a string value from a dictionary by key, if it exists and is a valid string."""
|
55
|
-
return data[key] if (data and key in data and data[key] and isinstance(data[key], str)) else None
|
56
|
-
|
57
|
-
@staticmethod
|
58
|
-
def _get_int_item(data: dict, key) -> Optional[int]:
|
59
|
-
"""Retrieve an integer value from a dictionary by key, if it exists and is a valid integer."""
|
60
|
-
return int(data[key]) if (data and key in data and data[key] and isinstance(data[key], int)) else None
|
61
|
-
|
62
|
-
@staticmethod
|
63
|
-
def _get_bool_item(data: dict, key) -> Optional[bool]:
|
64
|
-
"""Retrieve a boolean value from a dictionary by key, if it exists."""
|
65
|
-
return bool(data[key]) if (data and key in data and data[key]) else None
|
66
|
-
|
67
|
-
@staticmethod
|
68
|
-
def _get_native_item(data: dict, key) -> Optional[object]:
|
69
|
-
"""Retrieve a deep copy of a value from a dictionary by key, if it exists."""
|
70
|
-
return copy.deepcopy(data[key]) if (data and key in data and data[key]) else None
|
71
|
-
|
72
|
-
@unique
|
73
|
-
class Permission(Enum):
|
74
|
-
"""Application permission definition"""
|
75
|
-
|
76
|
-
DENY = "1"
|
77
|
-
READ = "2"
|
78
|
-
WRITE = "3"
|
79
|
-
|
80
|
-
class Behavior(object):
|
81
|
-
"""
|
82
|
-
Application error handling behavior definition object
|
83
|
-
"""
|
84
|
-
|
85
|
-
@unique
|
86
|
-
class Action(Enum):
|
87
|
-
"""Application exit behavior definition"""
|
88
|
-
|
89
|
-
RESTART = "restart"
|
90
|
-
STANDBY = "standby"
|
91
|
-
KEEPALIVE = "keepalive"
|
92
|
-
REMOVE = "remove"
|
93
|
-
|
94
|
-
def __init__(self, data=None) -> None:
|
95
|
-
if isinstance(data, (str, bytes, bytearray)):
|
96
|
-
data = json.loads(data)
|
97
|
-
|
98
|
-
self.exit = App._get_str_item(data, "exit")
|
99
|
-
"""default exit behavior [restart,standby,keepalive,remove]"""
|
100
|
-
|
101
|
-
self.control = App._get_native_item(data, "control") if App._get_native_item(data, "control") else dict()
|
102
|
-
"""exit code behavior (e.g, --control 0:restart --control 1:standby), higher priority than default exit behavior"""
|
103
|
-
|
104
|
-
def set_exit_behavior(self, a: Action) -> None:
|
105
|
-
"""Set error handling behavior while application exit"""
|
106
|
-
self.exit = a.value
|
107
|
-
|
108
|
-
def set_control_behavior(self, control_code: int, a: Action) -> None:
|
109
|
-
"""Set error handling behavior while application exit with specific return code"""
|
110
|
-
self.control[str(control_code)] = a.value
|
111
|
-
|
112
|
-
class DailyLimitation(object):
|
113
|
-
"""
|
114
|
-
Application avialable day time definition object
|
115
|
-
"""
|
116
|
-
|
117
|
-
def __init__(self, data=None) -> None:
|
118
|
-
if isinstance(data, (str, bytes, bytearray)):
|
119
|
-
data = json.loads(data)
|
120
|
-
|
121
|
-
self.daily_start = App._get_int_item(data, "daily_start")
|
122
|
-
"""daily start time (e.g., '09:00:00+08')"""
|
123
|
-
|
124
|
-
self.daily_end = App._get_int_item(data, "daily_end")
|
125
|
-
"""daily end time (e.g., '20:00:00+08')"""
|
126
|
-
|
127
|
-
def set_daily_range(self, start: datetime, end: datetime) -> None:
|
128
|
-
"""Set valid day hour range"""
|
129
|
-
self.daily_start = int(start.timestamp())
|
130
|
-
self.daily_end = int(end.timestamp())
|
131
|
-
|
132
|
-
class ResourceLimitation(object):
|
133
|
-
"""
|
134
|
-
Application cgroup limitation definition object
|
135
|
-
"""
|
136
|
-
|
137
|
-
def __init__(self, data=None) -> None:
|
138
|
-
if isinstance(data, (str, bytes, bytearray)):
|
139
|
-
data = json.loads(data)
|
140
|
-
|
141
|
-
self.cpu_shares = App._get_int_item(data, "cpu_shares")
|
142
|
-
"""CPU shares (relative weight)"""
|
143
|
-
|
144
|
-
self.memory_mb = App._get_int_item(data, "memory_mb")
|
145
|
-
"""physical memory limit in MByte"""
|
146
|
-
|
147
|
-
self.memory_virt_mb = App._get_int_item(data, "memory_virt_mb")
|
148
|
-
"""virtual memory limit in MByte"""
|
149
|
-
|
150
|
-
def __init__(self, data=None):
|
151
|
-
"""Construct an App Mesh Application object
|
152
|
-
|
153
|
-
Args:
|
154
|
-
data (str | dict | json, optional): application definition data
|
155
|
-
"""
|
156
|
-
|
157
|
-
if isinstance(data, (str, bytes, bytearray)):
|
158
|
-
data = json.loads(data)
|
159
|
-
|
160
|
-
self.name = App._get_str_item(data, "name")
|
161
|
-
"""application name (unique)"""
|
162
|
-
|
163
|
-
self.command = App._get_str_item(data, "command")
|
164
|
-
"""full command line with arguments"""
|
165
|
-
|
166
|
-
self.shell = App._get_bool_item(data, "shell")
|
167
|
-
"""use shell mode, cmd can be more shell commands with string format"""
|
168
|
-
|
169
|
-
self.session_login = App._get_bool_item(data, "session_login")
|
170
|
-
"""app run in session login mode"""
|
171
|
-
|
172
|
-
self.description = App._get_str_item(data, "description")
|
173
|
-
"""application description string"""
|
174
|
-
|
175
|
-
self.metadata = App._get_native_item(data, "metadata")
|
176
|
-
"""metadata string/JSON (input for application, pass to process stdin)"""
|
177
|
-
|
178
|
-
self.working_dir = App._get_str_item(data, "working_dir")
|
179
|
-
"""working directory"""
|
180
|
-
|
181
|
-
self.status = App._get_int_item(data, "status")
|
182
|
-
"""initial application status (true is enable, false is disabled)"""
|
183
|
-
|
184
|
-
self.docker_image = App._get_str_item(data, "docker_image")
|
185
|
-
"""docker image which used to run command line (for docker container application)"""
|
186
|
-
|
187
|
-
self.stdout_cache_num = App._get_int_item(data, "stdout_cache_num")
|
188
|
-
"""stdout file cache number"""
|
189
|
-
|
190
|
-
self.start_time = App._get_int_item(data, "start_time")
|
191
|
-
"""start date time for app (ISO8601 time format, e.g., '2020-10-11T09:22:05')"""
|
192
|
-
|
193
|
-
self.end_time = App._get_int_item(data, "end_time")
|
194
|
-
"""end date time for app (ISO8601 time format, e.g., '2020-10-11T10:22:05')"""
|
195
|
-
|
196
|
-
self.interval = App._get_int_item(data, "interval")
|
197
|
-
"""start interval seconds for short running app, support ISO 8601 durations and cron expression (e.g., 'P1Y2M3DT4H5M6S' 'P5W' '* */5 * * * *')"""
|
198
|
-
|
199
|
-
self.cron = App._get_bool_item(data, "cron")
|
200
|
-
"""indicate interval parameter use cron expression or not"""
|
201
|
-
|
202
|
-
self.daily_limitation = App.DailyLimitation(App._get_native_item(data, "daily_limitation"))
|
203
|
-
|
204
|
-
self.retention = App._get_str_item(data, "retention")
|
205
|
-
"""extra timeout seconds for stopping current process, support ISO 8601 durations (e.g., 'P1Y2M3DT4H5M6S' 'P5W')."""
|
206
|
-
|
207
|
-
self.health_check_cmd = App._get_str_item(data, "health_check_cmd")
|
208
|
-
"""health check script command (e.g., sh -x 'curl host:port/health', return 0 is health)"""
|
209
|
-
|
210
|
-
self.permission = App._get_int_item(data, "permission")
|
211
|
-
"""application user permission, value is 2 bit integer: [group & other], each bit can be deny:1, read:2, write: 3."""
|
212
|
-
self.behavior = App.Behavior(App._get_native_item(data, "behavior"))
|
213
|
-
|
214
|
-
self.env = dict()
|
215
|
-
"""environment variables (e.g., -e env1=value1 -e env2=value2, APP_DOCKER_OPTS is used to input docker run parameters)"""
|
216
|
-
if data and "env" in data:
|
217
|
-
for k, v in data["env"].items():
|
218
|
-
self.env[k] = v
|
219
|
-
|
220
|
-
self.sec_env = dict()
|
221
|
-
"""security environment variables, encrypt in server side with application owner's cipher"""
|
222
|
-
if data and "sec_env" in data:
|
223
|
-
for k, v in data["sec_env"].items():
|
224
|
-
self.sec_env[k] = v
|
225
|
-
|
226
|
-
self.pid = App._get_int_item(data, "pid")
|
227
|
-
"""process id used to attach to the running process"""
|
228
|
-
self.resource_limit = App.ResourceLimitation(App._get_native_item(data, "resource_limit"))
|
229
|
-
|
230
|
-
# readonly attributes
|
231
|
-
self.owner = App._get_str_item(data, "owner")
|
232
|
-
"""owner name"""
|
233
|
-
self.pstree = App._get_str_item(data, "pstree")
|
234
|
-
"""process tree"""
|
235
|
-
self.container_id = App._get_str_item(data, "container_id")
|
236
|
-
"""container id"""
|
237
|
-
self.memory = App._get_int_item(data, "memory")
|
238
|
-
"""memory usage"""
|
239
|
-
self.cpu = App._get_int_item(data, "cpu")
|
240
|
-
"""cpu usage"""
|
241
|
-
self.fd = App._get_int_item(data, "fd")
|
242
|
-
"""file descriptor usage"""
|
243
|
-
self.last_start_time = App._get_int_item(data, "last_start_time")
|
244
|
-
"""last start time"""
|
245
|
-
self.last_exit_time = App._get_int_item(data, "last_exit_time")
|
246
|
-
"""last exit time"""
|
247
|
-
self.health = App._get_int_item(data, "health")
|
248
|
-
"""health status"""
|
249
|
-
self.version = App._get_int_item(data, "version")
|
250
|
-
"""version number"""
|
251
|
-
self.return_code = App._get_int_item(data, "return_code")
|
252
|
-
"""last exit code"""
|
253
|
-
|
254
|
-
def set_valid_time(self, start: datetime, end: datetime) -> None:
|
255
|
-
"""Set avialable time window"""
|
256
|
-
self.start_time = int(start.timestamp()) if start else None
|
257
|
-
self.end_time = int(end.timestamp()) if end else None
|
258
|
-
|
259
|
-
def set_env(self, k: str, v: str, secure: bool = False) -> None:
|
260
|
-
"""Set environment variable"""
|
261
|
-
if secure:
|
262
|
-
self.sec_env[k] = v
|
263
|
-
else:
|
264
|
-
self.env[k] = v
|
265
|
-
|
266
|
-
def set_permission(self, group_user: Permission, others_user: Permission) -> None:
|
267
|
-
"""Set application permission"""
|
268
|
-
self.permission = int(group_user.value + others_user.value)
|
269
|
-
|
270
|
-
def __str__(self) -> str:
|
271
|
-
return json.dumps(self.json())
|
272
|
-
|
273
|
-
def json(self):
|
274
|
-
"""serialize with JSON format"""
|
275
|
-
output = copy.deepcopy(self.__dict__)
|
276
|
-
output["behavior"] = copy.deepcopy(self.behavior.__dict__)
|
277
|
-
output["daily_limitation"] = copy.deepcopy(self.daily_limitation.__dict__)
|
278
|
-
output["resource_limit"] = copy.deepcopy(self.resource_limit.__dict__)
|
279
|
-
|
280
|
-
def clean_empty_item(data, key) -> None:
|
281
|
-
value = data[key]
|
282
|
-
if not value:
|
283
|
-
del data[key]
|
284
|
-
elif isinstance(value, dict) and key != "metadata":
|
285
|
-
for k in list(value):
|
286
|
-
clean_empty_item(value, k)
|
287
|
-
|
288
|
-
for k in list(output):
|
289
|
-
clean_empty_item(output, k)
|
290
|
-
for k in list(output):
|
291
|
-
clean_empty_item(output, k)
|
292
|
-
return output
|
293
|
-
|
294
|
-
|
295
|
-
class AppOutput(object):
|
296
|
-
"""App output information"""
|
297
|
-
|
298
|
-
def __init__(self, status_code: HTTPStatus, output: str, out_position: Optional[int], exit_code: Optional[int]) -> None:
|
299
|
-
|
300
|
-
self.status_code = status_code
|
301
|
-
"""HTTP status code"""
|
302
|
-
|
303
|
-
self.output = output
|
304
|
-
"""HTTP response text"""
|
305
|
-
|
306
|
-
self.out_position = out_position
|
307
|
-
"""Current read position (int or None)"""
|
308
|
-
|
309
|
-
self.exit_code = exit_code
|
310
|
-
"""Process exit code (int or None)"""
|
311
|
-
|
312
|
-
|
313
|
-
class AppRun(object):
|
314
|
-
"""
|
315
|
-
Application run object indicate to a remote run from run_async()
|
316
|
-
"""
|
317
|
-
|
318
|
-
def __init__(self, client, app_name: str, process_id: str):
|
319
|
-
self.app_name = app_name
|
320
|
-
"""application name"""
|
321
|
-
self.proc_uid = process_id
|
322
|
-
"""process_uuid from run_async()"""
|
323
|
-
self._client = client
|
324
|
-
"""AppMeshClient object"""
|
325
|
-
self._forwarding_host = client.forwarding_host
|
326
|
-
"""forward host indicates the target server for this app run"""
|
327
|
-
|
328
|
-
@contextmanager
|
329
|
-
def forwarding_host(self):
|
330
|
-
"""context manager for forward host override to self._client"""
|
331
|
-
original_value = self._client.forwarding_host
|
332
|
-
self._client.forwarding_host = self._forwarding_host
|
333
|
-
try:
|
334
|
-
yield
|
335
|
-
finally:
|
336
|
-
self._client.forwarding_host = original_value
|
337
|
-
|
338
|
-
def wait(self, stdout_print: bool = True, timeout: int = 0) -> int:
|
339
|
-
"""Wait for an async run to be finished
|
340
|
-
|
341
|
-
Args:
|
342
|
-
stdout_print (bool, optional): print remote stdout to local or not.
|
343
|
-
timeout (int, optional): wait max timeout seconds and return if not finished, 0 means wait until finished
|
344
|
-
|
345
|
-
Returns:
|
346
|
-
int: return exit code if process finished, return None for timeout or exception.
|
347
|
-
"""
|
348
|
-
with self.forwarding_host():
|
349
|
-
return self._client.run_async_wait(self, stdout_print, timeout)
|
20
|
+
# Local application-specific imports
|
21
|
+
from .app import App
|
22
|
+
from .app_run import AppRun
|
23
|
+
from .app_output import AppOutput
|
350
24
|
|
351
25
|
|
352
26
|
class AppMeshClient(metaclass=abc.ABCMeta):
|
@@ -377,6 +51,7 @@ class AppMeshClient(metaclass=abc.ABCMeta):
|
|
377
51
|
token-based authentication and authorization to enforce fine-grained permissions.
|
378
52
|
|
379
53
|
Methods:
|
54
|
+
# Authentication Management
|
380
55
|
- login()
|
381
56
|
- logoff()
|
382
57
|
- authentication()
|
@@ -385,6 +60,7 @@ class AppMeshClient(metaclass=abc.ABCMeta):
|
|
385
60
|
- totp_secret()
|
386
61
|
- totp_setup()
|
387
62
|
|
63
|
+
# Application Management
|
388
64
|
- app_add()
|
389
65
|
- app_delete()
|
390
66
|
- app_disable()
|
@@ -394,24 +70,27 @@ class AppMeshClient(metaclass=abc.ABCMeta):
|
|
394
70
|
- app_view()
|
395
71
|
- app_view_all()
|
396
72
|
|
73
|
+
# Run Application Operations
|
397
74
|
- run_async()
|
398
75
|
- run_async_wait()
|
399
76
|
- run_sync()
|
400
77
|
|
78
|
+
# System Management
|
79
|
+
- forwarding_host
|
401
80
|
- config_set()
|
402
81
|
- config_view()
|
403
82
|
- log_level_set()
|
404
83
|
- host_resource()
|
405
|
-
- forwarding_host
|
406
84
|
- metrics()
|
407
|
-
|
408
85
|
- tag_add()
|
409
86
|
- tag_delete()
|
410
87
|
- tag_view()
|
411
88
|
|
89
|
+
# File Management
|
412
90
|
- file_download()
|
413
91
|
- file_upload()
|
414
92
|
|
93
|
+
# User and Role Management
|
415
94
|
- user_add()
|
416
95
|
- user_delete()
|
417
96
|
- user_lock()
|
@@ -427,6 +106,19 @@ class AppMeshClient(metaclass=abc.ABCMeta):
|
|
427
106
|
- groups_view()
|
428
107
|
"""
|
429
108
|
|
109
|
+
DURATION_ONE_WEEK_ISO = "P1W"
|
110
|
+
DURATION_TWO_DAYS_ISO = "P2D"
|
111
|
+
DURATION_TWO_DAYS_HALF_ISO = "P2DT12H"
|
112
|
+
|
113
|
+
DEFAULT_SSL_CA_CERT_PATH = "/opt/appmesh/ssl/ca.pem"
|
114
|
+
DEFAULT_SSL_CLIENT_CERT_PATH = "/opt/appmesh/ssl/client.pem"
|
115
|
+
DEFAULT_SSL_CLIENT_KEY_PATH = "/opt/appmesh/ssl/client-key.pem"
|
116
|
+
|
117
|
+
JSON_KEY_MESSAGE = "message"
|
118
|
+
HTTP_USER_AGENT = "appmesh/python"
|
119
|
+
HTTP_HEADER_KEY_USER_AGENT = "User-Agent"
|
120
|
+
HTTP_HEADER_KEY_X_TARGET_HOST = "X-Target-Host"
|
121
|
+
|
430
122
|
@unique
|
431
123
|
class Method(Enum):
|
432
124
|
"""REST methods"""
|
@@ -440,20 +132,31 @@ class AppMeshClient(metaclass=abc.ABCMeta):
|
|
440
132
|
def __init__(
|
441
133
|
self,
|
442
134
|
rest_url: str = "https://127.0.0.1:6060",
|
443
|
-
rest_ssl_verify=
|
444
|
-
rest_ssl_client_cert=(
|
135
|
+
rest_ssl_verify=DEFAULT_SSL_CA_CERT_PATH if os.path.exists(DEFAULT_SSL_CA_CERT_PATH) else False,
|
136
|
+
rest_ssl_client_cert=(DEFAULT_SSL_CLIENT_CERT_PATH, DEFAULT_SSL_CLIENT_KEY_PATH) if os.path.exists(DEFAULT_SSL_CLIENT_CERT_PATH) else None,
|
445
137
|
rest_timeout=(60, 300),
|
446
138
|
jwt_token=None,
|
447
139
|
):
|
448
|
-
"""
|
140
|
+
"""Initialize an App Mesh HTTP client for interacting with the App Mesh server via secure HTTPS.
|
449
141
|
|
450
142
|
Args:
|
451
|
-
rest_url (str, optional): server URI
|
452
|
-
|
453
|
-
|
454
|
-
|
455
|
-
|
456
|
-
|
143
|
+
rest_url (str, optional): The server's base URI, including protocol, hostname, and port. Defaults to `"https://127.0.0.1:6060"`.
|
144
|
+
|
145
|
+
rest_ssl_verify (Union[bool, str], optional): Configures SSL certificate verification for HTTPS requests:
|
146
|
+
- `True`: Uses system CA certificates to verify the server's identity.
|
147
|
+
- `False`: Disables SSL verification (insecure, use cautiously for development).
|
148
|
+
- `str`: Path to a custom CA certificate or directory for verification. This option allows custom CA configuration,
|
149
|
+
which may be necessary in environments requiring specific CA chains that differ from the default system CAs.
|
150
|
+
|
151
|
+
rest_ssl_client_cert (Union[tuple, str], optional): Specifies a client certificate for mutual TLS authentication:
|
152
|
+
- If a `str`, provides the path to a PEM file with both client certificate and private key.
|
153
|
+
- If a `tuple`, contains two paths as (`cert`, `key`), where `cert` is the certificate file and `key` is the private key file.
|
154
|
+
|
155
|
+
rest_timeout (tuple, optional): HTTP connection timeouts for API requests, as `(connect_timeout, read_timeout)`.
|
156
|
+
The default is `(60, 300)`, where `60` seconds is the maximum time to establish a connection and `300` seconds for the maximum read duration.
|
157
|
+
|
158
|
+
jwt_token (str, optional): JWT token for API authentication, used in headers to authorize requests where required.
|
159
|
+
|
457
160
|
"""
|
458
161
|
|
459
162
|
self.server_url = rest_url
|
@@ -465,44 +168,97 @@ class AppMeshClient(metaclass=abc.ABCMeta):
|
|
465
168
|
|
466
169
|
@property
|
467
170
|
def jwt_token(self) -> str:
|
468
|
-
"""
|
171
|
+
"""Get the current JWT (JSON Web Token) used for authentication.
|
172
|
+
|
173
|
+
This property manages the authentication token used for securing API requests.
|
174
|
+
The token is used to authenticate and authorize requests to the service.
|
469
175
|
|
470
176
|
Returns:
|
471
|
-
str:
|
177
|
+
str: The current JWT token string.
|
178
|
+
Returns empty string if no token is set.
|
179
|
+
|
180
|
+
Notes:
|
181
|
+
- The token typically includes claims for identity and permissions
|
182
|
+
- Token format: "header.payload.signature"
|
183
|
+
- Tokens are time-sensitive and may expire
|
472
184
|
"""
|
473
185
|
return self._jwt_token
|
474
186
|
|
475
187
|
@jwt_token.setter
|
476
188
|
def jwt_token(self, token: str) -> None:
|
477
|
-
"""
|
189
|
+
"""Set the JWT token for authentication.
|
190
|
+
|
191
|
+
Configure the JWT token used for authenticating requests. The token should be
|
192
|
+
a valid JWT issued by a trusted authority.
|
478
193
|
|
479
194
|
Args:
|
480
|
-
token (str):
|
195
|
+
token (str): JWT token string in standard JWT format
|
196
|
+
(e.g., "eyJhbGci...payload...signature")
|
197
|
+
Pass empty string to clear the token.
|
198
|
+
|
199
|
+
Example:
|
200
|
+
>>> client.jwt_token = "eyJhbGci..." # Set new token
|
201
|
+
>>> client.jwt_token = "" # Clear token
|
202
|
+
|
203
|
+
Notes:
|
204
|
+
Security best practices:
|
205
|
+
- Store tokens securely
|
206
|
+
- Never log or expose complete tokens
|
207
|
+
- Refresh tokens before expiration
|
208
|
+
- Validate token format before setting
|
481
209
|
"""
|
482
210
|
self._jwt_token = token
|
483
211
|
|
484
212
|
@property
|
485
213
|
def forwarding_host(self) -> str:
|
486
|
-
"""
|
214
|
+
"""Get the target host address for request forwarding in a cluster setup.
|
215
|
+
|
216
|
+
This property manages the destination host where requests will be forwarded to
|
217
|
+
within a cluster configuration. The host can be specified in two formats:
|
218
|
+
1. hostname/IP only: will use the current service's port
|
219
|
+
2. hostname/IP with port: will use the specified port
|
487
220
|
|
488
221
|
Returns:
|
489
|
-
str:
|
222
|
+
str: The target host address in either format:
|
223
|
+
- "hostname" or "IP" (using current service port)
|
224
|
+
- "hostname:port" or "IP:port" (using specified port)
|
225
|
+
Returns empty string if no forwarding host is set.
|
226
|
+
|
227
|
+
Notes:
|
228
|
+
For proper JWT token sharing across the cluster:
|
229
|
+
- All nodes must share the same JWT salt configuration
|
230
|
+
- All nodes must use identical JWT issuer settings
|
231
|
+
- When port is omitted, current service port will be used
|
490
232
|
"""
|
491
233
|
return self._forwarding_host
|
492
234
|
|
493
235
|
@forwarding_host.setter
|
494
236
|
def forwarding_host(self, host: str) -> None:
|
495
|
-
"""
|
237
|
+
"""Set the target host address for request forwarding.
|
238
|
+
|
239
|
+
Configure the destination host where requests should be forwarded to. This is
|
240
|
+
used in cluster setups for request routing and load distribution.
|
496
241
|
|
497
242
|
Args:
|
498
|
-
host (str):
|
243
|
+
host (str): Target host address in one of two formats:
|
244
|
+
1. "hostname" or "IP" - will use current service port
|
245
|
+
(e.g., "backend-node" or "192.168.1.100")
|
246
|
+
2. "hostname:port" or "IP:port" - will use specified port
|
247
|
+
(e.g., "backend-node:6060" or "192.168.1.100:6060")
|
248
|
+
Pass empty string to disable forwarding.
|
249
|
+
|
250
|
+
Examples:
|
251
|
+
>>> client.forwarding_host = "backend-node:6060" # Use specific port
|
252
|
+
>>> client.forwarding_host = "backend-node" # Use current service port
|
253
|
+
>>> client.forwarding_host = None # Disable forwarding
|
499
254
|
"""
|
255
|
+
|
500
256
|
self._forwarding_host = host
|
501
257
|
|
502
258
|
########################################
|
503
259
|
# Security
|
504
260
|
########################################
|
505
|
-
def login(self, user_name: str, user_pwd: str, totp_code="", timeout_seconds=
|
261
|
+
def login(self, user_name: str, user_pwd: str, totp_code="", timeout_seconds=DURATION_ONE_WEEK_ISO) -> str:
|
506
262
|
"""Login with user name and password
|
507
263
|
|
508
264
|
Args:
|
@@ -584,7 +340,7 @@ class AppMeshClient(metaclass=abc.ABCMeta):
|
|
584
340
|
raise Exception(resp.text)
|
585
341
|
return resp.status_code == HTTPStatus.OK
|
586
342
|
|
587
|
-
def renew(self, timeout_seconds=
|
343
|
+
def renew(self, timeout_seconds=DURATION_ONE_WEEK_ISO) -> str:
|
588
344
|
"""Renew current token
|
589
345
|
|
590
346
|
Args:
|
@@ -1304,31 +1060,44 @@ class AppMeshClient(metaclass=abc.ABCMeta):
|
|
1304
1060
|
|
1305
1061
|
def run_async(
|
1306
1062
|
self,
|
1307
|
-
app: App,
|
1308
|
-
max_time_seconds=
|
1309
|
-
life_cycle_seconds=
|
1310
|
-
):
|
1311
|
-
"""
|
1312
|
-
Asyncrized run will not block process
|
1063
|
+
app: Union[App, str],
|
1064
|
+
max_time_seconds: Union[int, str] = DURATION_TWO_DAYS_ISO,
|
1065
|
+
life_cycle_seconds: Union[int, str] = DURATION_TWO_DAYS_HALF_ISO,
|
1066
|
+
) -> AppRun:
|
1067
|
+
"""Run an application asynchronously on a remote system without blocking the API.
|
1313
1068
|
|
1314
1069
|
Args:
|
1315
|
-
app (App):
|
1316
|
-
|
1317
|
-
|
1070
|
+
app (Union[App, str]): An `App` instance or a shell command string.
|
1071
|
+
- If `app` is a string, it is treated as a shell command for the remote run,
|
1072
|
+
and an `App` instance is created as:
|
1073
|
+
`App({"command": "<command_string>", "shell": True})`.
|
1074
|
+
- If `app` is an `App` object, providing only the `name` attribute (without
|
1075
|
+
a command) will run an existing application; otherwise, it is treated as a new application.
|
1076
|
+
max_time_seconds (Union[int, str], optional): Maximum runtime for the remote process.
|
1077
|
+
Accepts ISO 8601 duration format (e.g., 'P1Y2M3DT4H5M6S', 'P5W'). Defaults to `P2D`.
|
1078
|
+
life_cycle_seconds (Union[int, str], optional): Maximum lifecycle time for the remote process.
|
1079
|
+
Accepts ISO 8601 duration format. Defaults to `P2DT12H`.
|
1318
1080
|
|
1319
1081
|
Returns:
|
1320
|
-
|
1321
|
-
str: process_uuid, process UUID for this run
|
1082
|
+
AppRun: An application run object that can be used to monitor and retrieve the result of the run.
|
1322
1083
|
"""
|
1084
|
+
if isinstance(app, str):
|
1085
|
+
app = App({"command": app, "shell": True})
|
1086
|
+
|
1323
1087
|
path = "/appmesh/app/run"
|
1324
1088
|
resp = self._request_http(
|
1325
1089
|
AppMeshClient.Method.POST,
|
1326
1090
|
body=app.json(),
|
1327
1091
|
path=path,
|
1328
|
-
query={
|
1092
|
+
query={
|
1093
|
+
"timeout": self._parse_duration(max_time_seconds),
|
1094
|
+
"lifecycle": self._parse_duration(life_cycle_seconds),
|
1095
|
+
},
|
1329
1096
|
)
|
1330
1097
|
if resp.status_code != HTTPStatus.OK:
|
1331
1098
|
raise Exception(resp.text)
|
1099
|
+
|
1100
|
+
# Return an AppRun object with the application name and process UUID
|
1332
1101
|
return AppRun(self, resp.json()["name"], resp.json()["process_uuid"])
|
1333
1102
|
|
1334
1103
|
def run_async_wait(self, run: AppRun, stdout_print: bool = True, timeout: int = 0) -> int:
|
@@ -1366,30 +1135,41 @@ class AppMeshClient(metaclass=abc.ABCMeta):
|
|
1366
1135
|
|
1367
1136
|
def run_sync(
|
1368
1137
|
self,
|
1369
|
-
app: App,
|
1138
|
+
app: Union[App, str],
|
1370
1139
|
stdout_print: bool = True,
|
1371
|
-
max_time_seconds=
|
1372
|
-
life_cycle_seconds=
|
1373
|
-
) -> Tuple[int, str]:
|
1374
|
-
"""
|
1375
|
-
|
1140
|
+
max_time_seconds: Union[int, str] = DURATION_TWO_DAYS_ISO,
|
1141
|
+
life_cycle_seconds: Union[int, str] = DURATION_TWO_DAYS_HALF_ISO,
|
1142
|
+
) -> Tuple[Union[int, None], str]:
|
1143
|
+
"""Synchronously run an application remotely, blocking until completion, and return the result.
|
1144
|
+
|
1145
|
+
If 'app' is a string, it is treated as a shell command and converted to an App instance.
|
1146
|
+
If 'app' is App object, the name attribute is used to run an existing application if specified.
|
1376
1147
|
|
1377
1148
|
Args:
|
1378
|
-
app (App):
|
1379
|
-
|
1380
|
-
|
1381
|
-
|
1149
|
+
app (Union[App, str]): An App instance or a shell command string.
|
1150
|
+
If a string, an App instance is created as:
|
1151
|
+
`appmesh_client.App({"command": "<command_string>", "shell": True})`
|
1152
|
+
stdout_print (bool, optional): If True, prints the remote stdout locally. Defaults to True.
|
1153
|
+
max_time_seconds (Union[int, str], optional): Maximum runtime for the remote process.
|
1154
|
+
Supports ISO 8601 duration format (e.g., 'P1Y2M3DT4H5M6S', 'P5W'). Defaults to DEFAULT_RUN_APP_TIMEOUT_SECONDS.
|
1155
|
+
life_cycle_seconds (Union[int, str], optional): Maximum lifecycle time for the remote process.
|
1156
|
+
Supports ISO 8601 duration format. Defaults to DEFAULT_RUN_APP_LIFECYCLE_SECONDS.
|
1382
1157
|
|
1383
1158
|
Returns:
|
1384
|
-
int:
|
1385
|
-
str: stdout text
|
1159
|
+
Tuple[Union[int, None], str]: Exit code of the process (None if unavailable) and the stdout text.
|
1386
1160
|
"""
|
1161
|
+
if isinstance(app, str):
|
1162
|
+
app = App({"command": app, "shell": True})
|
1163
|
+
|
1387
1164
|
path = "/appmesh/app/syncrun"
|
1388
1165
|
resp = self._request_http(
|
1389
1166
|
AppMeshClient.Method.POST,
|
1390
1167
|
body=app.json(),
|
1391
1168
|
path=path,
|
1392
|
-
query={
|
1169
|
+
query={
|
1170
|
+
"timeout": self._parse_duration(max_time_seconds),
|
1171
|
+
"lifecycle": self._parse_duration(life_cycle_seconds),
|
1172
|
+
},
|
1393
1173
|
)
|
1394
1174
|
exit_code = None
|
1395
1175
|
if resp.status_code == HTTPStatus.OK:
|
@@ -1399,6 +1179,7 @@ class AppMeshClient(metaclass=abc.ABCMeta):
|
|
1399
1179
|
exit_code = int(resp.headers.get("Exit-Code"))
|
1400
1180
|
elif stdout_print:
|
1401
1181
|
print(resp.text)
|
1182
|
+
|
1402
1183
|
return exit_code, resp.text
|
1403
1184
|
|
1404
1185
|
def _request_http(self, method: Method, path: str, query: dict = None, header: dict = None, body=None) -> requests.Response:
|
@@ -1421,10 +1202,10 @@ class AppMeshClient(metaclass=abc.ABCMeta):
|
|
1421
1202
|
header["Authorization"] = "Bearer " + self.jwt_token
|
1422
1203
|
if self.forwarding_host and len(self.forwarding_host) > 0:
|
1423
1204
|
if ":" in self.forwarding_host:
|
1424
|
-
header[HTTP_HEADER_KEY_X_TARGET_HOST] = self.forwarding_host
|
1205
|
+
header[self.HTTP_HEADER_KEY_X_TARGET_HOST] = self.forwarding_host
|
1425
1206
|
else:
|
1426
|
-
header[HTTP_HEADER_KEY_X_TARGET_HOST] = self.forwarding_host + ":" + str(parse.urlsplit(self.server_url).port)
|
1427
|
-
header[HTTP_HEADER_KEY_USER_AGENT] = HTTP_USER_AGENT
|
1207
|
+
header[self.HTTP_HEADER_KEY_X_TARGET_HOST] = self.forwarding_host + ":" + str(parse.urlsplit(self.server_url).port)
|
1208
|
+
header[self.HTTP_HEADER_KEY_USER_AGENT] = self.HTTP_USER_AGENT
|
1428
1209
|
|
1429
1210
|
if method is AppMeshClient.Method.GET:
|
1430
1211
|
return requests.get(url=rest_url, params=query, headers=header, cert=self.ssl_client_cert, verify=self.ssl_verify, timeout=self.rest_timeout)
|
@@ -1440,317 +1221,3 @@ class AppMeshClient(metaclass=abc.ABCMeta):
|
|
1440
1221
|
return requests.put(url=rest_url, params=query, headers=header, json=body, cert=self.ssl_client_cert, verify=self.ssl_verify, timeout=self.rest_timeout)
|
1441
1222
|
else:
|
1442
1223
|
raise Exception("Invalid http method", method)
|
1443
|
-
|
1444
|
-
|
1445
|
-
class AppMeshClientTCP(AppMeshClient):
|
1446
|
-
"""
|
1447
|
-
Client SDK for interacting with the App Mesh service over TCP, with enhanced support for large file transfers.
|
1448
|
-
|
1449
|
-
The `AppMeshClientTCP` class extends the functionality of `AppMeshClient` by offering a TCP-based communication layer
|
1450
|
-
for the App Mesh REST API. It overrides the file download and upload methods to support large file transfers with
|
1451
|
-
improved performance, leveraging TCP for lower latency and higher throughput compared to HTTP.
|
1452
|
-
|
1453
|
-
This client is suitable for applications requiring efficient data transfers and high-throughput operations within the
|
1454
|
-
App Mesh ecosystem, while maintaining compatibility with all other attributes and methods from `AppMeshClient`.
|
1455
|
-
|
1456
|
-
Dependency:
|
1457
|
-
- Install the required package for message serialization:
|
1458
|
-
pip3 install msgpack
|
1459
|
-
|
1460
|
-
Usage:
|
1461
|
-
- Import the client module:
|
1462
|
-
from appmesh import appmesh_client
|
1463
|
-
|
1464
|
-
Example:
|
1465
|
-
client = appmesh_client.AppMeshClientTCP()
|
1466
|
-
client.login("your-name", "your-password")
|
1467
|
-
client.file_download("/tmp/os-release", "os-release")
|
1468
|
-
|
1469
|
-
Attributes:
|
1470
|
-
- Inherits all attributes from `AppMeshClient`, including TLS secure connections and JWT-based authentication.
|
1471
|
-
- Optimized for TCP-based communication to provide better performance for large file transfers.
|
1472
|
-
|
1473
|
-
Methods:
|
1474
|
-
- file_download()
|
1475
|
-
- file_upload()
|
1476
|
-
- Inherits all other methods from `AppMeshClient`, providing a consistent interface for managing applications within App Mesh.
|
1477
|
-
"""
|
1478
|
-
|
1479
|
-
def __init__(
|
1480
|
-
self,
|
1481
|
-
rest_ssl_verify=DEFAULT_SSL_CA_PEM_FILE if os.path.exists(DEFAULT_SSL_CA_PEM_FILE) else False,
|
1482
|
-
rest_ssl_client_cert=None,
|
1483
|
-
jwt_token=None,
|
1484
|
-
tcp_address=("localhost", 6059),
|
1485
|
-
):
|
1486
|
-
"""Construct an App Mesh client TCP object
|
1487
|
-
|
1488
|
-
Args:
|
1489
|
-
rest_ssl_verify (str, optional): (optional) SSL CA certification. Either a boolean, in which case it controls whether we verify
|
1490
|
-
the server's TLS certificate, or a string, in which case it must be a path to a CA bundle to use. Defaults to ``True``.
|
1491
|
-
rest_ssl_client_cert (tuple, optional): SSL client certificate and key pair . If String, path to ssl client cert file (.pem). If Tuple, ('cert', 'key') pair.
|
1492
|
-
jwt_token (str, optional): JWT token, provide correct token is same with login() & authentication().
|
1493
|
-
|
1494
|
-
tcp_address (tuple, optional): TCP connect address.
|
1495
|
-
"""
|
1496
|
-
self.tcp_address = tcp_address
|
1497
|
-
self.__socket_client = None
|
1498
|
-
super().__init__(rest_ssl_verify=rest_ssl_verify, rest_ssl_client_cert=rest_ssl_client_cert, jwt_token=jwt_token)
|
1499
|
-
|
1500
|
-
def __del__(self) -> None:
|
1501
|
-
"""De-construction"""
|
1502
|
-
self.__close_socket()
|
1503
|
-
|
1504
|
-
def __connect_socket(self) -> None:
|
1505
|
-
"""Establish tcp connection"""
|
1506
|
-
context = ssl.create_default_context(ssl.Purpose.SERVER_AUTH)
|
1507
|
-
# Set minimum TLS version
|
1508
|
-
if hasattr(context, "minimum_version"):
|
1509
|
-
context.minimum_version = ssl.TLSVersion.TLSv1_2
|
1510
|
-
else:
|
1511
|
-
context.options |= ssl.OP_NO_TLSv1 | ssl.OP_NO_TLSv1_1
|
1512
|
-
# Configure SSL verification
|
1513
|
-
if not self.ssl_verify:
|
1514
|
-
context.verify_mode = ssl.CERT_NONE
|
1515
|
-
else:
|
1516
|
-
context.verify_mode = ssl.CERT_REQUIRED # Require certificate verification
|
1517
|
-
context.load_default_certs() # Load system's default CA certificates
|
1518
|
-
if isinstance(self.ssl_verify, str):
|
1519
|
-
if os.path.isfile(self.ssl_verify):
|
1520
|
-
# Add custom CA certificate file
|
1521
|
-
try:
|
1522
|
-
context.load_verify_locations(cafile=self.ssl_verify)
|
1523
|
-
except ssl.SSLError:
|
1524
|
-
# If loading fails, try using just the default CAs
|
1525
|
-
context = ssl.create_default_context(ssl.Purpose.SERVER_AUTH)
|
1526
|
-
elif os.path.isdir(self.ssl_verify):
|
1527
|
-
# Load CA certificates from directory
|
1528
|
-
context.load_verify_locations(capath=self.ssl_verify)
|
1529
|
-
else:
|
1530
|
-
raise ValueError(f"ssl_verify path '{self.ssl_verify}' is neither a file nor a directory")
|
1531
|
-
|
1532
|
-
if self.ssl_client_cert is not None:
|
1533
|
-
# Load client-side certificate and private key
|
1534
|
-
context.load_cert_chain(certfile=self.ssl_client_cert[0], keyfile=self.ssl_client_cert[1])
|
1535
|
-
|
1536
|
-
# Create a TCP socket
|
1537
|
-
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
1538
|
-
sock.setblocking(True)
|
1539
|
-
# Wrap the socket with SSL/TLS
|
1540
|
-
self.__socket_client = context.wrap_socket(sock, server_hostname=self.tcp_address[0])
|
1541
|
-
# Connect to the server
|
1542
|
-
self.__socket_client.connect(self.tcp_address)
|
1543
|
-
# Disable Nagle's algorithm
|
1544
|
-
self.__socket_client.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
|
1545
|
-
|
1546
|
-
def __close_socket(self) -> None:
|
1547
|
-
"""Close socket connection"""
|
1548
|
-
if self.__socket_client:
|
1549
|
-
try:
|
1550
|
-
self.__socket_client.close()
|
1551
|
-
self.__socket_client = None
|
1552
|
-
except Exception as ex:
|
1553
|
-
print(ex)
|
1554
|
-
|
1555
|
-
def __recvall(self, length: int) -> bytes:
|
1556
|
-
"""socket recv data with fixed length
|
1557
|
-
https://stackoverflow.com/questions/64466530/using-a-custom-socket-recvall-function-works-only-if-thread-is-put-to-sleep
|
1558
|
-
Args:
|
1559
|
-
length (bytes): data length to be recieved
|
1560
|
-
|
1561
|
-
Raises:
|
1562
|
-
EOFError: _description_
|
1563
|
-
|
1564
|
-
Returns:
|
1565
|
-
bytes: socket data
|
1566
|
-
"""
|
1567
|
-
fragments = []
|
1568
|
-
while length:
|
1569
|
-
chunk = self.__socket_client.recv(length)
|
1570
|
-
if not chunk:
|
1571
|
-
raise EOFError("socket closed")
|
1572
|
-
length -= len(chunk)
|
1573
|
-
fragments.append(chunk)
|
1574
|
-
return b"".join(fragments)
|
1575
|
-
|
1576
|
-
def _request_http(self, method: AppMeshClient.Method, path: str, query: dict = None, header: dict = None, body=None) -> requests.Response:
|
1577
|
-
"""TCP API
|
1578
|
-
|
1579
|
-
Args:
|
1580
|
-
method (Method): AppMeshClient.Method.
|
1581
|
-
path (str): URI patch str.
|
1582
|
-
query (dict, optional): HTTP query parameters.
|
1583
|
-
header (dict, optional): HTTP headers.
|
1584
|
-
body (_type_, optional): object to send in the body of the :class:`Request`.
|
1585
|
-
|
1586
|
-
Returns:
|
1587
|
-
requests.Response: HTTP response
|
1588
|
-
"""
|
1589
|
-
import msgpack
|
1590
|
-
|
1591
|
-
class RequestMsg:
|
1592
|
-
"""HTTP request message"""
|
1593
|
-
|
1594
|
-
uuid: str = ""
|
1595
|
-
request_uri: str = ""
|
1596
|
-
http_method: str = ""
|
1597
|
-
client_addr: str = ""
|
1598
|
-
body: bytes = b""
|
1599
|
-
headers: dict = {}
|
1600
|
-
querys: dict = {}
|
1601
|
-
|
1602
|
-
def serialize(self) -> bytes:
|
1603
|
-
"""Serialize request message to bytes"""
|
1604
|
-
# http://www.cnitblog.com/luckydmz/archive/2019/11/20/91959.html
|
1605
|
-
self_dict = vars(self)
|
1606
|
-
self_dict["headers"] = self.headers
|
1607
|
-
self_dict["querys"] = self.querys
|
1608
|
-
return msgpack.dumps(self_dict)
|
1609
|
-
|
1610
|
-
class ResponseMsg:
|
1611
|
-
"""HTTP response message"""
|
1612
|
-
|
1613
|
-
uuid: str = ""
|
1614
|
-
request_uri: str = ""
|
1615
|
-
http_status: int = 0
|
1616
|
-
body_msg_type: str = ""
|
1617
|
-
body: str = ""
|
1618
|
-
headers: dict = {}
|
1619
|
-
|
1620
|
-
def desirialize(self, buf: bytes):
|
1621
|
-
"""Deserialize response message"""
|
1622
|
-
dic = msgpack.unpackb(buf)
|
1623
|
-
for k, v in dic.items():
|
1624
|
-
setattr(self, k, v)
|
1625
|
-
return self
|
1626
|
-
|
1627
|
-
if self.__socket_client is None:
|
1628
|
-
self.__connect_socket()
|
1629
|
-
|
1630
|
-
appmesh_requst = RequestMsg()
|
1631
|
-
if super().jwt_token:
|
1632
|
-
appmesh_requst.headers["Authorization"] = "Bearer " + super().jwt_token
|
1633
|
-
if super().forwarding_host and len(super().forwarding_host) > 0:
|
1634
|
-
raise Exception("Not support forward request in TCP mode")
|
1635
|
-
appmesh_requst.headers[HTTP_HEADER_KEY_USER_AGENT] = HTTP_USER_AGENT_TCP
|
1636
|
-
appmesh_requst.uuid = str(uuid.uuid1())
|
1637
|
-
appmesh_requst.http_method = method.value
|
1638
|
-
appmesh_requst.request_uri = path
|
1639
|
-
appmesh_requst.client_addr = socket.gethostname()
|
1640
|
-
if body:
|
1641
|
-
if isinstance(body, dict) or isinstance(body, list):
|
1642
|
-
appmesh_requst.body = bytes(json.dumps(body, indent=2), MESSAGE_ENCODING_UTF8)
|
1643
|
-
elif isinstance(body, str):
|
1644
|
-
appmesh_requst.body = bytes(body, MESSAGE_ENCODING_UTF8)
|
1645
|
-
elif isinstance(body, bytes):
|
1646
|
-
appmesh_requst.body = body
|
1647
|
-
else:
|
1648
|
-
raise Exception(f"UnSupported body type: {type(body)}")
|
1649
|
-
if header:
|
1650
|
-
for k, v in header.items():
|
1651
|
-
appmesh_requst.headers[k] = v
|
1652
|
-
if query:
|
1653
|
-
for k, v in query.items():
|
1654
|
-
appmesh_requst.querys[k] = v
|
1655
|
-
data = appmesh_requst.serialize()
|
1656
|
-
self.__socket_client.sendall(len(data).to_bytes(TCP_MESSAGE_HEADER_LENGTH, byteorder="big", signed=False))
|
1657
|
-
self.__socket_client.sendall(data)
|
1658
|
-
|
1659
|
-
# https://developers.google.com/protocol-buffers/docs/pythontutorial
|
1660
|
-
# https://stackoverflow.com/questions/33913308/socket-module-how-to-send-integer
|
1661
|
-
resp_data = bytes()
|
1662
|
-
resp_data = self.__recvall(int.from_bytes(self.__recvall(TCP_MESSAGE_HEADER_LENGTH), byteorder="big", signed=False))
|
1663
|
-
if resp_data is None or len(resp_data) == 0:
|
1664
|
-
self.__close_socket()
|
1665
|
-
raise Exception("socket connection broken")
|
1666
|
-
appmesh_resp = ResponseMsg().desirialize(resp_data)
|
1667
|
-
http_resp = requests.Response()
|
1668
|
-
http_resp.status_code = appmesh_resp.http_status
|
1669
|
-
http_resp._content = appmesh_resp.body.encode("utf8")
|
1670
|
-
http_resp.headers = appmesh_resp.headers
|
1671
|
-
http_resp.encoding = MESSAGE_ENCODING_UTF8
|
1672
|
-
if appmesh_resp.body_msg_type:
|
1673
|
-
http_resp.headers["Content-Type"] = appmesh_resp.body_msg_type
|
1674
|
-
return http_resp
|
1675
|
-
|
1676
|
-
########################################
|
1677
|
-
# File management
|
1678
|
-
########################################
|
1679
|
-
def file_download(self, remote_file: str, local_file: str, apply_file_attributes: bool = True) -> None:
|
1680
|
-
"""Copy a remote file to local, the local file will have the same permission as the remote file
|
1681
|
-
|
1682
|
-
Args:
|
1683
|
-
remote_file (str): the remote file path.
|
1684
|
-
local_file (str): the local file path to be downloaded.
|
1685
|
-
apply_file_attributes (bool): whether to apply file attributes (permissions, owner, group) to the local file.
|
1686
|
-
"""
|
1687
|
-
header = {"File-Path": remote_file}
|
1688
|
-
header[HTTP_HEADER_KEY_X_RECV_FILE_SOCKET] = "true"
|
1689
|
-
resp = self._request_http(AppMeshClient.Method.GET, path="/appmesh/file/download", header=header)
|
1690
|
-
|
1691
|
-
resp.raise_for_status()
|
1692
|
-
if HTTP_HEADER_KEY_X_RECV_FILE_SOCKET not in resp.headers:
|
1693
|
-
raise ValueError(f"Server did not respond with socket transfer option: {HTTP_HEADER_KEY_X_RECV_FILE_SOCKET}")
|
1694
|
-
|
1695
|
-
with open(local_file, "wb") as fp:
|
1696
|
-
chunk_data = bytes()
|
1697
|
-
chunk_size = int.from_bytes(self.__recvall(TCP_MESSAGE_HEADER_LENGTH), byteorder="big", signed=False)
|
1698
|
-
while chunk_size > 0:
|
1699
|
-
chunk_data = self.__recvall(chunk_size)
|
1700
|
-
if chunk_data is None or len(chunk_data) == 0:
|
1701
|
-
self.__close_socket()
|
1702
|
-
raise Exception("socket connection broken")
|
1703
|
-
fp.write(chunk_data)
|
1704
|
-
chunk_size = int.from_bytes(self.__recvall(TCP_MESSAGE_HEADER_LENGTH), byteorder="big", signed=False)
|
1705
|
-
|
1706
|
-
if apply_file_attributes:
|
1707
|
-
if "File-Mode" in resp.headers:
|
1708
|
-
os.chmod(path=local_file, mode=int(resp.headers["File-Mode"]))
|
1709
|
-
if "File-User" in resp.headers and "File-Group" in resp.headers:
|
1710
|
-
file_uid = int(resp.headers["File-User"])
|
1711
|
-
file_gid = int(resp.headers["File-Group"])
|
1712
|
-
try:
|
1713
|
-
os.chown(path=local_file, uid=file_uid, gid=file_gid)
|
1714
|
-
except PermissionError:
|
1715
|
-
print(f"Warning: Unable to change owner/group of {local_file}. Operation requires elevated privileges.")
|
1716
|
-
|
1717
|
-
def file_upload(self, local_file: str, remote_file: str, apply_file_attributes: bool = True) -> None:
|
1718
|
-
"""Upload a local file to the remote server, the remote file will have the same permission as the local file
|
1719
|
-
|
1720
|
-
Dependency:
|
1721
|
-
sudo apt install python3-pip
|
1722
|
-
pip3 install requests_toolbelt
|
1723
|
-
|
1724
|
-
Args:
|
1725
|
-
local_file (str): the local file path.
|
1726
|
-
remote_file (str): the target remote file to be uploaded.
|
1727
|
-
apply_file_attributes (bool): whether to upload file attributes (permissions, owner, group) along with the file.
|
1728
|
-
"""
|
1729
|
-
if not os.path.exists(local_file):
|
1730
|
-
raise FileNotFoundError(f"Local file not found: {local_file}")
|
1731
|
-
|
1732
|
-
with open(file=local_file, mode="rb") as fp:
|
1733
|
-
header = {"File-Path": remote_file, "Content-Type": "text/plain"}
|
1734
|
-
header[HTTP_HEADER_KEY_X_SEND_FILE_SOCKET] = "true"
|
1735
|
-
|
1736
|
-
if apply_file_attributes:
|
1737
|
-
file_stat = os.stat(local_file)
|
1738
|
-
header["File-Mode"] = str(file_stat.st_mode & 0o777) # Mask to keep only permission bits
|
1739
|
-
header["File-User"] = str(file_stat.st_uid)
|
1740
|
-
header["File-Group"] = str(file_stat.st_gid)
|
1741
|
-
|
1742
|
-
# https://stackoverflow.com/questions/22567306/python-requests-file-upload
|
1743
|
-
resp = self._request_http(AppMeshClient.Method.POST, path="/appmesh/file/upload", header=header)
|
1744
|
-
|
1745
|
-
resp.raise_for_status()
|
1746
|
-
if HTTP_HEADER_KEY_X_SEND_FILE_SOCKET not in resp.headers:
|
1747
|
-
raise ValueError(f"Server did not respond with socket transfer option: {HTTP_HEADER_KEY_X_SEND_FILE_SOCKET}")
|
1748
|
-
|
1749
|
-
chunk_size = TCP_CHUNK_BLOCK_SIZE
|
1750
|
-
while True:
|
1751
|
-
chunk_data = fp.read(chunk_size)
|
1752
|
-
if not chunk_data:
|
1753
|
-
self.__socket_client.sendall((0).to_bytes(TCP_MESSAGE_HEADER_LENGTH, byteorder="big", signed=False))
|
1754
|
-
break
|
1755
|
-
self.__socket_client.sendall(len(chunk_data).to_bytes(TCP_MESSAGE_HEADER_LENGTH, byteorder="big", signed=False))
|
1756
|
-
self.__socket_client.sendall(chunk_data)
|