appmesh 1.3.6__py3-none-any.whl → 1.3.8__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 CHANGED
@@ -1,1774 +1,8 @@
1
- #!/usr/bin/python3
2
- """App Mesh Python SDK"""
3
- import abc
4
- import base64
5
- from contextlib import contextmanager
6
- import copy
7
- import json
8
- import os
9
- import socket
10
- import ssl
11
- import uuid
1
+ # appmesh_client.py
12
2
 
13
- from enum import Enum, unique
14
- from datetime import datetime
15
- from http import HTTPStatus
16
- from typing import Optional, Tuple, Union
17
- from urllib import parse
3
+ # Legacy Compatibility Layer
4
+ # These imports provide backward compatibility for older code that relies on
5
+ # AppMeshClient, App, and AppOutput classes. The updated implementation can be found
6
+ # in http_client.py, where these classes are now primarily maintained.
18
7
 
19
- import aniso8601
20
- import requests
21
-
22
- # pylint: disable=broad-exception-raised,line-too-long,broad-exception-caught,too-many-lines, import-outside-toplevel, protected-access
23
-
24
- DURATION_ONE_WEEK_ISO = "P1W"
25
- DURATION_TWO_DAYS_ISO = "P2D"
26
- DURATION_TWO_DAYS_HALF_ISO = "P2DT12H"
27
-
28
- DEFAULT_SSL_CA_CERT_PATH = "/opt/appmesh/ssl/ca.pem"
29
- DEFAULT_SSL_CLIENT_CERT_PATH = "/opt/appmesh/ssl/client.pem"
30
- DEFAULT_SSL_CLIENT_KEY_PATH = "/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_BLOCK_SIZE = 16 * 1024 - 256 # target to 16KB
35
- TCP_HEADER_LENGTH = 4
36
- JSON_KEY_MESSAGE = "message"
37
- 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
- Represents an application in App Mesh, including configuration, resource limitations, behaviors, and permissions.
50
- """
51
-
52
- @staticmethod
53
- def _get_str_item(data: dict, key: str) -> 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: str) -> 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: str) -> Optional[bool]:
64
- """Retrieve a boolean value from a dictionary by key, if it exists and is boolean-like."""
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: str) -> 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
- """Defines application permission levels."""
75
-
76
- DENY = "1"
77
- READ = "2"
78
- WRITE = "3"
79
-
80
- class Behavior(object):
81
- """
82
- Manages application error handling behavior, including exit and control behaviors.
83
- """
84
-
85
- @unique
86
- class Action(Enum):
87
- """Defines actions for application exit behaviors."""
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, options: 'restart', 'standby', 'keepalive', 'remove'."""
100
-
101
- self.control = App._get_native_item(data, "control") or {}
102
- """Exit code specific behavior (e.g, --control 0:restart --control 1:standby), higher priority than default exit behavior"""
103
-
104
- def set_exit_behavior(self, action: Action) -> None:
105
- """Set default behavior for application exit."""
106
- self.exit = action.value
107
-
108
- def set_control_behavior(self, control_code: int, action: Action) -> None:
109
- """Define behavior for specific exit codes."""
110
- self.control[str(control_code)] = action.value
111
-
112
- class DailyLimitation(object):
113
- """
114
- Defines application availability within a daily time range.
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
- """Start time for application availability (e.g., 09:00:00+08)."""
123
-
124
- self.daily_end = App._get_int_item(data, "daily_end")
125
- """End time for application availability (e.g., 20:00:00+08)."""
126
-
127
- def set_daily_range(self, start: datetime, end: datetime) -> None:
128
- """Set the valid daily start and end times."""
129
- self.daily_start = int(start.timestamp())
130
- self.daily_end = int(end.timestamp())
131
-
132
- class ResourceLimitation(object):
133
- """
134
- Defines application resource limits, such as CPU and memory usage.
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 of CPU usage."""
143
-
144
- self.memory_mb = App._get_int_item(data, "memory_mb")
145
- """Physical memory limit in MB."""
146
-
147
- self.memory_virt_mb = App._get_int_item(data, "memory_virt_mb")
148
- """Virtual memory limit in MB."""
149
-
150
- def __init__(self, data=None) -> None:
151
- """Initialize an App instance with optional configuration data."""
152
- if isinstance(data, (str, bytes, bytearray)):
153
- data = json.loads(data)
154
-
155
- self.name = App._get_str_item(data, "name")
156
- """application name (unique)"""
157
- self.command = App._get_str_item(data, "command")
158
- """full command line with arguments"""
159
- self.shell = App._get_bool_item(data, "shell")
160
- """use shell mode, cmd can be more shell commands with string format"""
161
- self.session_login = App._get_bool_item(data, "session_login")
162
- """app run in session login mode"""
163
- self.description = App._get_str_item(data, "description")
164
- """application description string"""
165
- self.metadata = App._get_native_item(data, "metadata")
166
- """metadata string/JSON (input for application, pass to process stdin)"""
167
- self.working_dir = App._get_str_item(data, "working_dir")
168
- """working directory"""
169
- self.status = App._get_int_item(data, "status")
170
- """initial application status (true is enable, false is disabled)"""
171
- self.docker_image = App._get_str_item(data, "docker_image")
172
- """docker image which used to run command line (for docker container application)"""
173
- self.stdout_cache_num = App._get_int_item(data, "stdout_cache_num")
174
- """stdout file cache number"""
175
- self.start_time = App._get_int_item(data, "start_time")
176
- """start date time for app (ISO8601 time format, e.g., '2020-10-11T09:22:05')"""
177
- self.end_time = App._get_int_item(data, "end_time")
178
- """end date time for app (ISO8601 time format, e.g., '2020-10-11T10:22:05')"""
179
- self.interval = App._get_int_item(data, "interval")
180
- """start interval seconds for short running app, support ISO 8601 durations and cron expression (e.g., 'P1Y2M3DT4H5M6S' 'P5W' '* */5 * * * *')"""
181
- self.cron = App._get_bool_item(data, "cron")
182
- """indicate interval parameter use cron expression or not"""
183
- self.daily_limitation = App.DailyLimitation(App._get_native_item(data, "daily_limitation"))
184
- self.retention = App._get_str_item(data, "retention")
185
- """extra timeout seconds for stopping current process, support ISO 8601 durations (e.g., 'P1Y2M3DT4H5M6S' 'P5W')."""
186
- self.health_check_cmd = App._get_str_item(data, "health_check_cmd")
187
- """health check script command (e.g., sh -x 'curl host:port/health', return 0 is health)"""
188
- self.permission = App._get_int_item(data, "permission")
189
- """application user permission, value is 2 bit integer: [group & other], each bit can be deny:1, read:2, write: 3."""
190
- self.behavior = App.Behavior(App._get_native_item(data, "behavior"))
191
-
192
- self.env = data.get("env", {}) if data else {}
193
- """environment variables (e.g., -e env1=value1 -e env2=value2, APP_DOCKER_OPTS is used to input docker run parameters)"""
194
- self.sec_env = data.get("sec_env", {}) if data else {}
195
- """security environment variables, encrypt in server side with application owner's cipher"""
196
- self.pid = App._get_int_item(data, "pid")
197
- """process id used to attach to the running process"""
198
- self.resource_limit = App.ResourceLimitation(App._get_native_item(data, "resource_limit"))
199
-
200
- # Read-only attributes
201
- self.owner = App._get_str_item(data, "owner")
202
- """owner name"""
203
- self.pstree = App._get_str_item(data, "pstree")
204
- """process tree"""
205
- self.container_id = App._get_str_item(data, "container_id")
206
- """container id"""
207
- self.memory = App._get_int_item(data, "memory")
208
- """memory usage"""
209
- self.cpu = App._get_int_item(data, "cpu")
210
- """cpu usage"""
211
- self.fd = App._get_int_item(data, "fd")
212
- """file descriptor usage"""
213
- self.last_start_time = App._get_int_item(data, "last_start_time")
214
- """last start time"""
215
- self.last_exit_time = App._get_int_item(data, "last_exit_time")
216
- """last exit time"""
217
- self.health = App._get_int_item(data, "health")
218
- """health status"""
219
- self.version = App._get_int_item(data, "version")
220
- """version number"""
221
- self.return_code = App._get_int_item(data, "return_code")
222
- """last exit code"""
223
-
224
- def set_valid_time(self, start: datetime, end: datetime) -> None:
225
- """Define the valid time window for the application."""
226
- self.start_time = int(start.timestamp()) if start else None
227
- self.end_time = int(end.timestamp()) if end else None
228
-
229
- def set_env(self, key: str, value: str, secure: bool = False) -> None:
230
- """Set an environment variable, marking it secure if specified."""
231
- (self.sec_env if secure else self.env)[key] = value
232
-
233
- def set_permission(self, group_user: Permission, others_user: Permission) -> None:
234
- """Define application permissions based on user roles."""
235
- self.permission = int(group_user.value + others_user.value)
236
-
237
- def __str__(self) -> str:
238
- """Return a JSON string representation of the application."""
239
- return json.dumps(self.json())
240
-
241
- def json(self) -> dict:
242
- """Convert the application data into a JSON-compatible dictionary, removing empty items."""
243
- output = copy.deepcopy(self.__dict__)
244
- output["behavior"] = self.behavior.__dict__
245
- output["daily_limitation"] = self.daily_limitation.__dict__
246
- output["resource_limit"] = self.resource_limit.__dict__
247
-
248
- def clean_empty(data: dict) -> None:
249
- keys_to_delete = []
250
- for key, value in data.items():
251
- if isinstance(value, dict) and key != "metadata":
252
- clean_empty(value) # Recursive call (without check user metadata)
253
- if data[key] in [None, "", {}]:
254
- keys_to_delete.append(key) # Mark keys for deletion
255
-
256
- for key in keys_to_delete: # Delete keys after the loop to avoid modifying dict during iteration
257
- del data[key]
258
-
259
- clean_empty(output)
260
- return output
261
-
262
-
263
- class AppOutput(object):
264
- """
265
- Represents the output information returned by the `app_output()` API, including the application's
266
- stdout content, current read position, status code, and exit code.
267
- """
268
-
269
- def __init__(self, status_code: HTTPStatus, output: str, out_position: Optional[int], exit_code: Optional[int]) -> None:
270
- self.status_code = status_code
271
- """HTTP status code from the `app_output()` API request, indicating the result status."""
272
-
273
- self.output = output
274
- """Captured stdout content of the application as returned by the `app_output()` API."""
275
-
276
- self.out_position = out_position
277
- """Current read position in the application's stdout stream, or `None` if not applicable."""
278
-
279
- self.exit_code = exit_code
280
- """Exit code of the application, or `None` if the process is still running or hasn't exited."""
281
-
282
-
283
- class AppRun(object):
284
- """
285
- Represents an application run object initiated by `run_async()` for monitoring and retrieving
286
- the result of a remote application run.
287
- """
288
-
289
- def __init__(self, client, app_name: str, process_id: str):
290
- self.app_name = app_name
291
- """Name of the application associated with this run."""
292
-
293
- self.proc_uid = process_id
294
- """Unique process ID from `run_async()`."""
295
-
296
- self._client = client
297
- """Instance of `AppMeshClient` used to manage this application run."""
298
-
299
- self._forwarding_host = client.forwarding_host
300
- """Target server for the application run, used for forwarding."""
301
-
302
- @contextmanager
303
- def forwarding_host(self):
304
- """Context manager to override the `forwarding_host` for the duration of the run."""
305
- original_value = self._client.forwarding_host
306
- self._client.forwarding_host = self._forwarding_host
307
- try:
308
- yield
309
- finally:
310
- self._client.forwarding_host = original_value
311
-
312
- def wait(self, stdout_print: bool = True, timeout: int = 0) -> int:
313
- """Wait for the asynchronous run to complete.
314
-
315
- Args:
316
- stdout_print (bool, optional): If `True`, prints remote stdout to local. Defaults to `True`.
317
- timeout (int, optional): Maximum time to wait in seconds. If `0`, waits until completion. Defaults to `0`.
318
-
319
- Returns:
320
- int: Exit code if the process finishes successfully. Returns `None` on timeout or exception.
321
- """
322
- with self.forwarding_host():
323
- return self._client.run_async_wait(self, stdout_print, timeout)
324
-
325
-
326
- class AppMeshClient(metaclass=abc.ABCMeta):
327
- """
328
- Client SDK for interacting with the App Mesh service via REST API.
329
-
330
- The `AppMeshClient` class provides a comprehensive interface for managing and monitoring distributed applications
331
- within the App Mesh ecosystem. It enables communication with the App Mesh REST API for operations such as
332
- application lifecycle management, monitoring, and configuration.
333
-
334
- This client is designed for direct usage in applications that require access to App Mesh services over HTTP-based REST.
335
-
336
- Usage:
337
- - Install the App Mesh Python package:
338
- python3 -m pip install --upgrade appmesh
339
- - Import the client module:
340
- from appmesh import appmesh_client
341
-
342
- Example:
343
- client = appmesh_client.AppMeshClient()
344
- client.login("your-name", "your-password")
345
- response = client.app_view(app_name='ping')
346
-
347
- Attributes:
348
- - TLS (Transport Layer Security): Supports secure connections between the client and App Mesh service,
349
- ensuring encrypted communication.
350
- - JWT (JSON Web Token) and RBAC (Role-Based Access Control): Provides secure API access with
351
- token-based authentication and authorization to enforce fine-grained permissions.
352
-
353
- Methods:
354
- - login()
355
- - logoff()
356
- - authentication()
357
- - renew()
358
- - totp_disable()
359
- - totp_secret()
360
- - totp_setup()
361
-
362
- - app_add()
363
- - app_delete()
364
- - app_disable()
365
- - app_enable()
366
- - app_health()
367
- - app_output()
368
- - app_view()
369
- - app_view_all()
370
-
371
- - run_async()
372
- - run_async_wait()
373
- - run_sync()
374
-
375
- - config_set()
376
- - config_view()
377
- - log_level_set()
378
- - host_resource()
379
- - forwarding_host
380
- - metrics()
381
-
382
- - tag_add()
383
- - tag_delete()
384
- - tag_view()
385
-
386
- - file_download()
387
- - file_upload()
388
-
389
- - user_add()
390
- - user_delete()
391
- - user_lock()
392
- - user_passwd_update()
393
- - user_self()
394
- - user_unlock()
395
- - users_view()
396
- - permissions_for_user()
397
- - permissions_view()
398
- - role_delete()
399
- - role_update()
400
- - roles_view()
401
- - groups_view()
402
- """
403
-
404
- @unique
405
- class Method(Enum):
406
- """REST methods"""
407
-
408
- GET = "GET"
409
- PUT = "PUT"
410
- POST = "POST"
411
- DELETE = "DELETE"
412
- POST_STREAM = "POST_STREAM"
413
-
414
- def __init__(
415
- self,
416
- rest_url: str = "https://127.0.0.1:6060",
417
- rest_ssl_verify=DEFAULT_SSL_CA_CERT_PATH if os.path.exists(DEFAULT_SSL_CA_CERT_PATH) else False,
418
- 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,
419
- rest_timeout=(60, 300),
420
- jwt_token=None,
421
- ):
422
- """Initialize an App Mesh HTTP client for interacting with the App Mesh server via secure HTTPS.
423
-
424
- Args:
425
- rest_url (str, optional): The server's base URI, including protocol, hostname, and port. Defaults to `"https://127.0.0.1:6060"`.
426
-
427
- rest_ssl_verify (Union[bool, str], optional): Configures SSL certificate verification for HTTPS requests:
428
- - `True`: Uses system CA certificates to verify the server's identity.
429
- - `False`: Disables SSL verification (insecure, use cautiously for development).
430
- - `str`: Path to a custom CA certificate or directory for verification. This option allows custom CA configuration,
431
- which may be necessary in environments requiring specific CA chains that differ from the default system CAs.
432
-
433
- rest_ssl_client_cert (Union[tuple, str], optional): Specifies a client certificate for mutual TLS authentication:
434
- - If a `str`, provides the path to a PEM file with both client certificate and private key.
435
- - If a `tuple`, contains two paths as (`cert`, `key`), where `cert` is the certificate file and `key` is the private key file.
436
-
437
- rest_timeout (tuple, optional): HTTP connection timeouts for API requests, as `(connect_timeout, read_timeout)`.
438
- The default is `(60, 300)`, where `60` seconds is the maximum time to establish a connection and `300` seconds for the maximum read duration.
439
-
440
- jwt_token (str, optional): JWT token for API authentication, used in headers to authorize requests where required.
441
-
442
- """
443
-
444
- self.server_url = rest_url
445
- self._jwt_token = jwt_token
446
- self.ssl_verify = rest_ssl_verify
447
- self.ssl_client_cert = rest_ssl_client_cert
448
- self.rest_timeout = rest_timeout
449
- self._forwarding_host = None
450
-
451
- @property
452
- def jwt_token(self) -> str:
453
- """property for jwt_token
454
-
455
- Returns:
456
- str: _description_
457
- """
458
- return self._jwt_token
459
-
460
- @jwt_token.setter
461
- def jwt_token(self, token: str) -> None:
462
- """setter for jwt_token
463
-
464
- Args:
465
- token (str): _description_
466
- """
467
- self._jwt_token = token
468
-
469
- @property
470
- def forwarding_host(self) -> str:
471
- """property for forwarding_host
472
-
473
- Returns:
474
- str: forward request to target host (host:port)
475
- """
476
- return self._forwarding_host
477
-
478
- @forwarding_host.setter
479
- def forwarding_host(self, host: str) -> None:
480
- """setter for forwarding_host
481
-
482
- Args:
483
- host (str): forward request to target host (host:port)
484
- """
485
- self._forwarding_host = host
486
-
487
- ########################################
488
- # Security
489
- ########################################
490
- def login(self, user_name: str, user_pwd: str, totp_code="", timeout_seconds=DURATION_ONE_WEEK_ISO) -> str:
491
- """Login with user name and password
492
-
493
- Args:
494
- user_name (str): the name of the user.
495
- user_pwd (str): the password of the user.
496
- totp_code (str, optional): the TOTP code if enabled for the user.
497
- timeout_seconds (int | str, optional): token expire timeout of seconds. support ISO 8601 durations (e.g., 'P1Y2M3DT4H5M6S' 'P1W').
498
-
499
- Returns:
500
- str: JWT token.
501
- """
502
- self.jwt_token = None
503
- resp = self._request_http(
504
- AppMeshClient.Method.POST,
505
- path="/appmesh/login",
506
- header={
507
- "Authorization": "Basic " + base64.b64encode((user_name + ":" + user_pwd).encode()).decode(),
508
- "Expire-Seconds": self._parse_duration(timeout_seconds),
509
- },
510
- )
511
- if resp.status_code == HTTPStatus.OK:
512
- if "Access-Token" in resp.json():
513
- self.jwt_token = resp.json()["Access-Token"]
514
- elif resp.status_code == HTTPStatus.UNAUTHORIZED and "Totp-Challenge" in resp.json():
515
- challenge = resp.json()["Totp-Challenge"]
516
- resp = self._request_http(
517
- AppMeshClient.Method.POST,
518
- path="/appmesh/totp/validate",
519
- header={
520
- "Username": base64.b64encode(user_name.encode()).decode(),
521
- "Totp-Challenge": base64.b64encode(challenge.encode()).decode(),
522
- "Totp": totp_code,
523
- "Expire-Seconds": self._parse_duration(timeout_seconds),
524
- },
525
- )
526
- if resp.status_code == HTTPStatus.OK:
527
- if "Access-Token" in resp.json():
528
- self.jwt_token = resp.json()["Access-Token"]
529
- else:
530
- raise Exception(resp.text)
531
- else:
532
- raise Exception(resp.text)
533
- return self.jwt_token
534
-
535
- def logoff(self) -> bool:
536
- """Logoff current session from server
537
-
538
- Returns:
539
- bool: logoff success or failure.
540
- """
541
- resp = self._request_http(AppMeshClient.Method.POST, path="/appmesh/self/logoff")
542
- if resp.status_code != HTTPStatus.OK:
543
- raise Exception(resp.text)
544
- self.jwt_token = None
545
- return resp.status_code == HTTPStatus.OK
546
-
547
- def authentication(self, token: str, permission=None) -> bool:
548
- """Login with token and verify permission when specified,
549
- verified token will be stored in client object when success
550
-
551
- Args:
552
- token (str): JWT token returned from login().
553
- permission (str, optional): the permission ID used to verify the token user
554
- permission ID can be:
555
- - pre-defined by App Mesh from security.yaml (e.g 'app-view', 'app-delete')
556
- - defined by input from role_update() or security.yaml
557
-
558
- Returns:
559
- bool: authentication success or failure.
560
- """
561
- old_token = self.jwt_token
562
- self.jwt_token = token
563
- headers = {}
564
- if permission:
565
- headers["Auth-Permission"] = permission
566
- resp = self._request_http(AppMeshClient.Method.POST, path="/appmesh/auth", header=headers)
567
- if resp.status_code != HTTPStatus.OK:
568
- self.jwt_token = old_token
569
- raise Exception(resp.text)
570
- return resp.status_code == HTTPStatus.OK
571
-
572
- def renew(self, timeout_seconds=DURATION_ONE_WEEK_ISO) -> str:
573
- """Renew current token
574
-
575
- Args:
576
- timeout_seconds (int | str, optional): token expire timeout of seconds. support ISO 8601 durations (e.g., 'P1Y2M3DT4H5M6S' 'P1W').
577
-
578
- Returns:
579
- str: The new JWT token if renew success, otherwise return None.
580
- """
581
- assert self.jwt_token
582
- resp = self._request_http(
583
- AppMeshClient.Method.POST,
584
- path="/appmesh/token/renew",
585
- header={
586
- "Expire-Seconds": self._parse_duration(timeout_seconds),
587
- },
588
- )
589
- if resp.status_code == HTTPStatus.OK:
590
- if "Access-Token" in resp.json():
591
- self.jwt_token = resp.json()["Access-Token"]
592
- return self.jwt_token
593
- raise Exception(resp.text)
594
-
595
- def totp_secret(self) -> str:
596
- """Generate TOTP secret for current login user and return MFA URI with JSON body
597
-
598
- Returns:
599
- str: TOTP secret str
600
- """
601
- resp = self._request_http(method=AppMeshClient.Method.POST, path="/appmesh/totp/secret")
602
- if resp.status_code == HTTPStatus.OK:
603
- totp_uri = base64.b64decode(resp.json()["Mfa-Uri"]).decode()
604
- return self._parse_totp_uri(totp_uri).get("secret")
605
- raise Exception(resp.text)
606
-
607
- def totp_setup(self, totp_code: str) -> bool:
608
- """Setup 2FA for current login user
609
-
610
- Args:
611
- totp_code (str): TOTP code
612
-
613
- Returns:
614
- bool: success or failure.
615
- """
616
- resp = self._request_http(
617
- method=AppMeshClient.Method.POST,
618
- path="/appmesh/totp/setup",
619
- header={"Totp": totp_code},
620
- )
621
- if resp.status_code != HTTPStatus.OK:
622
- raise Exception(resp.text)
623
- return resp.status_code == HTTPStatus.OK
624
-
625
- def totp_disable(self, user="self") -> bool:
626
- """Disable 2FA for current user
627
-
628
- Args:
629
- user (str, optional): user name for disable TOTP.
630
-
631
- Returns:
632
- bool: success or failure.
633
- """
634
- resp = self._request_http(
635
- method=AppMeshClient.Method.POST,
636
- path=f"/appmesh/totp/{user}/disable",
637
- )
638
- if resp.status_code != HTTPStatus.OK:
639
- raise Exception(resp.text)
640
- return resp.status_code == HTTPStatus.OK
641
-
642
- @staticmethod
643
- def _parse_totp_uri(totp_uri: str) -> dict:
644
- """Extract TOTP parameters
645
-
646
- Args:
647
- totp_uri (str): TOTP uri
648
-
649
- Returns:
650
- dict: eextract parameters
651
- """
652
- parsed_info = {}
653
- parsed_uri = parse.urlparse(totp_uri)
654
-
655
- # Extract label from the path
656
- parsed_info["label"] = parsed_uri.path[1:] # Remove the leading slash
657
-
658
- # Extract parameters from the query string
659
- query_params = parse.parse_qs(parsed_uri.query)
660
- for key, value in query_params.items():
661
- parsed_info[key] = value[0]
662
- return parsed_info
663
-
664
- ########################################
665
- # Application view
666
- ########################################
667
- def app_view(self, app_name: str) -> App:
668
- """Get one application information
669
-
670
- Args:
671
- app_name (str): the application name.
672
-
673
- Returns:
674
- App: the application object both contain static configuration and runtime information.
675
-
676
- Exception:
677
- failed request or no such application
678
- """
679
- resp = self._request_http(AppMeshClient.Method.GET, path=f"/appmesh/app/{app_name}")
680
- if resp.status_code != HTTPStatus.OK:
681
- raise Exception(resp.text)
682
- return App(resp.json())
683
-
684
- def app_view_all(self):
685
- """Get all applications
686
-
687
- Returns:
688
- list: the application object both contain static configuration and runtime information, only return applications that the user has permissions.
689
-
690
- Exception:
691
- failed request or no such application
692
- """
693
- resp = self._request_http(AppMeshClient.Method.GET, path="/appmesh/applications")
694
- if resp.status_code != HTTPStatus.OK:
695
- raise Exception(resp.text)
696
- apps = []
697
- for app in resp.json():
698
- apps.append(App(app))
699
- return apps
700
-
701
- def app_output(self, app_name: str, stdout_position: int = 0, stdout_index: int = 0, stdout_maxsize: int = 10240, process_uuid: str = "", timeout: int = 0) -> AppOutput:
702
- """Get application stdout/stderr
703
-
704
- Args:
705
- app_name (str): the application name
706
- stdout_position (int, optional): start read position, 0 means start from beginning.
707
- stdout_index (int, optional): index of history process stdout, 0 means get from current running process,
708
- the stdout number depends on 'stdout_cache_size' of the application.
709
- stdout_maxsize (int, optional): max buffer size to read.
710
- process_uuid (str, optional): used to get the specified process.
711
- timeout (int, optional): wait for the running process for some time(seconds) to get the output.
712
-
713
- Returns:
714
- AppOutput object.
715
- """
716
- resp = self._request_http(
717
- AppMeshClient.Method.GET,
718
- path=f"/appmesh/app/{app_name}/output",
719
- query={
720
- "stdout_position": str(stdout_position),
721
- "stdout_index": str(stdout_index),
722
- "stdout_maxsize": str(stdout_maxsize),
723
- "process_uuid": process_uuid,
724
- "timeout": str(timeout),
725
- },
726
- )
727
- out_position = int(resp.headers["Output-Position"]) if "Output-Position" in resp.headers else None
728
- exit_code = int(resp.headers["Exit-Code"]) if "Exit-Code" in resp.headers else None
729
- return AppOutput(status_code=resp.status_code, output=resp.text, out_position=out_position, exit_code=exit_code)
730
-
731
- def app_health(self, app_name: str) -> bool:
732
- """Get application health status
733
-
734
- Args:
735
- app_name (str): the application name.
736
-
737
- Returns:
738
- bool: healthy or not
739
- """
740
- resp = self._request_http(AppMeshClient.Method.GET, path=f"/appmesh/app/{app_name}/health")
741
- if resp.status_code != HTTPStatus.OK:
742
- raise Exception(resp.text)
743
- return int(resp.text) == 0
744
-
745
- ########################################
746
- # Application manage
747
- ########################################
748
- def app_add(self, app: App) -> App:
749
- """Register an application
750
-
751
- Args:
752
- app (App): the application definition.
753
-
754
- Returns:
755
- App: resigtered application object.
756
-
757
- Exception:
758
- failed request
759
- """
760
- resp = self._request_http(AppMeshClient.Method.PUT, path=f"/appmesh/app/{app.name}", body=app.json())
761
- if resp.status_code != HTTPStatus.OK:
762
- raise Exception(resp.text)
763
- return App(resp.json())
764
-
765
- def app_delete(self, app_name: str) -> bool:
766
- """Remove an application.
767
-
768
- Args:
769
- app_name (str): the application name.
770
-
771
- Returns:
772
- bool: True for delete success, Flase for not exist anymore.
773
- """
774
- resp = self._request_http(AppMeshClient.Method.DELETE, path=f"/appmesh/app/{app_name}")
775
- if resp.status_code == HTTPStatus.OK:
776
- return True
777
- elif resp.status_code == HTTPStatus.NOT_FOUND:
778
- return False
779
- else:
780
- raise Exception(resp.text)
781
-
782
- def app_enable(self, app_name: str) -> bool:
783
- """Enable an application
784
-
785
- Args:
786
- app_name (str): the application name.
787
-
788
- Returns:
789
- bool: success or failure.
790
- """
791
- resp = self._request_http(AppMeshClient.Method.POST, path=f"/appmesh/app/{app_name}/enable")
792
- if resp.status_code != HTTPStatus.OK:
793
- raise Exception(resp.text)
794
- return resp.status_code == HTTPStatus.OK
795
-
796
- def app_disable(self, app_name: str) -> bool:
797
- """Stop and disable an application
798
-
799
- Args:
800
- app_name (str): the application name.
801
-
802
- Returns:
803
- bool: success or failure.
804
- """
805
- resp = self._request_http(AppMeshClient.Method.POST, path=f"/appmesh/app/{app_name}/disable")
806
- if resp.status_code != HTTPStatus.OK:
807
- raise Exception(resp.text)
808
- return resp.status_code == HTTPStatus.OK
809
-
810
- ########################################
811
- # Cloud management
812
- ########################################
813
- def cloud_app_view_all(self) -> dict:
814
- """Get all cloud applications
815
-
816
- Returns:
817
- dict: cloud applications in JSON format.
818
- """
819
- resp = self._request_http(AppMeshClient.Method.GET, path="/appmesh/cloud/applications")
820
- if resp.status_code != HTTPStatus.OK:
821
- raise Exception(resp.text)
822
- return resp.json()
823
-
824
- def cloud_app(self, app_name: str) -> dict:
825
- """Get an cloud application
826
-
827
- Args:
828
- app_name (str): the application name.
829
-
830
- Returns:
831
- dict: application in JSON format.
832
- """
833
- resp = self._request_http(AppMeshClient.Method.GET, path=f"/appmesh/cloud/app/{app_name}")
834
- if resp.status_code != HTTPStatus.OK:
835
- raise Exception(resp.text)
836
- return resp.json()
837
-
838
- def cloud_app_output(self, app_name: str, host_name: str, stdout_position: int = 0, stdout_index: int = 0, stdout_maxsize: int = 10240, process_uuid: str = ""):
839
- """Get cloud application stdout/stderr from master agent
840
-
841
- Args:
842
- app_name (str): the application name
843
- host_name (str): the target host name where the application is running
844
- stdout_position (int, optional): start read position, 0 means start from beginning.
845
- stdout_index (int, optional): index of history process stdout, 0 means get from current running process,
846
- the stdout number depends on 'stdout_cache_size' of the application.
847
- stdout_maxsize (int, optional): max buffer size to read.
848
- process_uuid (str, optional): used to get the specified process.
849
-
850
- Returns:
851
- bool: success or failure.
852
- str: output string.
853
- int or None: current read position.
854
- int or None: process exit code.
855
- """
856
- resp = self._request_http(
857
- AppMeshClient.Method.GET,
858
- path=f"/appmesh/cloud/app/{app_name}/output/{host_name}",
859
- query={
860
- "stdout_position": str(stdout_position),
861
- "stdout_index": str(stdout_index),
862
- "stdout_maxsize": str(stdout_maxsize),
863
- "process_uuid": process_uuid,
864
- },
865
- )
866
- out_position = int(resp.headers["Output-Position"]) if "Output-Position" in resp.headers else None
867
- exit_code = int(resp.headers["Exit-Code"]) if "Exit-Code" in resp.headers else None
868
- return (resp.status_code == HTTPStatus.OK), resp.text, out_position, exit_code
869
-
870
- def cloud_app_delete(self, app_name: str) -> bool:
871
- """Delete a cloud application
872
-
873
- Args:
874
- app_name (str): The application name for cloud
875
-
876
- Returns:
877
- bool: success or failure.
878
- """
879
- resp = self._request_http(AppMeshClient.Method.DELETE, path=f"/appmesh/cloud/app/{app_name}")
880
- if resp.status_code != HTTPStatus.OK:
881
- raise Exception(resp.text)
882
- return resp.status_code == HTTPStatus.OK
883
-
884
- def cloud_app_add(self, app_json: dict) -> dict:
885
- """Add a cloud application
886
-
887
- Args:
888
- app_json (dict): the cloud application definition with replication, condition and resource requirement
889
-
890
- Returns:
891
- dict: cluster application json.
892
- """
893
- resp = self._request_http(AppMeshClient.Method.PUT, path=f"/appmesh/cloud/app/{app_json['content']['name']}", body=app_json)
894
- if resp.status_code != HTTPStatus.OK:
895
- raise Exception(resp.text)
896
- return resp.json()
897
-
898
- def cloud_nodes(self) -> dict:
899
- """Get cluster node list
900
-
901
- Returns:
902
- dict: cluster node list json.
903
- """
904
- resp = self._request_http(AppMeshClient.Method.GET, path="/appmesh/cloud/nodes")
905
- if resp.status_code != HTTPStatus.OK:
906
- raise Exception(resp.text)
907
- return resp.json()
908
-
909
- ########################################
910
- # Configuration
911
- ########################################
912
- def host_resource(self) -> dict:
913
- """Get App Mesh host resource report include CPU, memory and disk
914
-
915
- Returns:
916
- dict: the host resource json.
917
- """
918
- resp = self._request_http(AppMeshClient.Method.GET, path="/appmesh/resources")
919
- if resp.status_code != HTTPStatus.OK:
920
- raise Exception(resp.text)
921
- return resp.json()
922
-
923
- def config_view(self) -> dict:
924
- """Get App Mesh configuration JSON
925
-
926
- Returns:
927
- dict: the configuration json.
928
- """
929
- resp = self._request_http(AppMeshClient.Method.GET, path="/appmesh/config")
930
- if resp.status_code != HTTPStatus.OK:
931
- raise Exception(resp.text)
932
- return resp.json()
933
-
934
- def config_set(self, cfg_json) -> dict:
935
- """Update configuration, the format follow 'config.yaml', support partial update
936
-
937
- Args:
938
- cfg_json (dict): the new configuration json.
939
-
940
- Returns:
941
- dict: the updated configuration json.
942
- """
943
- resp = self._request_http(AppMeshClient.Method.POST, path="/appmesh/config", body=cfg_json)
944
- if resp.status_code != HTTPStatus.OK:
945
- raise Exception(resp.text)
946
- return resp.json()
947
-
948
- def log_level_set(self, level: str = "DEBUG") -> str:
949
- """Update App Mesh log level(DEBUG/INFO/NOTICE/WARN/ERROR), a wrapper of config_set()
950
-
951
- Args:
952
- level (str, optional): log level.
953
-
954
- Returns:
955
- str: the updated log level.
956
- """
957
- resp = self._request_http(AppMeshClient.Method.POST, path="/appmesh/config", body={"BaseConfig": {"LogLevel": level}})
958
- if resp.status_code != HTTPStatus.OK:
959
- raise Exception(resp.text)
960
- return resp.json()["BaseConfig"]["LogLevel"]
961
-
962
- ########################################
963
- # User Management
964
- ########################################
965
- def user_passwd_update(self, new_password: str, user_name: str = "self") -> bool:
966
- """Change user password
967
-
968
- Args:
969
- user_name (str): the user name.
970
- new_password (str):the new password string
971
-
972
- Returns:
973
- bool: success
974
- """
975
- resp = self._request_http(
976
- method=AppMeshClient.Method.POST,
977
- path=f"/appmesh/user/{user_name}/passwd",
978
- header={"New-Password": base64.b64encode(new_password.encode())},
979
- )
980
- if resp.status_code != HTTPStatus.OK:
981
- raise Exception(resp.text)
982
- return True
983
-
984
- def user_add(self, user_name: str, user_json: dict) -> bool:
985
- """Add a new user, not available for LDAP user
986
-
987
- Args:
988
- user_name (str): the user name.
989
- user_json (dict): user definition, follow same user format from security.yaml.
990
-
991
- Returns:
992
- bool: success or failure.
993
- """
994
- resp = self._request_http(
995
- method=AppMeshClient.Method.PUT,
996
- path=f"/appmesh/user/{user_name}",
997
- body=user_json,
998
- )
999
- return resp.status_code == HTTPStatus.OK
1000
-
1001
- def user_delete(self, user_name: str) -> bool:
1002
- """Delete a user
1003
-
1004
- Args:
1005
- user_name (str): the user name.
1006
-
1007
- Returns:
1008
- bool: success or failure.
1009
- """
1010
- resp = self._request_http(
1011
- method=AppMeshClient.Method.DELETE,
1012
- path=f"/appmesh/user/{user_name}",
1013
- )
1014
- return resp.status_code == HTTPStatus.OK
1015
-
1016
- def user_lock(self, user_name: str) -> bool:
1017
- """Lock a user
1018
-
1019
- Args:
1020
- user_name (str): the user name.
1021
-
1022
- Returns:
1023
- bool: success or failure.
1024
- """
1025
- resp = self._request_http(
1026
- method=AppMeshClient.Method.POST,
1027
- path=f"/appmesh/user/{user_name}/lock",
1028
- )
1029
- if resp.status_code != HTTPStatus.OK:
1030
- raise Exception(resp.text)
1031
- return resp.status_code == HTTPStatus.OK
1032
-
1033
- def user_unlock(self, user_name: str) -> bool:
1034
- """Unlock a user
1035
-
1036
- Args:
1037
- user_name (str): the user name.
1038
-
1039
- Returns:
1040
- bool: success or failure.
1041
- """
1042
- resp = self._request_http(
1043
- method=AppMeshClient.Method.POST,
1044
- path=f"/appmesh/user/{user_name}/unlock",
1045
- )
1046
- if resp.status_code != HTTPStatus.OK:
1047
- raise Exception(resp.text)
1048
- return resp.status_code == HTTPStatus.OK
1049
-
1050
- def users_view(self) -> dict:
1051
- """Get all users
1052
-
1053
- Returns:
1054
- dict: all user definition
1055
- """
1056
- resp = self._request_http(method=AppMeshClient.Method.GET, path="/appmesh/users")
1057
- if resp.status_code != HTTPStatus.OK:
1058
- raise Exception(resp.text)
1059
- return resp.json()
1060
-
1061
- def user_self(self) -> dict:
1062
- """Get current user infomation
1063
-
1064
- Returns:
1065
- dict: user definition.
1066
- """
1067
- resp = self._request_http(method=AppMeshClient.Method.GET, path="/appmesh/user/self")
1068
- if resp.status_code != HTTPStatus.OK:
1069
- raise Exception(resp.text)
1070
- return resp.json()
1071
-
1072
- def groups_view(self) -> list:
1073
- """Get all user groups
1074
-
1075
- Returns:
1076
- dict: user group array.
1077
- """
1078
- resp = self._request_http(method=AppMeshClient.Method.GET, path="/appmesh/user/groups")
1079
- if resp.status_code != HTTPStatus.OK:
1080
- raise Exception(resp.text)
1081
- return resp.json()
1082
-
1083
- def permissions_view(self) -> list:
1084
- """Get all available permissions
1085
-
1086
- Returns:
1087
- dict: permission array
1088
- """
1089
- resp = self._request_http(method=AppMeshClient.Method.GET, path="/appmesh/permissions")
1090
- if resp.status_code != HTTPStatus.OK:
1091
- raise Exception(resp.text)
1092
- return resp.json()
1093
-
1094
- def permissions_for_user(self) -> list:
1095
- """Get current user permissions
1096
-
1097
- Returns:
1098
- dict: user permission array.
1099
- """
1100
- resp = self._request_http(method=AppMeshClient.Method.GET, path="/appmesh/user/permissions")
1101
- if resp.status_code != HTTPStatus.OK:
1102
- raise Exception(resp.text)
1103
- return resp.json()
1104
-
1105
- def roles_view(self) -> list:
1106
- """Get all roles with permission definition
1107
-
1108
- Returns:
1109
- dict: all role definition.
1110
- """
1111
- resp = self._request_http(method=AppMeshClient.Method.GET, path="/appmesh/roles")
1112
- if resp.status_code != HTTPStatus.OK:
1113
- raise Exception(resp.text)
1114
- return resp.json()
1115
-
1116
- def role_update(self, role_name: str, role_permission_json: dict) -> bool:
1117
- """Update (or add) a role with defined permissions, the permission ID can be App Mesh pre-defined or other permission ID.
1118
-
1119
- Args:
1120
- role_name (str): the role name.
1121
- role_permission_json (dict): role permission definition array, e.g: ["app-control", "app-delete", "cloud-app-reg", "cloud-app-delete"]
1122
-
1123
- Returns:
1124
- bool: success or failure.
1125
- """
1126
- resp = self._request_http(method=AppMeshClient.Method.POST, path=f"/appmesh/role/{role_name}", body=role_permission_json)
1127
- if resp.status_code != HTTPStatus.OK:
1128
- raise Exception(resp.text)
1129
- return resp.status_code == HTTPStatus.OK
1130
-
1131
- def role_delete(self, role_name: str) -> bool:
1132
- """Delete a user role
1133
-
1134
- Args:
1135
- role_name (str): the role name.
1136
-
1137
- Returns:
1138
- bool: success or failure.
1139
- """
1140
- resp = self._request_http(
1141
- method=AppMeshClient.Method.DELETE,
1142
- path=f"/appmesh/role/{role_name}",
1143
- )
1144
- if resp.status_code != HTTPStatus.OK:
1145
- raise Exception(resp.text)
1146
- return resp.status_code == HTTPStatus.OK
1147
-
1148
- ########################################
1149
- # Tag management
1150
- ########################################
1151
- def tag_add(self, tag_name: str, tag_value: str) -> bool:
1152
- """Add a new label
1153
-
1154
- Args:
1155
- tag_name (str): the label name.
1156
- tag_value (str): the label value.
1157
-
1158
- Returns:
1159
- bool: success or failure.
1160
- """
1161
- resp = self._request_http(
1162
- AppMeshClient.Method.PUT,
1163
- query={"value": tag_value},
1164
- path=f"/appmesh/label/{tag_name}",
1165
- )
1166
- if resp.status_code != HTTPStatus.OK:
1167
- raise Exception(resp.text)
1168
- return resp.status_code == HTTPStatus.OK
1169
-
1170
- def tag_delete(self, tag_name: str) -> bool:
1171
- """Delete a label
1172
-
1173
- Args:
1174
- tag_name (str): the label name.
1175
-
1176
- Returns:
1177
- bool: success or failure.
1178
- """
1179
- resp = self._request_http(AppMeshClient.Method.DELETE, path=f"/appmesh/label/{tag_name}")
1180
- if resp.status_code != HTTPStatus.OK:
1181
- raise Exception(resp.text)
1182
- return resp.status_code == HTTPStatus.OK
1183
-
1184
- def tag_view(self) -> dict:
1185
- """Get the server labels
1186
-
1187
- Returns:
1188
- dict: label data.
1189
- """
1190
- resp = self._request_http(AppMeshClient.Method.GET, path="/appmesh/labels")
1191
- if resp.status_code != HTTPStatus.OK:
1192
- raise Exception(resp.text)
1193
- return resp.json()
1194
-
1195
- ########################################
1196
- # Promethus metrics
1197
- ########################################
1198
- def metrics(self):
1199
- """Prometheus metrics (this does not call Prometheus API /metrics, just copy the same metrics data)
1200
-
1201
- Returns:
1202
- str: prometheus metrics texts
1203
- """
1204
- resp = self._request_http(AppMeshClient.Method.GET, path="/appmesh/metrics")
1205
- if resp.status_code != HTTPStatus.OK:
1206
- raise Exception(resp.text)
1207
- return resp.text
1208
-
1209
- ########################################
1210
- # File management
1211
- ########################################
1212
- def file_download(self, remote_file: str, local_file: str, apply_file_attributes: bool = True) -> None:
1213
- """Copy a remote file to local. Optionally, the local file will have the same permission as the remote file.
1214
-
1215
- Args:
1216
- remote_file (str): the remote file path.
1217
- local_file (str): the local file path to be downloaded.
1218
- apply_file_attributes (bool): whether to apply file attributes (permissions, owner, group) to the local file.
1219
- """
1220
- resp = self._request_http(AppMeshClient.Method.GET, path="/appmesh/file/download", header={"File-Path": remote_file})
1221
- resp.raise_for_status()
1222
-
1223
- # Write the file content locally
1224
- with open(local_file, "wb") as fp:
1225
- for chunk in resp.iter_content(chunk_size=8 * 1024): # 8 KB
1226
- if chunk:
1227
- fp.write(chunk)
1228
-
1229
- # Apply file attributes (permissions, owner, group) if requested
1230
- if apply_file_attributes:
1231
- if "File-Mode" in resp.headers:
1232
- os.chmod(path=local_file, mode=int(resp.headers["File-Mode"]))
1233
- if "File-User" in resp.headers and "File-Group" in resp.headers:
1234
- file_uid = int(resp.headers["File-User"])
1235
- file_gid = int(resp.headers["File-Group"])
1236
- try:
1237
- os.chown(path=local_file, uid=file_uid, gid=file_gid)
1238
- except PermissionError:
1239
- print(f"Warning: Unable to change owner/group of {local_file}. Operation requires elevated privileges.")
1240
-
1241
- def file_upload(self, local_file: str, remote_file: str, apply_file_attributes: bool = True) -> None:
1242
- """Upload a local file to the remote server. Optionally, the remote file will have the same permission as the local file.
1243
-
1244
- Dependency:
1245
- sudo apt install python3-pip
1246
- pip3 install requests_toolbelt
1247
-
1248
- Args:
1249
- local_file (str): the local file path.
1250
- remote_file (str): the target remote file to be uploaded.
1251
- apply_file_attributes (bool): whether to upload file attributes (permissions, owner, group) along with the file.
1252
- """
1253
- if not os.path.exists(local_file):
1254
- raise FileNotFoundError(f"Local file not found: {local_file}")
1255
-
1256
- from requests_toolbelt import MultipartEncoder
1257
-
1258
- with open(file=local_file, mode="rb") as fp:
1259
- encoder = MultipartEncoder(fields={"filename": os.path.basename(remote_file), "file": ("filename", fp, "application/octet-stream")})
1260
- header = {"File-Path": remote_file, "Content-Type": encoder.content_type}
1261
-
1262
- # Include file attributes (permissions, owner, group) if requested
1263
- if apply_file_attributes:
1264
- file_stat = os.stat(local_file)
1265
- header["File-Mode"] = str(file_stat.st_mode & 0o777) # Mask to keep only permission bits
1266
- header["File-User"] = str(file_stat.st_uid)
1267
- header["File-Group"] = str(file_stat.st_gid)
1268
-
1269
- # Upload file with or without attributes
1270
- # https://stackoverflow.com/questions/22567306/python-requests-file-upload
1271
- resp = self._request_http(
1272
- AppMeshClient.Method.POST_STREAM,
1273
- path="/appmesh/file/upload",
1274
- header=header,
1275
- body=encoder,
1276
- )
1277
- resp.raise_for_status()
1278
-
1279
- ########################################
1280
- # Application run
1281
- ########################################
1282
- def _parse_duration(self, timeout) -> str:
1283
- if isinstance(timeout, int):
1284
- return str(timeout)
1285
- elif isinstance(timeout, str):
1286
- return str(int(aniso8601.parse_duration(timeout).total_seconds()))
1287
- else:
1288
- raise TypeError(f"Invalid timeout type: {str(timeout)}")
1289
-
1290
- def run_async(
1291
- self,
1292
- app: Union[App, str],
1293
- max_time_seconds: Union[int, str] = DURATION_TWO_DAYS_ISO,
1294
- life_cycle_seconds: Union[int, str] = DURATION_TWO_DAYS_HALF_ISO,
1295
- ) -> AppRun:
1296
- """Run an application asynchronously on a remote system without blocking the API.
1297
-
1298
- Args:
1299
- app (Union[App, str]): An `App` instance or a shell command string.
1300
- - If `app` is a string, it is treated as a shell command for the remote run,
1301
- and an `App` instance is created as:
1302
- `App({"command": "<command_string>", "shell": True})`.
1303
- - If `app` is an `App` object, providing only the `name` attribute (without
1304
- a command) will run an existing application; otherwise, it is treated as a new application.
1305
- max_time_seconds (Union[int, str], optional): Maximum runtime for the remote process.
1306
- Accepts ISO 8601 duration format (e.g., 'P1Y2M3DT4H5M6S', 'P5W'). Defaults to `P2D`.
1307
- life_cycle_seconds (Union[int, str], optional): Maximum lifecycle time for the remote process.
1308
- Accepts ISO 8601 duration format. Defaults to `P2DT12H`.
1309
-
1310
- Returns:
1311
- AppRun: An application run object that can be used to monitor and retrieve the result of the run.
1312
- """
1313
- if isinstance(app, str):
1314
- app = App({"command": app, "shell": True})
1315
-
1316
- path = "/appmesh/app/run"
1317
- resp = self._request_http(
1318
- AppMeshClient.Method.POST,
1319
- body=app.json(),
1320
- path=path,
1321
- query={
1322
- "timeout": self._parse_duration(max_time_seconds),
1323
- "lifecycle": self._parse_duration(life_cycle_seconds),
1324
- },
1325
- )
1326
- if resp.status_code != HTTPStatus.OK:
1327
- raise Exception(resp.text)
1328
-
1329
- # Return an AppRun object with the application name and process UUID
1330
- return AppRun(self, resp.json()["name"], resp.json()["process_uuid"])
1331
-
1332
- def run_async_wait(self, run: AppRun, stdout_print: bool = True, timeout: int = 0) -> int:
1333
- """Wait for an async run to be finished
1334
-
1335
- Args:
1336
- run (AppRun): asyncrized run result from run_async().
1337
- stdout_print (bool, optional): print remote stdout to local or not.
1338
- timeout (int, optional): wait max timeout seconds and return if not finished, 0 means wait until finished
1339
-
1340
- Returns:
1341
- int: return exit code if process finished, return None for timeout or exception.
1342
- """
1343
- if run:
1344
- last_output_position = 0
1345
- start = datetime.now()
1346
- interval = 1 if self.__class__.__name__ == "AppMeshClient" else 1000
1347
- while len(run.proc_uid) > 0:
1348
- app_out = self.app_output(app_name=run.app_name, stdout_position=last_output_position, stdout_index=0, process_uuid=run.proc_uid, timeout=interval)
1349
- if app_out.output and stdout_print:
1350
- print(app_out.output, end="")
1351
- if app_out.out_position is not None:
1352
- last_output_position = app_out.out_position
1353
- if app_out.exit_code is not None:
1354
- # success
1355
- self.app_delete(run.app_name)
1356
- return app_out.exit_code
1357
- if app_out.status_code != HTTPStatus.OK:
1358
- # failed
1359
- break
1360
- if timeout > 0 and (datetime.now() - start).seconds > timeout:
1361
- # timeout
1362
- break
1363
- return None
1364
-
1365
- def run_sync(
1366
- self,
1367
- app: Union[App, str],
1368
- stdout_print: bool = True,
1369
- max_time_seconds: Union[int, str] = DURATION_TWO_DAYS_ISO,
1370
- life_cycle_seconds: Union[int, str] = DURATION_TWO_DAYS_HALF_ISO,
1371
- ) -> Tuple[Union[int, None], str]:
1372
- """Synchronously run an application remotely, blocking until completion, and return the result.
1373
-
1374
- If 'app' is a string, it is treated as a shell command and converted to an App instance.
1375
- If 'app' is App object, the name attribute is used to run an existing application if specified.
1376
-
1377
- Args:
1378
- app (Union[App, str]): An App instance or a shell command string.
1379
- If a string, an App instance is created as:
1380
- `appmesh_client.App({"command": "<command_string>", "shell": True})`
1381
- stdout_print (bool, optional): If True, prints the remote stdout locally. Defaults to True.
1382
- max_time_seconds (Union[int, str], optional): Maximum runtime for the remote process.
1383
- Supports ISO 8601 duration format (e.g., 'P1Y2M3DT4H5M6S', 'P5W'). Defaults to DEFAULT_RUN_APP_TIMEOUT_SECONDS.
1384
- life_cycle_seconds (Union[int, str], optional): Maximum lifecycle time for the remote process.
1385
- Supports ISO 8601 duration format. Defaults to DEFAULT_RUN_APP_LIFECYCLE_SECONDS.
1386
-
1387
- Returns:
1388
- Tuple[Union[int, None], str]: Exit code of the process (None if unavailable) and the stdout text.
1389
- """
1390
- if isinstance(app, str):
1391
- app = App({"command": app, "shell": True})
1392
-
1393
- path = "/appmesh/app/syncrun"
1394
- resp = self._request_http(
1395
- AppMeshClient.Method.POST,
1396
- body=app.json(),
1397
- path=path,
1398
- query={
1399
- "timeout": self._parse_duration(max_time_seconds),
1400
- "lifecycle": self._parse_duration(life_cycle_seconds),
1401
- },
1402
- )
1403
- exit_code = None
1404
- if resp.status_code == HTTPStatus.OK:
1405
- if stdout_print:
1406
- print(resp.text, end="")
1407
- if "Exit-Code" in resp.headers:
1408
- exit_code = int(resp.headers.get("Exit-Code"))
1409
- elif stdout_print:
1410
- print(resp.text)
1411
-
1412
- return exit_code, resp.text
1413
-
1414
- def _request_http(self, method: Method, path: str, query: dict = None, header: dict = None, body=None) -> requests.Response:
1415
- """REST API
1416
-
1417
- Args:
1418
- method (Method): AppMeshClient.Method.
1419
- path (str): URI patch str.
1420
- query (dict, optional): HTTP query parameters.
1421
- header (dict, optional): HTTP headers.
1422
- body (_type_, optional): object to send in the body of the :class:`Request`.
1423
-
1424
- Returns:
1425
- requests.Response: HTTP response
1426
- """
1427
- rest_url = parse.urljoin(self.server_url, path)
1428
-
1429
- header = {} if header is None else header
1430
- if self.jwt_token:
1431
- header["Authorization"] = "Bearer " + self.jwt_token
1432
- if self.forwarding_host and len(self.forwarding_host) > 0:
1433
- if ":" in self.forwarding_host:
1434
- header[HTTP_HEADER_KEY_X_TARGET_HOST] = self.forwarding_host
1435
- else:
1436
- header[HTTP_HEADER_KEY_X_TARGET_HOST] = self.forwarding_host + ":" + str(parse.urlsplit(self.server_url).port)
1437
- header[HTTP_HEADER_KEY_USER_AGENT] = HTTP_USER_AGENT
1438
-
1439
- if method is AppMeshClient.Method.GET:
1440
- return requests.get(url=rest_url, params=query, headers=header, cert=self.ssl_client_cert, verify=self.ssl_verify, timeout=self.rest_timeout)
1441
- elif method is AppMeshClient.Method.POST:
1442
- return requests.post(
1443
- url=rest_url, params=query, headers=header, data=json.dumps(body) if type(body) in (dict, list) else body, cert=self.ssl_client_cert, verify=self.ssl_verify, timeout=self.rest_timeout
1444
- )
1445
- elif method is AppMeshClient.Method.POST_STREAM:
1446
- return requests.post(url=rest_url, params=query, headers=header, data=body, cert=self.ssl_client_cert, verify=self.ssl_verify, stream=True, timeout=self.rest_timeout)
1447
- elif method is AppMeshClient.Method.DELETE:
1448
- return requests.delete(url=rest_url, headers=header, cert=self.ssl_client_cert, verify=self.ssl_verify, timeout=self.rest_timeout)
1449
- elif method is AppMeshClient.Method.PUT:
1450
- 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)
1451
- else:
1452
- raise Exception("Invalid http method", method)
1453
-
1454
-
1455
- class AppMeshClientTCP(AppMeshClient):
1456
- """
1457
- Client SDK for interacting with the App Mesh service over TCP, with enhanced support for large file transfers.
1458
-
1459
- The `AppMeshClientTCP` class extends the functionality of `AppMeshClient` by offering a TCP-based communication layer
1460
- for the App Mesh REST API. It overrides the file download and upload methods to support large file transfers with
1461
- improved performance, leveraging TCP for lower latency and higher throughput compared to HTTP.
1462
-
1463
- This client is suitable for applications requiring efficient data transfers and high-throughput operations within the
1464
- App Mesh ecosystem, while maintaining compatibility with all other attributes and methods from `AppMeshClient`.
1465
-
1466
- Dependency:
1467
- - Install the required package for message serialization:
1468
- pip3 install msgpack
1469
-
1470
- Usage:
1471
- - Import the client module:
1472
- from appmesh import appmesh_client
1473
-
1474
- Example:
1475
- client = appmesh_client.AppMeshClientTCP()
1476
- client.login("your-name", "your-password")
1477
- client.file_download("/tmp/os-release", "os-release")
1478
-
1479
- Attributes:
1480
- - Inherits all attributes from `AppMeshClient`, including TLS secure connections and JWT-based authentication.
1481
- - Optimized for TCP-based communication to provide better performance for large file transfers.
1482
-
1483
- Methods:
1484
- - file_download()
1485
- - file_upload()
1486
- - Inherits all other methods from `AppMeshClient`, providing a consistent interface for managing applications within App Mesh.
1487
- """
1488
-
1489
- def __init__(
1490
- self,
1491
- rest_ssl_verify=DEFAULT_SSL_CA_CERT_PATH if os.path.exists(DEFAULT_SSL_CA_CERT_PATH) else False,
1492
- rest_ssl_client_cert=None,
1493
- jwt_token=None,
1494
- tcp_address=("localhost", 6059),
1495
- ):
1496
- """Construct an App Mesh client TCP object to communicate securely with an App Mesh server over TLS.
1497
-
1498
- Args:
1499
- rest_ssl_verify (Union[bool, str], optional): Specifies SSL certificate verification behavior. Can be:
1500
- - `True`: Uses the system’s default CA certificates to verify the server’s identity.
1501
- - `False`: Disables SSL certificate verification (insecure, intended for development).
1502
- - `str`: Specifies a custom CA bundle or directory for server certificate verification. If a string is provided,
1503
- it should either be a file path to a custom CA certificate (CA bundle) or a directory path containing multiple
1504
- certificates (CA directory).
1505
-
1506
- **Note**: Unlike HTTP requests, TCP connections cannot automatically retrieve intermediate or public CA certificates.
1507
- When `rest_ssl_verify` is a path, it explicitly identifies a CA issuer to ensure certificate validation.
1508
-
1509
- rest_ssl_client_cert (Union[str, Tuple[str, str]], optional): Path to the SSL client certificate and key. If a `str`,
1510
- it should be the path to a PEM file containing both the client certificate and private key. If a `tuple`, it should
1511
- be a pair of paths: (`cert`, `key`), where `cert` is the client certificate file and `key` is the private key file.
1512
-
1513
- jwt_token (str, optional): JWT token for authentication. Used in methods requiring login and user authorization.
1514
-
1515
- tcp_address (Tuple[str, int], optional): Address and port for establishing a TCP connection to the server.
1516
- Defaults to `("localhost", 6059)`.
1517
- """
1518
- self.tcp_address = tcp_address
1519
- self.__socket_client = None
1520
- super().__init__(rest_ssl_verify=rest_ssl_verify, rest_ssl_client_cert=rest_ssl_client_cert, jwt_token=jwt_token)
1521
-
1522
- def __del__(self) -> None:
1523
- """De-construction"""
1524
- self.__close_socket()
1525
-
1526
- def __connect_socket(self) -> None:
1527
- """Establish tcp connection"""
1528
- context = ssl.create_default_context(ssl.Purpose.SERVER_AUTH)
1529
- # Set minimum TLS version
1530
- if hasattr(context, "minimum_version"):
1531
- context.minimum_version = ssl.TLSVersion.TLSv1_2
1532
- else:
1533
- context.options |= ssl.OP_NO_TLSv1 | ssl.OP_NO_TLSv1_1
1534
- # Configure SSL verification
1535
- if not self.ssl_verify:
1536
- context.verify_mode = ssl.CERT_NONE
1537
- else:
1538
- context.verify_mode = ssl.CERT_REQUIRED # Require certificate verification
1539
- context.load_default_certs() # Load system's default CA certificates
1540
- if isinstance(self.ssl_verify, str):
1541
- if os.path.isfile(self.ssl_verify):
1542
- # Load custom CA certificate file
1543
- context.load_verify_locations(cafile=self.ssl_verify)
1544
- elif os.path.isdir(self.ssl_verify):
1545
- # Load CA certificates from directory
1546
- context.load_verify_locations(capath=self.ssl_verify)
1547
- else:
1548
- raise ValueError(f"ssl_verify path '{self.ssl_verify}' is neither a file nor a directory")
1549
-
1550
- if self.ssl_client_cert is not None:
1551
- # Load client-side certificate and private key
1552
- context.load_cert_chain(certfile=self.ssl_client_cert[0], keyfile=self.ssl_client_cert[1])
1553
-
1554
- # Create a TCP socket
1555
- sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
1556
- sock.setblocking(True)
1557
- # Wrap the socket with SSL/TLS
1558
- self.__socket_client = context.wrap_socket(sock, server_hostname=self.tcp_address[0])
1559
- # Connect to the server
1560
- self.__socket_client.connect(self.tcp_address)
1561
- # Disable Nagle's algorithm
1562
- self.__socket_client.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
1563
-
1564
- def __close_socket(self) -> None:
1565
- """Close socket connection"""
1566
- if self.__socket_client:
1567
- try:
1568
- self.__socket_client.close()
1569
- self.__socket_client = None
1570
- except Exception as ex:
1571
- print(ex)
1572
-
1573
- def __recvall(self, length: int) -> bytes:
1574
- """socket recv data with fixed length
1575
- https://stackoverflow.com/questions/64466530/using-a-custom-socket-recvall-function-works-only-if-thread-is-put-to-sleep
1576
- Args:
1577
- length (bytes): data length to be received
1578
-
1579
- Raises:
1580
- EOFError: socket closed unexpectedly
1581
-
1582
- Returns:
1583
- bytes: socket data
1584
- """
1585
- fragments = []
1586
- while length:
1587
- chunk = self.__socket_client.recv(length)
1588
- if not chunk:
1589
- raise EOFError("socket closed")
1590
- length -= len(chunk)
1591
- fragments.append(chunk)
1592
- return b"".join(fragments)
1593
-
1594
- def _request_http(self, method: AppMeshClient.Method, path: str, query: dict = None, header: dict = None, body=None) -> requests.Response:
1595
- """TCP API
1596
-
1597
- Args:
1598
- method (Method): AppMeshClient.Method.
1599
- path (str): URI patch str.
1600
- query (dict, optional): HTTP query parameters.
1601
- header (dict, optional): HTTP headers.
1602
- body (_type_, optional): object to send in the body of the :class:`Request`.
1603
-
1604
- Returns:
1605
- requests.Response: HTTP response
1606
- """
1607
- import msgpack
1608
-
1609
- class RequestMsg:
1610
- """HTTP request message"""
1611
-
1612
- uuid: str = ""
1613
- request_uri: str = ""
1614
- http_method: str = ""
1615
- client_addr: str = ""
1616
- body: bytes = b""
1617
- headers: dict = {}
1618
- querys: dict = {}
1619
-
1620
- def serialize(self) -> bytes:
1621
- """Serialize request message to bytes"""
1622
- # http://www.cnitblog.com/luckydmz/archive/2019/11/20/91959.html
1623
- self_dict = vars(self)
1624
- self_dict["headers"] = self.headers
1625
- self_dict["querys"] = self.querys
1626
- return msgpack.dumps(self_dict)
1627
-
1628
- class ResponseMsg:
1629
- """HTTP response message"""
1630
-
1631
- uuid: str = ""
1632
- request_uri: str = ""
1633
- http_status: int = 0
1634
- body_msg_type: str = ""
1635
- body: str = ""
1636
- headers: dict = {}
1637
-
1638
- def desirialize(self, buf: bytes):
1639
- """Deserialize response message"""
1640
- dic = msgpack.unpackb(buf)
1641
- for k, v in dic.items():
1642
- setattr(self, k, v)
1643
- return self
1644
-
1645
- if self.__socket_client is None:
1646
- self.__connect_socket()
1647
-
1648
- appmesh_request = RequestMsg()
1649
- if super().jwt_token:
1650
- appmesh_request.headers["Authorization"] = "Bearer " + super().jwt_token
1651
- if super().forwarding_host and len(super().forwarding_host) > 0:
1652
- raise Exception("Not support forward request in TCP mode")
1653
- appmesh_request.headers[HTTP_HEADER_KEY_USER_AGENT] = HTTP_USER_AGENT_TCP
1654
- appmesh_request.uuid = str(uuid.uuid1())
1655
- appmesh_request.http_method = method.value
1656
- appmesh_request.request_uri = path
1657
- appmesh_request.client_addr = socket.gethostname()
1658
- if body:
1659
- if isinstance(body, dict) or isinstance(body, list):
1660
- appmesh_request.body = bytes(json.dumps(body, indent=2), ENCODING_UTF8)
1661
- elif isinstance(body, str):
1662
- appmesh_request.body = bytes(body, ENCODING_UTF8)
1663
- elif isinstance(body, bytes):
1664
- appmesh_request.body = body
1665
- else:
1666
- raise Exception(f"UnSupported body type: {type(body)}")
1667
- if header:
1668
- for k, v in header.items():
1669
- appmesh_request.headers[k] = v
1670
- if query:
1671
- for k, v in query.items():
1672
- appmesh_request.querys[k] = v
1673
- data = appmesh_request.serialize()
1674
- self.__socket_client.sendall(len(data).to_bytes(TCP_HEADER_LENGTH, byteorder="big", signed=False))
1675
- self.__socket_client.sendall(data)
1676
-
1677
- # https://developers.google.com/protocol-buffers/docs/pythontutorial
1678
- # https://stackoverflow.com/questions/33913308/socket-module-how-to-send-integer
1679
- resp_data = bytes()
1680
- resp_data = self.__recvall(int.from_bytes(self.__recvall(TCP_HEADER_LENGTH), byteorder="big", signed=False))
1681
- if resp_data is None or len(resp_data) == 0:
1682
- self.__close_socket()
1683
- raise Exception("socket connection broken")
1684
- appmesh_resp = ResponseMsg().desirialize(resp_data)
1685
- response = requests.Response()
1686
- response.status_code = appmesh_resp.http_status
1687
- response.encoding = ENCODING_UTF8
1688
- response._content = appmesh_resp.body.encode(ENCODING_UTF8)
1689
- response.headers = appmesh_resp.headers
1690
- if appmesh_resp.body_msg_type:
1691
- response.headers["Content-Type"] = appmesh_resp.body_msg_type
1692
- return response
1693
-
1694
- ########################################
1695
- # File management
1696
- ########################################
1697
- def file_download(self, remote_file: str, local_file: str, apply_file_attributes: bool = True) -> None:
1698
- """Copy a remote file to local, the local file will have the same permission as the remote file
1699
-
1700
- Args:
1701
- remote_file (str): the remote file path.
1702
- local_file (str): the local file path to be downloaded.
1703
- apply_file_attributes (bool): whether to apply file attributes (permissions, owner, group) to the local file.
1704
- """
1705
- header = {"File-Path": remote_file}
1706
- header[HTTP_HEADER_KEY_X_RECV_FILE_SOCKET] = "true"
1707
- resp = self._request_http(AppMeshClient.Method.GET, path="/appmesh/file/download", header=header)
1708
-
1709
- resp.raise_for_status()
1710
- if HTTP_HEADER_KEY_X_RECV_FILE_SOCKET not in resp.headers:
1711
- raise ValueError(f"Server did not respond with socket transfer option: {HTTP_HEADER_KEY_X_RECV_FILE_SOCKET}")
1712
-
1713
- with open(local_file, "wb") as fp:
1714
- chunk_data = bytes()
1715
- chunk_size = int.from_bytes(self.__recvall(TCP_HEADER_LENGTH), byteorder="big", signed=False)
1716
- while chunk_size > 0:
1717
- chunk_data = self.__recvall(chunk_size)
1718
- if chunk_data is None or len(chunk_data) == 0:
1719
- self.__close_socket()
1720
- raise Exception("socket connection broken")
1721
- fp.write(chunk_data)
1722
- chunk_size = int.from_bytes(self.__recvall(TCP_HEADER_LENGTH), byteorder="big", signed=False)
1723
-
1724
- if apply_file_attributes:
1725
- if "File-Mode" in resp.headers:
1726
- os.chmod(path=local_file, mode=int(resp.headers["File-Mode"]))
1727
- if "File-User" in resp.headers and "File-Group" in resp.headers:
1728
- file_uid = int(resp.headers["File-User"])
1729
- file_gid = int(resp.headers["File-Group"])
1730
- try:
1731
- os.chown(path=local_file, uid=file_uid, gid=file_gid)
1732
- except PermissionError:
1733
- print(f"Warning: Unable to change owner/group of {local_file}. Operation requires elevated privileges.")
1734
-
1735
- def file_upload(self, local_file: str, remote_file: str, apply_file_attributes: bool = True) -> None:
1736
- """Upload a local file to the remote server, the remote file will have the same permission as the local file
1737
-
1738
- Dependency:
1739
- sudo apt install python3-pip
1740
- pip3 install requests_toolbelt
1741
-
1742
- Args:
1743
- local_file (str): the local file path.
1744
- remote_file (str): the target remote file to be uploaded.
1745
- apply_file_attributes (bool): whether to upload file attributes (permissions, owner, group) along with the file.
1746
- """
1747
- if not os.path.exists(local_file):
1748
- raise FileNotFoundError(f"Local file not found: {local_file}")
1749
-
1750
- with open(file=local_file, mode="rb") as fp:
1751
- header = {"File-Path": remote_file, "Content-Type": "text/plain"}
1752
- header[HTTP_HEADER_KEY_X_SEND_FILE_SOCKET] = "true"
1753
-
1754
- if apply_file_attributes:
1755
- file_stat = os.stat(local_file)
1756
- header["File-Mode"] = str(file_stat.st_mode & 0o777) # Mask to keep only permission bits
1757
- header["File-User"] = str(file_stat.st_uid)
1758
- header["File-Group"] = str(file_stat.st_gid)
1759
-
1760
- # https://stackoverflow.com/questions/22567306/python-requests-file-upload
1761
- resp = self._request_http(AppMeshClient.Method.POST, path="/appmesh/file/upload", header=header)
1762
-
1763
- resp.raise_for_status()
1764
- if HTTP_HEADER_KEY_X_SEND_FILE_SOCKET not in resp.headers:
1765
- raise ValueError(f"Server did not respond with socket transfer option: {HTTP_HEADER_KEY_X_SEND_FILE_SOCKET}")
1766
-
1767
- chunk_size = TCP_BLOCK_SIZE
1768
- while True:
1769
- chunk_data = fp.read(chunk_size)
1770
- if not chunk_data:
1771
- self.__socket_client.sendall((0).to_bytes(TCP_HEADER_LENGTH, byteorder="big", signed=False))
1772
- break
1773
- self.__socket_client.sendall(len(chunk_data).to_bytes(TCP_HEADER_LENGTH, byteorder="big", signed=False))
1774
- self.__socket_client.sendall(chunk_data)
8
+ from .http_client import AppMeshClient, App, AppOutput