appmesh 1.3.2__py3-none-any.whl → 1.3.4__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/appmesh_client.py +278 -163
- {appmesh-1.3.2.dist-info → appmesh-1.3.4.dist-info}/METADATA +5 -6
- appmesh-1.3.4.dist-info/RECORD +6 -0
- {appmesh-1.3.2.dist-info → appmesh-1.3.4.dist-info}/WHEEL +1 -1
- appmesh-1.3.2.dist-info/RECORD +0 -6
- {appmesh-1.3.2.dist-info → appmesh-1.3.4.dist-info}/top_level.txt +0 -0
appmesh/appmesh_client.py
CHANGED
@@ -24,59 +24,48 @@ import requests
|
|
24
24
|
DEFAULT_TOKEN_EXPIRE_SECONDS = "P1W" # default 7 day(s)
|
25
25
|
DEFAULT_RUN_APP_TIMEOUT_SECONDS = "P2D" # 2 days
|
26
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
|
+
|
27
32
|
REST_TEXT_MESSAGE_JSON_KEY = "message"
|
28
33
|
MESSAGE_ENCODING_UTF8 = "utf-8"
|
29
34
|
TCP_MESSAGE_HEADER_LENGTH = 4
|
30
|
-
|
31
|
-
_SSL_CLIENT_PEM_FILE = "/opt/appmesh/ssl/client.pem"
|
32
|
-
_SSL_CLIENT_PEM_KEY_FILE = "/opt/appmesh/ssl/client-key.pem"
|
33
|
-
HTTP_USER_AGENT_HEADER_NAME = "User-Agent"
|
35
|
+
|
34
36
|
HTTP_USER_AGENT = "appmesh/python"
|
35
37
|
HTTP_USER_AGENT_TCP = "appmesh/python/tcp"
|
38
|
+
HTTP_HEADER_KEY_USER_AGENT = "User-Agent"
|
36
39
|
HTTP_HEADER_KEY_X_SEND_FILE_SOCKET = "X-Send-File-Socket"
|
37
40
|
HTTP_HEADER_KEY_X_RECV_FILE_SOCKET = "X-Recv-File-Socket"
|
38
41
|
HTTP_HEADER_KEY_X_TARGET_HOST = "X-Target-Host"
|
39
42
|
|
40
43
|
|
41
|
-
def _get_str_item(data: dict, key):
|
42
|
-
return data[key] if (data and key in data and data[key] and isinstance(data[key], str)) else None
|
43
|
-
|
44
|
-
|
45
|
-
def _get_int_item(data: dict, key):
|
46
|
-
return int(data[key]) if (data and key in data and data[key] and isinstance(data[key], int)) else None
|
47
|
-
|
48
|
-
|
49
|
-
def _get_bool_item(data: dict, key):
|
50
|
-
return bool(data[key]) if (data and key in data and data[key]) else None
|
51
|
-
|
52
|
-
|
53
|
-
def _get_native_item(data: dict, key):
|
54
|
-
return copy.deepcopy(data[key]) if (data and key in data and data[key]) else None
|
55
|
-
|
56
|
-
|
57
|
-
class AppOutput(object):
|
58
|
-
"""App output object for app_output() method"""
|
59
|
-
|
60
|
-
def __init__(self, status_code: HTTPStatus, output: str, out_position: Optional[int], exit_code: Optional[int]) -> None:
|
61
|
-
|
62
|
-
self.status_code = status_code
|
63
|
-
"""HTTP status code"""
|
64
|
-
|
65
|
-
self.output = output
|
66
|
-
"""HTTP response text"""
|
67
|
-
|
68
|
-
self.out_position = out_position
|
69
|
-
"""Current read position (int or None)"""
|
70
|
-
|
71
|
-
self.exit_code = exit_code
|
72
|
-
"""Process exit code (int or None)"""
|
73
|
-
|
74
|
-
|
75
44
|
class App(object):
|
76
45
|
"""
|
77
46
|
App object present an application in App Mesh
|
78
47
|
"""
|
79
48
|
|
49
|
+
@staticmethod
|
50
|
+
def _get_str_item(data: dict, key) -> Optional[str]:
|
51
|
+
"""Retrieve a string value from a dictionary by key, if it exists and is a valid string."""
|
52
|
+
return data[key] if (data and key in data and data[key] and isinstance(data[key], str)) else None
|
53
|
+
|
54
|
+
@staticmethod
|
55
|
+
def _get_int_item(data: dict, key) -> Optional[int]:
|
56
|
+
"""Retrieve an integer value from a dictionary by key, if it exists and is a valid integer."""
|
57
|
+
return int(data[key]) if (data and key in data and data[key] and isinstance(data[key], int)) else None
|
58
|
+
|
59
|
+
@staticmethod
|
60
|
+
def _get_bool_item(data: dict, key) -> Optional[bool]:
|
61
|
+
"""Retrieve a boolean value from a dictionary by key, if it exists."""
|
62
|
+
return bool(data[key]) if (data and key in data and data[key]) else None
|
63
|
+
|
64
|
+
@staticmethod
|
65
|
+
def _get_native_item(data: dict, key) -> Optional[object]:
|
66
|
+
"""Retrieve a deep copy of a value from a dictionary by key, if it exists."""
|
67
|
+
return copy.deepcopy(data[key]) if (data and key in data and data[key]) else None
|
68
|
+
|
80
69
|
@unique
|
81
70
|
class Permission(Enum):
|
82
71
|
"""Application permission definition"""
|
@@ -103,10 +92,10 @@ class App(object):
|
|
103
92
|
if isinstance(data, (str, bytes, bytearray)):
|
104
93
|
data = json.loads(data)
|
105
94
|
|
106
|
-
self.exit = _get_str_item(data, "exit")
|
95
|
+
self.exit = App._get_str_item(data, "exit")
|
107
96
|
"""default exit behavior [restart,standby,keepalive,remove]"""
|
108
97
|
|
109
|
-
self.control = _get_native_item(data, "control") if _get_native_item(data, "control") else dict()
|
98
|
+
self.control = App._get_native_item(data, "control") if App._get_native_item(data, "control") else dict()
|
110
99
|
"""exit code behavior (e.g, --control 0:restart --control 1:standby), higher priority than default exit behavior"""
|
111
100
|
|
112
101
|
def set_exit_behavior(self, a: Action) -> None:
|
@@ -126,10 +115,10 @@ class App(object):
|
|
126
115
|
if isinstance(data, (str, bytes, bytearray)):
|
127
116
|
data = json.loads(data)
|
128
117
|
|
129
|
-
self.daily_start = _get_int_item(data, "daily_start")
|
118
|
+
self.daily_start = App._get_int_item(data, "daily_start")
|
130
119
|
"""daily start time (e.g., '09:00:00+08')"""
|
131
120
|
|
132
|
-
self.daily_end = _get_int_item(data, "daily_end")
|
121
|
+
self.daily_end = App._get_int_item(data, "daily_end")
|
133
122
|
"""daily end time (e.g., '20:00:00+08')"""
|
134
123
|
|
135
124
|
def set_daily_range(self, start: datetime, end: datetime) -> None:
|
@@ -146,13 +135,13 @@ class App(object):
|
|
146
135
|
if isinstance(data, (str, bytes, bytearray)):
|
147
136
|
data = json.loads(data)
|
148
137
|
|
149
|
-
self.cpu_shares = _get_int_item(data, "cpu_shares")
|
138
|
+
self.cpu_shares = App._get_int_item(data, "cpu_shares")
|
150
139
|
"""CPU shares (relative weight)"""
|
151
140
|
|
152
|
-
self.memory_mb = _get_int_item(data, "memory_mb")
|
141
|
+
self.memory_mb = App._get_int_item(data, "memory_mb")
|
153
142
|
"""physical memory limit in MByte"""
|
154
143
|
|
155
|
-
self.memory_virt_mb = _get_int_item(data, "memory_virt_mb")
|
144
|
+
self.memory_virt_mb = App._get_int_item(data, "memory_virt_mb")
|
156
145
|
"""virtual memory limit in MByte"""
|
157
146
|
|
158
147
|
def __init__(self, data=None):
|
@@ -165,59 +154,59 @@ class App(object):
|
|
165
154
|
if isinstance(data, (str, bytes, bytearray)):
|
166
155
|
data = json.loads(data)
|
167
156
|
|
168
|
-
self.name = _get_str_item(data, "name")
|
157
|
+
self.name = App._get_str_item(data, "name")
|
169
158
|
"""application name (unique)"""
|
170
159
|
|
171
|
-
self.command = _get_str_item(data, "command")
|
160
|
+
self.command = App._get_str_item(data, "command")
|
172
161
|
"""full command line with arguments"""
|
173
162
|
|
174
|
-
self.shell = _get_bool_item(data, "shell")
|
163
|
+
self.shell = App._get_bool_item(data, "shell")
|
175
164
|
"""use shell mode, cmd can be more shell commands with string format"""
|
176
165
|
|
177
|
-
self.session_login = _get_bool_item(data, "session_login")
|
166
|
+
self.session_login = App._get_bool_item(data, "session_login")
|
178
167
|
"""app run in session login mode"""
|
179
168
|
|
180
|
-
self.description = _get_str_item(data, "description")
|
169
|
+
self.description = App._get_str_item(data, "description")
|
181
170
|
"""application description string"""
|
182
171
|
|
183
|
-
self.metadata = _get_native_item(data, "metadata")
|
172
|
+
self.metadata = App._get_native_item(data, "metadata")
|
184
173
|
"""metadata string/JSON (input for application, pass to process stdin)"""
|
185
174
|
|
186
|
-
self.working_dir = _get_str_item(data, "working_dir")
|
175
|
+
self.working_dir = App._get_str_item(data, "working_dir")
|
187
176
|
"""working directory"""
|
188
177
|
|
189
|
-
self.status = _get_int_item(data, "status")
|
178
|
+
self.status = App._get_int_item(data, "status")
|
190
179
|
"""initial application status (true is enable, false is disabled)"""
|
191
180
|
|
192
|
-
self.docker_image = _get_str_item(data, "docker_image")
|
181
|
+
self.docker_image = App._get_str_item(data, "docker_image")
|
193
182
|
"""docker image which used to run command line (for docker container application)"""
|
194
183
|
|
195
|
-
self.stdout_cache_num = _get_int_item(data, "stdout_cache_num")
|
184
|
+
self.stdout_cache_num = App._get_int_item(data, "stdout_cache_num")
|
196
185
|
"""stdout file cache number"""
|
197
186
|
|
198
|
-
self.start_time = _get_int_item(data, "start_time")
|
187
|
+
self.start_time = App._get_int_item(data, "start_time")
|
199
188
|
"""start date time for app (ISO8601 time format, e.g., '2020-10-11T09:22:05')"""
|
200
189
|
|
201
|
-
self.end_time = _get_int_item(data, "end_time")
|
190
|
+
self.end_time = App._get_int_item(data, "end_time")
|
202
191
|
"""end date time for app (ISO8601 time format, e.g., '2020-10-11T10:22:05')"""
|
203
192
|
|
204
|
-
self.interval = _get_int_item(data, "interval")
|
193
|
+
self.interval = App._get_int_item(data, "interval")
|
205
194
|
"""start interval seconds for short running app, support ISO 8601 durations and cron expression (e.g., 'P1Y2M3DT4H5M6S' 'P5W' '* */5 * * * *')"""
|
206
195
|
|
207
|
-
self.cron = _get_bool_item(data, "cron")
|
196
|
+
self.cron = App._get_bool_item(data, "cron")
|
208
197
|
"""indicate interval parameter use cron expression or not"""
|
209
198
|
|
210
|
-
self.daily_limitation = App.DailyLimitation(_get_native_item(data, "daily_limitation"))
|
199
|
+
self.daily_limitation = App.DailyLimitation(App._get_native_item(data, "daily_limitation"))
|
211
200
|
|
212
|
-
self.retention = _get_str_item(data, "retention")
|
201
|
+
self.retention = App._get_str_item(data, "retention")
|
213
202
|
"""extra timeout seconds for stopping current process, support ISO 8601 durations (e.g., 'P1Y2M3DT4H5M6S' 'P5W')."""
|
214
203
|
|
215
|
-
self.health_check_cmd = _get_str_item(data, "health_check_cmd")
|
204
|
+
self.health_check_cmd = App._get_str_item(data, "health_check_cmd")
|
216
205
|
"""health check script command (e.g., sh -x 'curl host:port/health', return 0 is health)"""
|
217
206
|
|
218
|
-
self.permission = _get_int_item(data, "permission")
|
207
|
+
self.permission = App._get_int_item(data, "permission")
|
219
208
|
"""application user permission, value is 2 bit integer: [group & other], each bit can be deny:1, read:2, write: 3."""
|
220
|
-
self.behavior = App.Behavior(_get_native_item(data, "behavior"))
|
209
|
+
self.behavior = App.Behavior(App._get_native_item(data, "behavior"))
|
221
210
|
|
222
211
|
self.env = dict()
|
223
212
|
"""environment variables (e.g., -e env1=value1 -e env2=value2, APP_DOCKER_OPTS is used to input docker run parameters)"""
|
@@ -231,32 +220,32 @@ class App(object):
|
|
231
220
|
for k, v in data["sec_env"].items():
|
232
221
|
self.sec_env[k] = v
|
233
222
|
|
234
|
-
self.pid = _get_int_item(data, "pid")
|
223
|
+
self.pid = App._get_int_item(data, "pid")
|
235
224
|
"""process id used to attach to the running process"""
|
236
|
-
self.resource_limit = App.ResourceLimitation(_get_native_item(data, "resource_limit"))
|
225
|
+
self.resource_limit = App.ResourceLimitation(App._get_native_item(data, "resource_limit"))
|
237
226
|
|
238
227
|
# readonly attributes
|
239
|
-
self.owner = _get_str_item(data, "owner")
|
228
|
+
self.owner = App._get_str_item(data, "owner")
|
240
229
|
"""owner name"""
|
241
|
-
self.pstree = _get_str_item(data, "pstree")
|
230
|
+
self.pstree = App._get_str_item(data, "pstree")
|
242
231
|
"""process tree"""
|
243
|
-
self.container_id = _get_str_item(data, "container_id")
|
232
|
+
self.container_id = App._get_str_item(data, "container_id")
|
244
233
|
"""container id"""
|
245
|
-
self.memory = _get_int_item(data, "memory")
|
234
|
+
self.memory = App._get_int_item(data, "memory")
|
246
235
|
"""memory usage"""
|
247
|
-
self.cpu = _get_int_item(data, "cpu")
|
236
|
+
self.cpu = App._get_int_item(data, "cpu")
|
248
237
|
"""cpu usage"""
|
249
|
-
self.fd = _get_int_item(data, "fd")
|
238
|
+
self.fd = App._get_int_item(data, "fd")
|
250
239
|
"""file descriptor usage"""
|
251
|
-
self.last_start_time = _get_int_item(data, "last_start_time")
|
240
|
+
self.last_start_time = App._get_int_item(data, "last_start_time")
|
252
241
|
"""last start time"""
|
253
|
-
self.last_exit_time = _get_int_item(data, "last_exit_time")
|
242
|
+
self.last_exit_time = App._get_int_item(data, "last_exit_time")
|
254
243
|
"""last exit time"""
|
255
|
-
self.health = _get_int_item(data, "health")
|
244
|
+
self.health = App._get_int_item(data, "health")
|
256
245
|
"""health status"""
|
257
|
-
self.version = _get_int_item(data, "version")
|
246
|
+
self.version = App._get_int_item(data, "version")
|
258
247
|
"""version number"""
|
259
|
-
self.return_code = _get_int_item(data, "return_code")
|
248
|
+
self.return_code = App._get_int_item(data, "return_code")
|
260
249
|
"""last exit code"""
|
261
250
|
|
262
251
|
def set_valid_time(self, start: datetime, end: datetime) -> None:
|
@@ -300,6 +289,24 @@ class App(object):
|
|
300
289
|
return output
|
301
290
|
|
302
291
|
|
292
|
+
class AppOutput(object):
|
293
|
+
"""App output information"""
|
294
|
+
|
295
|
+
def __init__(self, status_code: HTTPStatus, output: str, out_position: Optional[int], exit_code: Optional[int]) -> None:
|
296
|
+
|
297
|
+
self.status_code = status_code
|
298
|
+
"""HTTP status code"""
|
299
|
+
|
300
|
+
self.output = output
|
301
|
+
"""HTTP response text"""
|
302
|
+
|
303
|
+
self.out_position = out_position
|
304
|
+
"""Current read position (int or None)"""
|
305
|
+
|
306
|
+
self.exit_code = exit_code
|
307
|
+
"""Process exit code (int or None)"""
|
308
|
+
|
309
|
+
|
303
310
|
class AppRun(object):
|
304
311
|
"""
|
305
312
|
Application run object indicate to a remote run from run_async()
|
@@ -340,10 +347,81 @@ class AppRun(object):
|
|
340
347
|
|
341
348
|
|
342
349
|
class AppMeshClient(metaclass=abc.ABCMeta):
|
343
|
-
"""
|
344
|
-
|
345
|
-
|
346
|
-
|
350
|
+
"""
|
351
|
+
Client SDK for interacting with the App Mesh service via REST API.
|
352
|
+
|
353
|
+
The `AppMeshClient` class provides a comprehensive interface for managing and monitoring distributed applications
|
354
|
+
within the App Mesh ecosystem. It enables communication with the App Mesh REST API for operations such as
|
355
|
+
application lifecycle management, monitoring, and configuration.
|
356
|
+
|
357
|
+
This client is designed for direct usage in applications that require access to App Mesh services over HTTP-based REST.
|
358
|
+
|
359
|
+
Usage:
|
360
|
+
- Install the App Mesh Python package:
|
361
|
+
python3 -m pip install --upgrade appmesh
|
362
|
+
- Import the client module:
|
363
|
+
from appmesh import appmesh_client
|
364
|
+
|
365
|
+
Example:
|
366
|
+
client = appmesh_client.AppMeshClient()
|
367
|
+
client.login("your-name", "your-password")
|
368
|
+
response = client.app_view(app_name='ping')
|
369
|
+
|
370
|
+
Attributes:
|
371
|
+
- TLS (Transport Layer Security): Supports secure connections between the client and App Mesh service,
|
372
|
+
ensuring encrypted communication.
|
373
|
+
- JWT (JSON Web Token) and RBAC (Role-Based Access Control): Provides secure API access with
|
374
|
+
token-based authentication and authorization to enforce fine-grained permissions.
|
375
|
+
|
376
|
+
Methods:
|
377
|
+
- login()
|
378
|
+
- logoff()
|
379
|
+
- authentication()
|
380
|
+
- renew()
|
381
|
+
- totp_disable()
|
382
|
+
- totp_secret()
|
383
|
+
- totp_setup()
|
384
|
+
|
385
|
+
- app_add()
|
386
|
+
- app_delete()
|
387
|
+
- app_disable()
|
388
|
+
- app_enable()
|
389
|
+
- app_health()
|
390
|
+
- app_output()
|
391
|
+
- app_view()
|
392
|
+
- app_view_all()
|
393
|
+
|
394
|
+
- run_async()
|
395
|
+
- run_async_wait()
|
396
|
+
- run_sync()
|
397
|
+
|
398
|
+
- config_set()
|
399
|
+
- config_view()
|
400
|
+
- log_level_set()
|
401
|
+
- host_resource()
|
402
|
+
- forwarding_host
|
403
|
+
- metrics()
|
404
|
+
|
405
|
+
- tag_add()
|
406
|
+
- tag_delete()
|
407
|
+
- tag_view()
|
408
|
+
|
409
|
+
- file_download()
|
410
|
+
- file_upload()
|
411
|
+
|
412
|
+
- user_add()
|
413
|
+
- user_delete()
|
414
|
+
- user_lock()
|
415
|
+
- user_passwd_update()
|
416
|
+
- user_self()
|
417
|
+
- user_unlock()
|
418
|
+
- users_view()
|
419
|
+
- permissions_for_user()
|
420
|
+
- permissions_view()
|
421
|
+
- role_delete()
|
422
|
+
- role_update()
|
423
|
+
- roles_view()
|
424
|
+
- groups_view()
|
347
425
|
"""
|
348
426
|
|
349
427
|
@unique
|
@@ -359,8 +437,8 @@ class AppMeshClient(metaclass=abc.ABCMeta):
|
|
359
437
|
def __init__(
|
360
438
|
self,
|
361
439
|
rest_url: str = "https://127.0.0.1:6060",
|
362
|
-
rest_ssl_verify=
|
363
|
-
rest_ssl_client_cert=(
|
440
|
+
rest_ssl_verify=DEFAULT_SSL_CA_PEM_FILE if os.path.exists(DEFAULT_SSL_CA_PEM_FILE) else False,
|
441
|
+
rest_ssl_client_cert=(DEFAULT_SSL_CLIENT_PEM_FILE, DEFAULT_SSL_CLIENT_PEM_KEY_FILE) if os.path.exists(DEFAULT_SSL_CLIENT_PEM_FILE) else None,
|
364
442
|
rest_timeout=(60, 300),
|
365
443
|
jwt_token=None,
|
366
444
|
):
|
@@ -475,6 +553,7 @@ class AppMeshClient(metaclass=abc.ABCMeta):
|
|
475
553
|
resp = self._request_http(AppMeshClient.Method.POST, path="/appmesh/self/logoff")
|
476
554
|
if resp.status_code != HTTPStatus.OK:
|
477
555
|
raise Exception(resp.text)
|
556
|
+
self.jwt_token = None
|
478
557
|
return resp.status_code == HTTPStatus.OK
|
479
558
|
|
480
559
|
def authentication(self, token: str, permission=None) -> bool:
|
@@ -662,7 +741,7 @@ class AppMeshClient(metaclass=abc.ABCMeta):
|
|
662
741
|
return AppOutput(status_code=resp.status_code, output=resp.text, out_position=out_position, exit_code=exit_code)
|
663
742
|
|
664
743
|
def app_health(self, app_name: str) -> bool:
|
665
|
-
"""Get application health status
|
744
|
+
"""Get application health status
|
666
745
|
|
667
746
|
Args:
|
668
747
|
app_name (str): the application name.
|
@@ -1142,37 +1221,37 @@ class AppMeshClient(metaclass=abc.ABCMeta):
|
|
1142
1221
|
########################################
|
1143
1222
|
# File management
|
1144
1223
|
########################################
|
1145
|
-
def file_download(self, file_path: str, local_file: str) ->
|
1146
|
-
"""Copy a remote file to local, the local file will have the same permission as the remote file
|
1224
|
+
def file_download(self, file_path: str, local_file: str, apply_file_attributes: bool = True) -> None:
|
1225
|
+
"""Copy a remote file to local. Optionally, the local file will have the same permission as the remote file.
|
1147
1226
|
|
1148
1227
|
Args:
|
1149
1228
|
file_path (str): the remote file path.
|
1150
1229
|
local_file (str): the local file path to be downloaded.
|
1151
|
-
|
1152
|
-
Returns:
|
1153
|
-
bool: success or failure.
|
1230
|
+
apply_file_attributes (bool): whether to apply file attributes (permissions, owner, group) to the local file.
|
1154
1231
|
"""
|
1155
1232
|
resp = self._request_http(AppMeshClient.Method.GET, path="/appmesh/file/download", header={"File-Path": file_path})
|
1156
|
-
|
1157
|
-
raise Exception(resp.text)
|
1233
|
+
resp.raise_for_status()
|
1158
1234
|
|
1235
|
+
# Write the file content locally
|
1159
1236
|
with open(local_file, "wb") as fp:
|
1160
|
-
for chunk in resp.iter_content(chunk_size=
|
1237
|
+
for chunk in resp.iter_content(chunk_size=8192): # 8 KB
|
1161
1238
|
if chunk:
|
1162
1239
|
fp.write(chunk)
|
1163
|
-
if "File-Mode" in resp.headers:
|
1164
|
-
os.chmod(path=local_file, mode=int(resp.headers["File-Mode"]))
|
1165
|
-
if "File-User" in resp.headers and "File-Group" in resp.headers:
|
1166
|
-
file_uid = int(resp.headers["File-User"])
|
1167
|
-
file_gid = int(resp.headers["File-Group"])
|
1168
|
-
try:
|
1169
|
-
os.chown(path=local_file, uid=file_uid, gid=file_gid)
|
1170
|
-
except Exception as ex:
|
1171
|
-
print(ex)
|
1172
|
-
return resp.status_code == HTTPStatus.OK
|
1173
1240
|
|
1174
|
-
|
1175
|
-
|
1241
|
+
# Apply file attributes (permissions, owner, group) if requested
|
1242
|
+
if apply_file_attributes:
|
1243
|
+
if "File-Mode" in resp.headers:
|
1244
|
+
os.chmod(path=local_file, mode=int(resp.headers["File-Mode"]))
|
1245
|
+
if "File-User" in resp.headers and "File-Group" in resp.headers:
|
1246
|
+
file_uid = int(resp.headers["File-User"])
|
1247
|
+
file_gid = int(resp.headers["File-Group"])
|
1248
|
+
try:
|
1249
|
+
os.chown(path=local_file, uid=file_uid, gid=file_gid)
|
1250
|
+
except PermissionError:
|
1251
|
+
print(f"Warning: Unable to change owner/group of {local_file}. Operation requires elevated privileges.")
|
1252
|
+
|
1253
|
+
def file_upload(self, local_file: str, file_path: str, apply_file_attributes: bool = True) -> None:
|
1254
|
+
"""Upload a local file to the remote server. Optionally, the remote file will have the same permission as the local file.
|
1176
1255
|
|
1177
1256
|
Dependency:
|
1178
1257
|
sudo apt install python3-pip
|
@@ -1181,21 +1260,25 @@ class AppMeshClient(metaclass=abc.ABCMeta):
|
|
1181
1260
|
Args:
|
1182
1261
|
local_file (str): the local file path.
|
1183
1262
|
file_path (str): the target remote file to be uploaded.
|
1184
|
-
|
1185
|
-
Returns:
|
1186
|
-
bool: success or failure.
|
1263
|
+
apply_file_attributes (bool): whether to upload file attributes (permissions, owner, group) along with the file.
|
1187
1264
|
"""
|
1265
|
+
if not os.path.exists(local_file):
|
1266
|
+
raise FileNotFoundError(f"Local file not found: {local_file}")
|
1267
|
+
|
1188
1268
|
from requests_toolbelt import MultipartEncoder
|
1189
1269
|
|
1190
1270
|
with open(file=local_file, mode="rb") as fp:
|
1191
1271
|
encoder = MultipartEncoder(fields={"filename": os.path.basename(file_path), "file": ("filename", fp, "application/octet-stream")})
|
1192
|
-
|
1193
|
-
|
1194
|
-
|
1195
|
-
|
1196
|
-
|
1197
|
-
|
1198
|
-
|
1272
|
+
header = {"File-Path": file_path, "Content-Type": encoder.content_type}
|
1273
|
+
|
1274
|
+
# Include file attributes (permissions, owner, group) if requested
|
1275
|
+
if apply_file_attributes:
|
1276
|
+
file_stat = os.stat(local_file)
|
1277
|
+
header["File-Mode"] = str(file_stat.st_mode)
|
1278
|
+
header["File-User"] = str(file_stat.st_uid)
|
1279
|
+
header["File-Group"] = str(file_stat.st_gid)
|
1280
|
+
|
1281
|
+
# Upload file with or without attributes
|
1199
1282
|
# https://stackoverflow.com/questions/22567306/python-requests-file-upload
|
1200
1283
|
resp = self._request_http(
|
1201
1284
|
AppMeshClient.Method.POST_STREAM,
|
@@ -1203,9 +1286,7 @@ class AppMeshClient(metaclass=abc.ABCMeta):
|
|
1203
1286
|
header=header,
|
1204
1287
|
body=encoder,
|
1205
1288
|
)
|
1206
|
-
|
1207
|
-
raise Exception(resp.text)
|
1208
|
-
return True
|
1289
|
+
resp.raise_for_status()
|
1209
1290
|
|
1210
1291
|
########################################
|
1211
1292
|
# Application run
|
@@ -1340,7 +1421,7 @@ class AppMeshClient(metaclass=abc.ABCMeta):
|
|
1340
1421
|
header[HTTP_HEADER_KEY_X_TARGET_HOST] = self.forwarding_host
|
1341
1422
|
else:
|
1342
1423
|
header[HTTP_HEADER_KEY_X_TARGET_HOST] = self.forwarding_host + ":" + str(parse.urlsplit(self.server_url).port)
|
1343
|
-
header[
|
1424
|
+
header[HTTP_HEADER_KEY_USER_AGENT] = HTTP_USER_AGENT
|
1344
1425
|
|
1345
1426
|
if method is AppMeshClient.Method.GET:
|
1346
1427
|
return requests.get(url=rest_url, params=query, headers=header, cert=self.ssl_client_cert, verify=self.ssl_verify, timeout=self.rest_timeout)
|
@@ -1359,15 +1440,42 @@ class AppMeshClient(metaclass=abc.ABCMeta):
|
|
1359
1440
|
|
1360
1441
|
|
1361
1442
|
class AppMeshClientTCP(AppMeshClient):
|
1362
|
-
"""
|
1443
|
+
"""
|
1444
|
+
Client SDK for interacting with the App Mesh service over TCP, with enhanced support for large file transfers.
|
1445
|
+
|
1446
|
+
The `AppMeshClientTCP` class extends the functionality of `AppMeshClient` by offering a TCP-based communication layer
|
1447
|
+
for the App Mesh REST API. It overrides the file download and upload methods to support large file transfers with
|
1448
|
+
improved performance, leveraging TCP for lower latency and higher throughput compared to HTTP.
|
1449
|
+
|
1450
|
+
This client is suitable for applications requiring efficient data transfers and high-throughput operations within the
|
1451
|
+
App Mesh ecosystem, while maintaining compatibility with all other attributes and methods from `AppMeshClient`.
|
1363
1452
|
|
1364
1453
|
Dependency:
|
1365
|
-
|
1454
|
+
- Install the required package for message serialization:
|
1455
|
+
pip3 install msgpack
|
1456
|
+
|
1457
|
+
Usage:
|
1458
|
+
- Import the client module:
|
1459
|
+
from appmesh import appmesh_client
|
1460
|
+
|
1461
|
+
Example:
|
1462
|
+
client = appmesh_client.AppMeshClientTCP()
|
1463
|
+
client.login("your-name", "your-password")
|
1464
|
+
client.file_download("/tmp/os-release", "os-release")
|
1465
|
+
|
1466
|
+
Attributes:
|
1467
|
+
- Inherits all attributes from `AppMeshClient`, including TLS secure connections and JWT-based authentication.
|
1468
|
+
- Optimized for TCP-based communication to provide better performance for large file transfers.
|
1469
|
+
|
1470
|
+
Methods:
|
1471
|
+
- file_download()
|
1472
|
+
- file_upload()
|
1473
|
+
- Inherits all other methods from `AppMeshClient`, providing a consistent interface for managing applications within App Mesh.
|
1366
1474
|
"""
|
1367
1475
|
|
1368
1476
|
def __init__(
|
1369
1477
|
self,
|
1370
|
-
rest_ssl_verify=
|
1478
|
+
rest_ssl_verify=DEFAULT_SSL_CA_PEM_FILE if os.path.exists(DEFAULT_SSL_CA_PEM_FILE) else False,
|
1371
1479
|
rest_ssl_client_cert=None,
|
1372
1480
|
jwt_token=None,
|
1373
1481
|
tcp_address=("localhost", 6059),
|
@@ -1505,7 +1613,7 @@ class AppMeshClientTCP(AppMeshClient):
|
|
1505
1613
|
appmesh_requst.headers["Authorization"] = "Bearer " + super().jwt_token
|
1506
1614
|
if super().forwarding_host and len(super().forwarding_host) > 0:
|
1507
1615
|
raise Exception("Not support forward request in TCP mode")
|
1508
|
-
appmesh_requst.headers[
|
1616
|
+
appmesh_requst.headers[HTTP_HEADER_KEY_USER_AGENT] = HTTP_USER_AGENT_TCP
|
1509
1617
|
appmesh_requst.uuid = str(uuid.uuid1())
|
1510
1618
|
appmesh_requst.http_method = method.value
|
1511
1619
|
appmesh_requst.request_uri = path
|
@@ -1549,31 +1657,34 @@ class AppMeshClientTCP(AppMeshClient):
|
|
1549
1657
|
########################################
|
1550
1658
|
# File management
|
1551
1659
|
########################################
|
1552
|
-
def file_download(self, file_path: str, local_file: str) ->
|
1660
|
+
def file_download(self, file_path: str, local_file: str, apply_file_attributes: bool = True) -> None:
|
1553
1661
|
"""Copy a remote file to local, the local file will have the same permission as the remote file
|
1554
1662
|
|
1555
1663
|
Args:
|
1556
1664
|
file_path (str): the remote file path.
|
1557
1665
|
local_file (str): the local file path to be downloaded.
|
1558
|
-
|
1559
|
-
Returns:
|
1560
|
-
bool: success or failure.
|
1666
|
+
apply_file_attributes (bool): whether to apply file attributes (permissions, owner, group) to the local file.
|
1561
1667
|
"""
|
1562
|
-
header = {}
|
1563
|
-
header["File-Path"] = file_path
|
1668
|
+
header = {"File-Path": file_path}
|
1564
1669
|
header[HTTP_HEADER_KEY_X_RECV_FILE_SOCKET] = "true"
|
1565
1670
|
resp = self._request_http(AppMeshClient.Method.GET, path="/appmesh/file/download", header=header)
|
1566
|
-
|
1567
|
-
|
1568
|
-
|
1671
|
+
|
1672
|
+
resp.raise_for_status()
|
1673
|
+
if HTTP_HEADER_KEY_X_RECV_FILE_SOCKET not in resp.headers:
|
1674
|
+
raise ValueError(f"Server did not respond with socket transfer option: {HTTP_HEADER_KEY_X_RECV_FILE_SOCKET}")
|
1675
|
+
|
1676
|
+
with open(local_file, "wb") as fp:
|
1677
|
+
chunk_data = bytes()
|
1678
|
+
chunk_size = int.from_bytes(self.__recvall(TCP_MESSAGE_HEADER_LENGTH), byteorder="big", signed=False)
|
1679
|
+
while chunk_size > 0:
|
1680
|
+
chunk_data = self.__recvall(chunk_size)
|
1681
|
+
if chunk_data is None or len(chunk_data) == 0:
|
1682
|
+
self.__close_socket()
|
1683
|
+
raise Exception("socket connection broken")
|
1684
|
+
fp.write(chunk_data)
|
1569
1685
|
chunk_size = int.from_bytes(self.__recvall(TCP_MESSAGE_HEADER_LENGTH), byteorder="big", signed=False)
|
1570
|
-
|
1571
|
-
|
1572
|
-
if chunk_data is None or len(chunk_data) == 0:
|
1573
|
-
self.__close_socket()
|
1574
|
-
raise Exception("socket connection broken")
|
1575
|
-
fp.write(chunk_data)
|
1576
|
-
chunk_size = int.from_bytes(self.__recvall(TCP_MESSAGE_HEADER_LENGTH), byteorder="big", signed=False)
|
1686
|
+
|
1687
|
+
if apply_file_attributes:
|
1577
1688
|
if "File-Mode" in resp.headers:
|
1578
1689
|
os.chmod(path=local_file, mode=int(resp.headers["File-Mode"]))
|
1579
1690
|
if "File-User" in resp.headers and "File-Group" in resp.headers:
|
@@ -1581,12 +1692,10 @@ class AppMeshClientTCP(AppMeshClient):
|
|
1581
1692
|
file_gid = int(resp.headers["File-Group"])
|
1582
1693
|
try:
|
1583
1694
|
os.chown(path=local_file, uid=file_uid, gid=file_gid)
|
1584
|
-
except
|
1585
|
-
print(
|
1586
|
-
return True
|
1587
|
-
return False
|
1695
|
+
except PermissionError:
|
1696
|
+
print(f"Warning: Unable to change owner/group of {local_file}. Operation requires elevated privileges.")
|
1588
1697
|
|
1589
|
-
def file_upload(self, local_file: str, file_path: str):
|
1698
|
+
def file_upload(self, local_file: str, file_path: str, apply_file_attributes: bool = True) -> None:
|
1590
1699
|
"""Upload a local file to the remote server, the remote file will have the same permission as the local file
|
1591
1700
|
|
1592
1701
|
Dependency:
|
@@ -1596,29 +1705,35 @@ class AppMeshClientTCP(AppMeshClient):
|
|
1596
1705
|
Args:
|
1597
1706
|
local_file (str): the local file path.
|
1598
1707
|
file_path (str): the target remote file to be uploaded.
|
1599
|
-
|
1600
|
-
Returns:
|
1601
|
-
bool: success or failure.
|
1602
|
-
str: text message.
|
1708
|
+
apply_file_attributes (bool): whether to upload file attributes (permissions, owner, group) along with the file.
|
1603
1709
|
"""
|
1710
|
+
if not os.path.exists(local_file):
|
1711
|
+
raise FileNotFoundError(f"Local file not found: {local_file}")
|
1712
|
+
|
1604
1713
|
with open(file=local_file, mode="rb") as fp:
|
1605
|
-
|
1606
|
-
header = {}
|
1607
|
-
header["File-Path"] = file_path
|
1608
|
-
header["File-Mode"] = str(file_stat.st_mode)
|
1609
|
-
header["File-User"] = str(file_stat.st_uid)
|
1610
|
-
header["File-Group"] = str(file_stat.st_gid)
|
1611
|
-
header["Content-Type"] = "text/plain"
|
1714
|
+
header = {"File-Path": file_path, "Content-Type": "text/plain"}
|
1612
1715
|
header[HTTP_HEADER_KEY_X_SEND_FILE_SOCKET] = "true"
|
1716
|
+
|
1717
|
+
if apply_file_attributes:
|
1718
|
+
file_stat = os.stat(local_file)
|
1719
|
+
header["File-Mode"] = str(file_stat.st_mode)
|
1720
|
+
header["File-User"] = str(file_stat.st_uid)
|
1721
|
+
header["File-Group"] = str(file_stat.st_gid)
|
1722
|
+
|
1613
1723
|
# https://stackoverflow.com/questions/22567306/python-requests-file-upload
|
1614
1724
|
resp = self._request_http(AppMeshClient.Method.POST, path="/appmesh/file/upload", header=header)
|
1615
|
-
|
1616
|
-
|
1725
|
+
|
1726
|
+
resp.raise_for_status()
|
1727
|
+
if HTTP_HEADER_KEY_X_SEND_FILE_SOCKET not in resp.headers:
|
1728
|
+
raise ValueError(f"Server did not respond with socket transfer option: {HTTP_HEADER_KEY_X_SEND_FILE_SOCKET}")
|
1729
|
+
|
1730
|
+
# TLS-optimized chunk size (slightly less than maximum TLS record size)
|
1731
|
+
# leaves some room for TLS overhead (like headers) within the 16 KB limit.
|
1732
|
+
chunk_size = 16 * 1024 - 512 # target to 16KB
|
1733
|
+
while True:
|
1617
1734
|
chunk_data = fp.read(chunk_size)
|
1618
|
-
|
1619
|
-
self.__socket_client.sendall(
|
1620
|
-
|
1621
|
-
|
1622
|
-
self.__socket_client.sendall(
|
1623
|
-
return True, ""
|
1624
|
-
return False, resp.json()[REST_TEXT_MESSAGE_JSON_KEY]
|
1735
|
+
if not chunk_data:
|
1736
|
+
self.__socket_client.sendall((0).to_bytes(TCP_MESSAGE_HEADER_LENGTH, byteorder="big", signed=False))
|
1737
|
+
break
|
1738
|
+
self.__socket_client.sendall(len(chunk_data).to_bytes(TCP_MESSAGE_HEADER_LENGTH, byteorder="big", signed=False))
|
1739
|
+
self.__socket_client.sendall(chunk_data)
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: appmesh
|
3
|
-
Version: 1.3.
|
3
|
+
Version: 1.3.4
|
4
4
|
Summary: Client SDK for App Mesh
|
5
5
|
Home-page: https://github.com/laoshanxi/app-mesh
|
6
6
|
Author: laoshanxi
|
@@ -25,11 +25,9 @@ Requires-Dist: aniso8601
|
|
25
25
|
</a>
|
26
26
|
[](https://api.securityscorecards.dev/projects/github.com/laoshanxi/app-mesh)
|
27
27
|
|
28
|
-
# App Mesh
|
28
|
+
# App Mesh: Advanced Application Management Platform
|
29
29
|
|
30
|
-
App Mesh is
|
31
|
-
|
32
|
-
App Mesh is similar to Kubernetes but is much more lightweight, supporting both containerized and native applications.
|
30
|
+
App Mesh is an open-source, multi-tenant application management platform designed for cloud-native environments. It efficiently manages, schedules, and monitors both microservices and traditional applications, offering a lightweight alternative to Kubernetes. App Mesh bridges the gap between simple process managers and complex container orchestration systems, making it ideal for organizations seeking to modernize their infrastructure without adopting full container-native complexity. Supporting both containerized and native applications, it provides a versatile solution for diverse enterprise needs.
|
33
31
|
|
34
32
|
<div align=center><img src="https://github.com/laoshanxi/picture/raw/master/appmesh/diagram.png" align=center /></div>
|
35
33
|
|
@@ -87,7 +85,7 @@ Refer to the [Installation doc](https://app-mesh.readthedocs.io/en/latest/Instal
|
|
87
85
|
| Non-container app | √ |
|
88
86
|
| Service expose | √ | √ |
|
89
87
|
| Scheduler | √ | √ |
|
90
|
-
| Definition file |
|
88
|
+
| Definition file | YAML | YAML |
|
91
89
|
| GUI | √ | √ |
|
92
90
|
| Virtual Network | | √ |
|
93
91
|
| Monitor tools | √ | √ |
|
@@ -112,6 +110,7 @@ Refer to the [Installation doc](https://app-mesh.readthedocs.io/en/latest/Instal
|
|
112
110
|
- [Kubernetes run none-container applications](https://app-mesh.readthedocs.io/en/latest/success/kubernetes_run_native_application.html)
|
113
111
|
- [Remote execute](https://app-mesh.readthedocs.io/en/latest/success/remote_run_cli_and_python.html)
|
114
112
|
- [Python parallel run](https://app-mesh.readthedocs.io/en/latest/success/python_parallel_run.html)
|
113
|
+
- [Secure consul cluster](https://app-mesh.readthedocs.io/en/latest/success/secure_consul_cluster.html)
|
115
114
|
|
116
115
|
---
|
117
116
|
|
@@ -0,0 +1,6 @@
|
|
1
|
+
appmesh/__init__.py,sha256=xRdXeFHEieRauuJZElbEBASgXG0ZzU1a5_0isAhM7Gw,11
|
2
|
+
appmesh/appmesh_client.py,sha256=6lnfsR6SAhF66jWBPIVPbC43LPhyAL3amBHxSx30Cu4,68327
|
3
|
+
appmesh-1.3.4.dist-info/METADATA,sha256=TZjYqpu2P33ZcN7ue8uTAMobwavbud7_zr9Eo2si7NY,11191
|
4
|
+
appmesh-1.3.4.dist-info/WHEEL,sha256=OVMc5UfuAQiSplgO0_WdW7vXVGAt9Hdd6qtN4HotdyA,91
|
5
|
+
appmesh-1.3.4.dist-info/top_level.txt,sha256=-y0MNQOGJxUzLdHZ6E_Rfv5_LNCkV-GTmOBME_b6pg8,8
|
6
|
+
appmesh-1.3.4.dist-info/RECORD,,
|
appmesh-1.3.2.dist-info/RECORD
DELETED
@@ -1,6 +0,0 @@
|
|
1
|
-
appmesh/__init__.py,sha256=xRdXeFHEieRauuJZElbEBASgXG0ZzU1a5_0isAhM7Gw,11
|
2
|
-
appmesh/appmesh_client.py,sha256=Ev0MMq7nu02pFbmTCLvPdxK3P-NmOeQQXiIYAibSYjQ,62923
|
3
|
-
appmesh-1.3.2.dist-info/METADATA,sha256=pp8AVbBvK6naixdn039eaqy49JFa9dX6Qs7XS7mD7-0,11031
|
4
|
-
appmesh-1.3.2.dist-info/WHEEL,sha256=GV9aMThwP_4oNCtvEC2ec3qUYutgWeAzklro_0m4WJQ,91
|
5
|
-
appmesh-1.3.2.dist-info/top_level.txt,sha256=-y0MNQOGJxUzLdHZ6E_Rfv5_LNCkV-GTmOBME_b6pg8,8
|
6
|
-
appmesh-1.3.2.dist-info/RECORD,,
|
File without changes
|