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 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
- _SSL_CA_PEM_FILE = "/opt/appmesh/ssl/ca.pem"
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
- """App Mesh client object used to access App Mesh REST Service
344
-
345
- - install pip package: python3 -m pip install --upgrade appmesh
346
- - import module: from appmesh import appmesh_client
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=_SSL_CA_PEM_FILE if os.path.exists(_SSL_CA_PEM_FILE) else False,
363
- rest_ssl_client_cert=(_SSL_CLIENT_PEM_FILE, _SSL_CLIENT_PEM_KEY_FILE) if os.path.exists(_SSL_CLIENT_PEM_FILE) else None,
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, 0 is health.
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) -> bool:
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
- if resp.status_code != HTTPStatus.OK:
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=512):
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
- def file_upload(self, local_file: str, file_path: str) -> bool:
1175
- """Upload a local file to the remote server, the remote file will have the same permission as the local file
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
- file_stat = os.stat(local_file)
1193
- header = {}
1194
- header["File-Path"] = file_path
1195
- header["File-Mode"] = str(file_stat.st_mode)
1196
- header["File-User"] = str(file_stat.st_uid)
1197
- header["File-Group"] = str(file_stat.st_gid)
1198
- header["Content-Type"] = encoder.content_type
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
- if resp.status_code != HTTPStatus.OK:
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[HTTP_USER_AGENT_HEADER_NAME] = HTTP_USER_AGENT
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
- """Client object used to access App Mesh REST Service over TCP (better performance than AppMeshClient)
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
- pip3 install msgpack
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=_SSL_CA_PEM_FILE if os.path.exists(_SSL_CA_PEM_FILE) else False,
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[HTTP_USER_AGENT_HEADER_NAME] = HTTP_USER_AGENT_TCP
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) -> bool:
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
- if resp.status_code == HTTPStatus.OK and HTTP_HEADER_KEY_X_RECV_FILE_SOCKET in resp.headers:
1567
- with open(local_file, "wb") as fp:
1568
- chunk_data = bytes()
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
- while chunk_size > 0:
1571
- chunk_data = self.__recvall(chunk_size)
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 Exception as ex:
1585
- print(ex)
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
- file_stat = os.stat(local_file)
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
- if resp.status_code == HTTPStatus.OK and HTTP_HEADER_KEY_X_SEND_FILE_SOCKET in resp.headers:
1616
- chunk_size = 8 * 1024 # (8 KB in bytes), 131072 bytes (128 KB) is default max ssl buffer size
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
- while chunk_data:
1619
- self.__socket_client.sendall(len(chunk_data).to_bytes(TCP_MESSAGE_HEADER_LENGTH, byteorder="big", signed=False))
1620
- self.__socket_client.sendall(chunk_data)
1621
- chunk_data = fp.read(chunk_size)
1622
- self.__socket_client.sendall(int(0).to_bytes(TCP_MESSAGE_HEADER_LENGTH, byteorder="big", signed=False))
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.2
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
  [![OpenSSF Scorecard](https://api.securityscorecards.dev/projects/github.com/laoshanxi/app-mesh/badge)](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 a `Multi-Tenant`, `Cloud Native`, `Micro Service` application management platform designed to manage, schedule, and monitor applications. Each app can be a specific microservice for service discovery or a standard app with replication. App Mesh ensures all defined applications run on time with the specified behavior and resource requests. The platform supports both standalone and cluster modes and provides REST APIs, a command-line interface, and a web UI.
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 | JSON | YAML |
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,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (75.1.0)
2
+ Generator: setuptools (75.2.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
@@ -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,,