appmesh 1.3.7__py3-none-any.whl → 1.3.9__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- appmesh/__init__.py +2 -2
- appmesh/appmesh_client.py +6 -1221
- appmesh/http_client.py +1216 -0
- appmesh/{appmesh_client_tcp.py → tcp_client.py} +17 -160
- appmesh/tcp_messages.py +41 -0
- appmesh/tcp_transport.py +160 -0
- {appmesh-1.3.7.dist-info → appmesh-1.3.9.dist-info}/METADATA +1 -1
- appmesh-1.3.9.dist-info/RECORD +13 -0
- {appmesh-1.3.7.dist-info → appmesh-1.3.9.dist-info}/WHEEL +1 -1
- appmesh-1.3.7.dist-info/RECORD +0 -10
- {appmesh-1.3.7.dist-info → appmesh-1.3.9.dist-info}/top_level.txt +0 -0
appmesh/appmesh_client.py
CHANGED
@@ -1,1223 +1,8 @@
|
|
1
|
-
|
1
|
+
# appmesh_client.py
|
2
2
|
|
3
|
-
#
|
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.
|
4
7
|
|
5
|
-
|
6
|
-
import abc
|
7
|
-
import base64
|
8
|
-
import json
|
9
|
-
import os
|
10
|
-
from datetime import datetime
|
11
|
-
from enum import Enum, unique
|
12
|
-
from http import HTTPStatus
|
13
|
-
from typing import Tuple, Union
|
14
|
-
from urllib import parse
|
15
|
-
|
16
|
-
# Third-party imports
|
17
|
-
import aniso8601
|
18
|
-
import requests
|
19
|
-
|
20
|
-
# Local application-specific imports
|
21
|
-
from .app import App
|
22
|
-
from .app_run import AppRun
|
23
|
-
from .app_output import AppOutput
|
24
|
-
|
25
|
-
|
26
|
-
class AppMeshClient(metaclass=abc.ABCMeta):
|
27
|
-
"""
|
28
|
-
Client SDK for interacting with the App Mesh service via REST API.
|
29
|
-
|
30
|
-
The `AppMeshClient` class provides a comprehensive interface for managing and monitoring distributed applications
|
31
|
-
within the App Mesh ecosystem. It enables communication with the App Mesh REST API for operations such as
|
32
|
-
application lifecycle management, monitoring, and configuration.
|
33
|
-
|
34
|
-
This client is designed for direct usage in applications that require access to App Mesh services over HTTP-based REST.
|
35
|
-
|
36
|
-
Usage:
|
37
|
-
- Install the App Mesh Python package:
|
38
|
-
python3 -m pip install --upgrade appmesh
|
39
|
-
- Import the client module:
|
40
|
-
from appmesh import appmesh_client
|
41
|
-
|
42
|
-
Example:
|
43
|
-
client = appmesh_client.AppMeshClient()
|
44
|
-
client.login("your-name", "your-password")
|
45
|
-
response = client.app_view(app_name='ping')
|
46
|
-
|
47
|
-
Attributes:
|
48
|
-
- TLS (Transport Layer Security): Supports secure connections between the client and App Mesh service,
|
49
|
-
ensuring encrypted communication.
|
50
|
-
- JWT (JSON Web Token) and RBAC (Role-Based Access Control): Provides secure API access with
|
51
|
-
token-based authentication and authorization to enforce fine-grained permissions.
|
52
|
-
|
53
|
-
Methods:
|
54
|
-
# Authentication Management
|
55
|
-
- login()
|
56
|
-
- logoff()
|
57
|
-
- authentication()
|
58
|
-
- renew()
|
59
|
-
- totp_disable()
|
60
|
-
- totp_secret()
|
61
|
-
- totp_setup()
|
62
|
-
|
63
|
-
# Application Management
|
64
|
-
- app_add()
|
65
|
-
- app_delete()
|
66
|
-
- app_disable()
|
67
|
-
- app_enable()
|
68
|
-
- app_health()
|
69
|
-
- app_output()
|
70
|
-
- app_view()
|
71
|
-
- app_view_all()
|
72
|
-
|
73
|
-
# Run Application Operations
|
74
|
-
- run_async()
|
75
|
-
- run_async_wait()
|
76
|
-
- run_sync()
|
77
|
-
|
78
|
-
# System Management
|
79
|
-
- forwarding_host
|
80
|
-
- config_set()
|
81
|
-
- config_view()
|
82
|
-
- log_level_set()
|
83
|
-
- host_resource()
|
84
|
-
- metrics()
|
85
|
-
- tag_add()
|
86
|
-
- tag_delete()
|
87
|
-
- tag_view()
|
88
|
-
|
89
|
-
# File Management
|
90
|
-
- file_download()
|
91
|
-
- file_upload()
|
92
|
-
|
93
|
-
# User and Role Management
|
94
|
-
- user_add()
|
95
|
-
- user_delete()
|
96
|
-
- user_lock()
|
97
|
-
- user_passwd_update()
|
98
|
-
- user_self()
|
99
|
-
- user_unlock()
|
100
|
-
- users_view()
|
101
|
-
- permissions_for_user()
|
102
|
-
- permissions_view()
|
103
|
-
- role_delete()
|
104
|
-
- role_update()
|
105
|
-
- roles_view()
|
106
|
-
- groups_view()
|
107
|
-
"""
|
108
|
-
|
109
|
-
DURATION_ONE_WEEK_ISO = "P1W"
|
110
|
-
DURATION_TWO_DAYS_ISO = "P2D"
|
111
|
-
DURATION_TWO_DAYS_HALF_ISO = "P2DT12H"
|
112
|
-
|
113
|
-
DEFAULT_SSL_CA_CERT_PATH = "/opt/appmesh/ssl/ca.pem"
|
114
|
-
DEFAULT_SSL_CLIENT_CERT_PATH = "/opt/appmesh/ssl/client.pem"
|
115
|
-
DEFAULT_SSL_CLIENT_KEY_PATH = "/opt/appmesh/ssl/client-key.pem"
|
116
|
-
|
117
|
-
JSON_KEY_MESSAGE = "message"
|
118
|
-
HTTP_USER_AGENT = "appmesh/python"
|
119
|
-
HTTP_HEADER_KEY_USER_AGENT = "User-Agent"
|
120
|
-
HTTP_HEADER_KEY_X_TARGET_HOST = "X-Target-Host"
|
121
|
-
|
122
|
-
@unique
|
123
|
-
class Method(Enum):
|
124
|
-
"""REST methods"""
|
125
|
-
|
126
|
-
GET = "GET"
|
127
|
-
PUT = "PUT"
|
128
|
-
POST = "POST"
|
129
|
-
DELETE = "DELETE"
|
130
|
-
POST_STREAM = "POST_STREAM"
|
131
|
-
|
132
|
-
def __init__(
|
133
|
-
self,
|
134
|
-
rest_url: str = "https://127.0.0.1:6060",
|
135
|
-
rest_ssl_verify=DEFAULT_SSL_CA_CERT_PATH if os.path.exists(DEFAULT_SSL_CA_CERT_PATH) else False,
|
136
|
-
rest_ssl_client_cert=(DEFAULT_SSL_CLIENT_CERT_PATH, DEFAULT_SSL_CLIENT_KEY_PATH) if os.path.exists(DEFAULT_SSL_CLIENT_CERT_PATH) else None,
|
137
|
-
rest_timeout=(60, 300),
|
138
|
-
jwt_token=None,
|
139
|
-
):
|
140
|
-
"""Initialize an App Mesh HTTP client for interacting with the App Mesh server via secure HTTPS.
|
141
|
-
|
142
|
-
Args:
|
143
|
-
rest_url (str, optional): The server's base URI, including protocol, hostname, and port. Defaults to `"https://127.0.0.1:6060"`.
|
144
|
-
|
145
|
-
rest_ssl_verify (Union[bool, str], optional): Configures SSL certificate verification for HTTPS requests:
|
146
|
-
- `True`: Uses system CA certificates to verify the server's identity.
|
147
|
-
- `False`: Disables SSL verification (insecure, use cautiously for development).
|
148
|
-
- `str`: Path to a custom CA certificate or directory for verification. This option allows custom CA configuration,
|
149
|
-
which may be necessary in environments requiring specific CA chains that differ from the default system CAs.
|
150
|
-
|
151
|
-
rest_ssl_client_cert (Union[tuple, str], optional): Specifies a client certificate for mutual TLS authentication:
|
152
|
-
- If a `str`, provides the path to a PEM file with both client certificate and private key.
|
153
|
-
- If a `tuple`, contains two paths as (`cert`, `key`), where `cert` is the certificate file and `key` is the private key file.
|
154
|
-
|
155
|
-
rest_timeout (tuple, optional): HTTP connection timeouts for API requests, as `(connect_timeout, read_timeout)`.
|
156
|
-
The default is `(60, 300)`, where `60` seconds is the maximum time to establish a connection and `300` seconds for the maximum read duration.
|
157
|
-
|
158
|
-
jwt_token (str, optional): JWT token for API authentication, used in headers to authorize requests where required.
|
159
|
-
|
160
|
-
"""
|
161
|
-
|
162
|
-
self.server_url = rest_url
|
163
|
-
self._jwt_token = jwt_token
|
164
|
-
self.ssl_verify = rest_ssl_verify
|
165
|
-
self.ssl_client_cert = rest_ssl_client_cert
|
166
|
-
self.rest_timeout = rest_timeout
|
167
|
-
self._forwarding_host = None
|
168
|
-
|
169
|
-
@property
|
170
|
-
def jwt_token(self) -> str:
|
171
|
-
"""Get the current JWT (JSON Web Token) used for authentication.
|
172
|
-
|
173
|
-
This property manages the authentication token used for securing API requests.
|
174
|
-
The token is used to authenticate and authorize requests to the service.
|
175
|
-
|
176
|
-
Returns:
|
177
|
-
str: The current JWT token string.
|
178
|
-
Returns empty string if no token is set.
|
179
|
-
|
180
|
-
Notes:
|
181
|
-
- The token typically includes claims for identity and permissions
|
182
|
-
- Token format: "header.payload.signature"
|
183
|
-
- Tokens are time-sensitive and may expire
|
184
|
-
"""
|
185
|
-
return self._jwt_token
|
186
|
-
|
187
|
-
@jwt_token.setter
|
188
|
-
def jwt_token(self, token: str) -> None:
|
189
|
-
"""Set the JWT token for authentication.
|
190
|
-
|
191
|
-
Configure the JWT token used for authenticating requests. The token should be
|
192
|
-
a valid JWT issued by a trusted authority.
|
193
|
-
|
194
|
-
Args:
|
195
|
-
token (str): JWT token string in standard JWT format
|
196
|
-
(e.g., "eyJhbGci...payload...signature")
|
197
|
-
Pass empty string to clear the token.
|
198
|
-
|
199
|
-
Example:
|
200
|
-
>>> client.jwt_token = "eyJhbGci..." # Set new token
|
201
|
-
>>> client.jwt_token = "" # Clear token
|
202
|
-
|
203
|
-
Notes:
|
204
|
-
Security best practices:
|
205
|
-
- Store tokens securely
|
206
|
-
- Never log or expose complete tokens
|
207
|
-
- Refresh tokens before expiration
|
208
|
-
- Validate token format before setting
|
209
|
-
"""
|
210
|
-
self._jwt_token = token
|
211
|
-
|
212
|
-
@property
|
213
|
-
def forwarding_host(self) -> str:
|
214
|
-
"""Get the target host address for request forwarding in a cluster setup.
|
215
|
-
|
216
|
-
This property manages the destination host where requests will be forwarded to
|
217
|
-
within a cluster configuration. The host can be specified in two formats:
|
218
|
-
1. hostname/IP only: will use the current service's port
|
219
|
-
2. hostname/IP with port: will use the specified port
|
220
|
-
|
221
|
-
Returns:
|
222
|
-
str: The target host address in either format:
|
223
|
-
- "hostname" or "IP" (using current service port)
|
224
|
-
- "hostname:port" or "IP:port" (using specified port)
|
225
|
-
Returns empty string if no forwarding host is set.
|
226
|
-
|
227
|
-
Notes:
|
228
|
-
For proper JWT token sharing across the cluster:
|
229
|
-
- All nodes must share the same JWT salt configuration
|
230
|
-
- All nodes must use identical JWT issuer settings
|
231
|
-
- When port is omitted, current service port will be used
|
232
|
-
"""
|
233
|
-
return self._forwarding_host
|
234
|
-
|
235
|
-
@forwarding_host.setter
|
236
|
-
def forwarding_host(self, host: str) -> None:
|
237
|
-
"""Set the target host address for request forwarding.
|
238
|
-
|
239
|
-
Configure the destination host where requests should be forwarded to. This is
|
240
|
-
used in cluster setups for request routing and load distribution.
|
241
|
-
|
242
|
-
Args:
|
243
|
-
host (str): Target host address in one of two formats:
|
244
|
-
1. "hostname" or "IP" - will use current service port
|
245
|
-
(e.g., "backend-node" or "192.168.1.100")
|
246
|
-
2. "hostname:port" or "IP:port" - will use specified port
|
247
|
-
(e.g., "backend-node:6060" or "192.168.1.100:6060")
|
248
|
-
Pass empty string to disable forwarding.
|
249
|
-
|
250
|
-
Examples:
|
251
|
-
>>> client.forwarding_host = "backend-node:6060" # Use specific port
|
252
|
-
>>> client.forwarding_host = "backend-node" # Use current service port
|
253
|
-
>>> client.forwarding_host = None # Disable forwarding
|
254
|
-
"""
|
255
|
-
|
256
|
-
self._forwarding_host = host
|
257
|
-
|
258
|
-
########################################
|
259
|
-
# Security
|
260
|
-
########################################
|
261
|
-
def login(self, user_name: str, user_pwd: str, totp_code="", timeout_seconds=DURATION_ONE_WEEK_ISO) -> str:
|
262
|
-
"""Login with user name and password
|
263
|
-
|
264
|
-
Args:
|
265
|
-
user_name (str): the name of the user.
|
266
|
-
user_pwd (str): the password of the user.
|
267
|
-
totp_code (str, optional): the TOTP code if enabled for the user.
|
268
|
-
timeout_seconds (int | str, optional): token expire timeout of seconds. support ISO 8601 durations (e.g., 'P1Y2M3DT4H5M6S' 'P1W').
|
269
|
-
|
270
|
-
Returns:
|
271
|
-
str: JWT token.
|
272
|
-
"""
|
273
|
-
self.jwt_token = None
|
274
|
-
resp = self._request_http(
|
275
|
-
AppMeshClient.Method.POST,
|
276
|
-
path="/appmesh/login",
|
277
|
-
header={
|
278
|
-
"Authorization": "Basic " + base64.b64encode((user_name + ":" + user_pwd).encode()).decode(),
|
279
|
-
"Expire-Seconds": self._parse_duration(timeout_seconds),
|
280
|
-
},
|
281
|
-
)
|
282
|
-
if resp.status_code == HTTPStatus.OK:
|
283
|
-
if "Access-Token" in resp.json():
|
284
|
-
self.jwt_token = resp.json()["Access-Token"]
|
285
|
-
elif resp.status_code == HTTPStatus.UNAUTHORIZED and "Totp-Challenge" in resp.json():
|
286
|
-
challenge = resp.json()["Totp-Challenge"]
|
287
|
-
resp = self._request_http(
|
288
|
-
AppMeshClient.Method.POST,
|
289
|
-
path="/appmesh/totp/validate",
|
290
|
-
header={
|
291
|
-
"Username": base64.b64encode(user_name.encode()).decode(),
|
292
|
-
"Totp-Challenge": base64.b64encode(challenge.encode()).decode(),
|
293
|
-
"Totp": totp_code,
|
294
|
-
"Expire-Seconds": self._parse_duration(timeout_seconds),
|
295
|
-
},
|
296
|
-
)
|
297
|
-
if resp.status_code == HTTPStatus.OK:
|
298
|
-
if "Access-Token" in resp.json():
|
299
|
-
self.jwt_token = resp.json()["Access-Token"]
|
300
|
-
else:
|
301
|
-
raise Exception(resp.text)
|
302
|
-
else:
|
303
|
-
raise Exception(resp.text)
|
304
|
-
return self.jwt_token
|
305
|
-
|
306
|
-
def logoff(self) -> bool:
|
307
|
-
"""Logoff current session from server
|
308
|
-
|
309
|
-
Returns:
|
310
|
-
bool: logoff success or failure.
|
311
|
-
"""
|
312
|
-
resp = self._request_http(AppMeshClient.Method.POST, path="/appmesh/self/logoff")
|
313
|
-
if resp.status_code != HTTPStatus.OK:
|
314
|
-
raise Exception(resp.text)
|
315
|
-
self.jwt_token = None
|
316
|
-
return resp.status_code == HTTPStatus.OK
|
317
|
-
|
318
|
-
def authentication(self, token: str, permission=None) -> bool:
|
319
|
-
"""Login with token and verify permission when specified,
|
320
|
-
verified token will be stored in client object when success
|
321
|
-
|
322
|
-
Args:
|
323
|
-
token (str): JWT token returned from login().
|
324
|
-
permission (str, optional): the permission ID used to verify the token user
|
325
|
-
permission ID can be:
|
326
|
-
- pre-defined by App Mesh from security.yaml (e.g 'app-view', 'app-delete')
|
327
|
-
- defined by input from role_update() or security.yaml
|
328
|
-
|
329
|
-
Returns:
|
330
|
-
bool: authentication success or failure.
|
331
|
-
"""
|
332
|
-
old_token = self.jwt_token
|
333
|
-
self.jwt_token = token
|
334
|
-
headers = {}
|
335
|
-
if permission:
|
336
|
-
headers["Auth-Permission"] = permission
|
337
|
-
resp = self._request_http(AppMeshClient.Method.POST, path="/appmesh/auth", header=headers)
|
338
|
-
if resp.status_code != HTTPStatus.OK:
|
339
|
-
self.jwt_token = old_token
|
340
|
-
raise Exception(resp.text)
|
341
|
-
return resp.status_code == HTTPStatus.OK
|
342
|
-
|
343
|
-
def renew(self, timeout_seconds=DURATION_ONE_WEEK_ISO) -> str:
|
344
|
-
"""Renew current token
|
345
|
-
|
346
|
-
Args:
|
347
|
-
timeout_seconds (int | str, optional): token expire timeout of seconds. support ISO 8601 durations (e.g., 'P1Y2M3DT4H5M6S' 'P1W').
|
348
|
-
|
349
|
-
Returns:
|
350
|
-
str: The new JWT token if renew success, otherwise return None.
|
351
|
-
"""
|
352
|
-
assert self.jwt_token
|
353
|
-
resp = self._request_http(
|
354
|
-
AppMeshClient.Method.POST,
|
355
|
-
path="/appmesh/token/renew",
|
356
|
-
header={
|
357
|
-
"Expire-Seconds": self._parse_duration(timeout_seconds),
|
358
|
-
},
|
359
|
-
)
|
360
|
-
if resp.status_code == HTTPStatus.OK:
|
361
|
-
if "Access-Token" in resp.json():
|
362
|
-
self.jwt_token = resp.json()["Access-Token"]
|
363
|
-
return self.jwt_token
|
364
|
-
raise Exception(resp.text)
|
365
|
-
|
366
|
-
def totp_secret(self) -> str:
|
367
|
-
"""Generate TOTP secret for current login user and return MFA URI with JSON body
|
368
|
-
|
369
|
-
Returns:
|
370
|
-
str: TOTP secret str
|
371
|
-
"""
|
372
|
-
resp = self._request_http(method=AppMeshClient.Method.POST, path="/appmesh/totp/secret")
|
373
|
-
if resp.status_code == HTTPStatus.OK:
|
374
|
-
totp_uri = base64.b64decode(resp.json()["Mfa-Uri"]).decode()
|
375
|
-
return self._parse_totp_uri(totp_uri).get("secret")
|
376
|
-
raise Exception(resp.text)
|
377
|
-
|
378
|
-
def totp_setup(self, totp_code: str) -> bool:
|
379
|
-
"""Setup 2FA for current login user
|
380
|
-
|
381
|
-
Args:
|
382
|
-
totp_code (str): TOTP code
|
383
|
-
|
384
|
-
Returns:
|
385
|
-
bool: success or failure.
|
386
|
-
"""
|
387
|
-
resp = self._request_http(
|
388
|
-
method=AppMeshClient.Method.POST,
|
389
|
-
path="/appmesh/totp/setup",
|
390
|
-
header={"Totp": totp_code},
|
391
|
-
)
|
392
|
-
if resp.status_code != HTTPStatus.OK:
|
393
|
-
raise Exception(resp.text)
|
394
|
-
return resp.status_code == HTTPStatus.OK
|
395
|
-
|
396
|
-
def totp_disable(self, user="self") -> bool:
|
397
|
-
"""Disable 2FA for current user
|
398
|
-
|
399
|
-
Args:
|
400
|
-
user (str, optional): user name for disable TOTP.
|
401
|
-
|
402
|
-
Returns:
|
403
|
-
bool: success or failure.
|
404
|
-
"""
|
405
|
-
resp = self._request_http(
|
406
|
-
method=AppMeshClient.Method.POST,
|
407
|
-
path=f"/appmesh/totp/{user}/disable",
|
408
|
-
)
|
409
|
-
if resp.status_code != HTTPStatus.OK:
|
410
|
-
raise Exception(resp.text)
|
411
|
-
return resp.status_code == HTTPStatus.OK
|
412
|
-
|
413
|
-
@staticmethod
|
414
|
-
def _parse_totp_uri(totp_uri: str) -> dict:
|
415
|
-
"""Extract TOTP parameters
|
416
|
-
|
417
|
-
Args:
|
418
|
-
totp_uri (str): TOTP uri
|
419
|
-
|
420
|
-
Returns:
|
421
|
-
dict: eextract parameters
|
422
|
-
"""
|
423
|
-
parsed_info = {}
|
424
|
-
parsed_uri = parse.urlparse(totp_uri)
|
425
|
-
|
426
|
-
# Extract label from the path
|
427
|
-
parsed_info["label"] = parsed_uri.path[1:] # Remove the leading slash
|
428
|
-
|
429
|
-
# Extract parameters from the query string
|
430
|
-
query_params = parse.parse_qs(parsed_uri.query)
|
431
|
-
for key, value in query_params.items():
|
432
|
-
parsed_info[key] = value[0]
|
433
|
-
return parsed_info
|
434
|
-
|
435
|
-
########################################
|
436
|
-
# Application view
|
437
|
-
########################################
|
438
|
-
def app_view(self, app_name: str) -> App:
|
439
|
-
"""Get one application information
|
440
|
-
|
441
|
-
Args:
|
442
|
-
app_name (str): the application name.
|
443
|
-
|
444
|
-
Returns:
|
445
|
-
App: the application object both contain static configuration and runtime information.
|
446
|
-
|
447
|
-
Exception:
|
448
|
-
failed request or no such application
|
449
|
-
"""
|
450
|
-
resp = self._request_http(AppMeshClient.Method.GET, path=f"/appmesh/app/{app_name}")
|
451
|
-
if resp.status_code != HTTPStatus.OK:
|
452
|
-
raise Exception(resp.text)
|
453
|
-
return App(resp.json())
|
454
|
-
|
455
|
-
def app_view_all(self):
|
456
|
-
"""Get all applications
|
457
|
-
|
458
|
-
Returns:
|
459
|
-
list: the application object both contain static configuration and runtime information, only return applications that the user has permissions.
|
460
|
-
|
461
|
-
Exception:
|
462
|
-
failed request or no such application
|
463
|
-
"""
|
464
|
-
resp = self._request_http(AppMeshClient.Method.GET, path="/appmesh/applications")
|
465
|
-
if resp.status_code != HTTPStatus.OK:
|
466
|
-
raise Exception(resp.text)
|
467
|
-
apps = []
|
468
|
-
for app in resp.json():
|
469
|
-
apps.append(App(app))
|
470
|
-
return apps
|
471
|
-
|
472
|
-
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:
|
473
|
-
"""Get application stdout/stderr
|
474
|
-
|
475
|
-
Args:
|
476
|
-
app_name (str): the application name
|
477
|
-
stdout_position (int, optional): start read position, 0 means start from beginning.
|
478
|
-
stdout_index (int, optional): index of history process stdout, 0 means get from current running process,
|
479
|
-
the stdout number depends on 'stdout_cache_size' of the application.
|
480
|
-
stdout_maxsize (int, optional): max buffer size to read.
|
481
|
-
process_uuid (str, optional): used to get the specified process.
|
482
|
-
timeout (int, optional): wait for the running process for some time(seconds) to get the output.
|
483
|
-
|
484
|
-
Returns:
|
485
|
-
AppOutput object.
|
486
|
-
"""
|
487
|
-
resp = self._request_http(
|
488
|
-
AppMeshClient.Method.GET,
|
489
|
-
path=f"/appmesh/app/{app_name}/output",
|
490
|
-
query={
|
491
|
-
"stdout_position": str(stdout_position),
|
492
|
-
"stdout_index": str(stdout_index),
|
493
|
-
"stdout_maxsize": str(stdout_maxsize),
|
494
|
-
"process_uuid": process_uuid,
|
495
|
-
"timeout": str(timeout),
|
496
|
-
},
|
497
|
-
)
|
498
|
-
out_position = int(resp.headers["Output-Position"]) if "Output-Position" in resp.headers else None
|
499
|
-
exit_code = int(resp.headers["Exit-Code"]) if "Exit-Code" in resp.headers else None
|
500
|
-
return AppOutput(status_code=resp.status_code, output=resp.text, out_position=out_position, exit_code=exit_code)
|
501
|
-
|
502
|
-
def app_health(self, app_name: str) -> bool:
|
503
|
-
"""Get application health status
|
504
|
-
|
505
|
-
Args:
|
506
|
-
app_name (str): the application name.
|
507
|
-
|
508
|
-
Returns:
|
509
|
-
bool: healthy or not
|
510
|
-
"""
|
511
|
-
resp = self._request_http(AppMeshClient.Method.GET, path=f"/appmesh/app/{app_name}/health")
|
512
|
-
if resp.status_code != HTTPStatus.OK:
|
513
|
-
raise Exception(resp.text)
|
514
|
-
return int(resp.text) == 0
|
515
|
-
|
516
|
-
########################################
|
517
|
-
# Application manage
|
518
|
-
########################################
|
519
|
-
def app_add(self, app: App) -> App:
|
520
|
-
"""Register an application
|
521
|
-
|
522
|
-
Args:
|
523
|
-
app (App): the application definition.
|
524
|
-
|
525
|
-
Returns:
|
526
|
-
App: resigtered application object.
|
527
|
-
|
528
|
-
Exception:
|
529
|
-
failed request
|
530
|
-
"""
|
531
|
-
resp = self._request_http(AppMeshClient.Method.PUT, path=f"/appmesh/app/{app.name}", body=app.json())
|
532
|
-
if resp.status_code != HTTPStatus.OK:
|
533
|
-
raise Exception(resp.text)
|
534
|
-
return App(resp.json())
|
535
|
-
|
536
|
-
def app_delete(self, app_name: str) -> bool:
|
537
|
-
"""Remove an application.
|
538
|
-
|
539
|
-
Args:
|
540
|
-
app_name (str): the application name.
|
541
|
-
|
542
|
-
Returns:
|
543
|
-
bool: True for delete success, Flase for not exist anymore.
|
544
|
-
"""
|
545
|
-
resp = self._request_http(AppMeshClient.Method.DELETE, path=f"/appmesh/app/{app_name}")
|
546
|
-
if resp.status_code == HTTPStatus.OK:
|
547
|
-
return True
|
548
|
-
elif resp.status_code == HTTPStatus.NOT_FOUND:
|
549
|
-
return False
|
550
|
-
else:
|
551
|
-
raise Exception(resp.text)
|
552
|
-
|
553
|
-
def app_enable(self, app_name: str) -> bool:
|
554
|
-
"""Enable an application
|
555
|
-
|
556
|
-
Args:
|
557
|
-
app_name (str): the application name.
|
558
|
-
|
559
|
-
Returns:
|
560
|
-
bool: success or failure.
|
561
|
-
"""
|
562
|
-
resp = self._request_http(AppMeshClient.Method.POST, path=f"/appmesh/app/{app_name}/enable")
|
563
|
-
if resp.status_code != HTTPStatus.OK:
|
564
|
-
raise Exception(resp.text)
|
565
|
-
return resp.status_code == HTTPStatus.OK
|
566
|
-
|
567
|
-
def app_disable(self, app_name: str) -> bool:
|
568
|
-
"""Stop and disable an application
|
569
|
-
|
570
|
-
Args:
|
571
|
-
app_name (str): the application name.
|
572
|
-
|
573
|
-
Returns:
|
574
|
-
bool: success or failure.
|
575
|
-
"""
|
576
|
-
resp = self._request_http(AppMeshClient.Method.POST, path=f"/appmesh/app/{app_name}/disable")
|
577
|
-
if resp.status_code != HTTPStatus.OK:
|
578
|
-
raise Exception(resp.text)
|
579
|
-
return resp.status_code == HTTPStatus.OK
|
580
|
-
|
581
|
-
########################################
|
582
|
-
# Cloud management
|
583
|
-
########################################
|
584
|
-
def cloud_app_view_all(self) -> dict:
|
585
|
-
"""Get all cloud applications
|
586
|
-
|
587
|
-
Returns:
|
588
|
-
dict: cloud applications in JSON format.
|
589
|
-
"""
|
590
|
-
resp = self._request_http(AppMeshClient.Method.GET, path="/appmesh/cloud/applications")
|
591
|
-
if resp.status_code != HTTPStatus.OK:
|
592
|
-
raise Exception(resp.text)
|
593
|
-
return resp.json()
|
594
|
-
|
595
|
-
def cloud_app(self, app_name: str) -> dict:
|
596
|
-
"""Get an cloud application
|
597
|
-
|
598
|
-
Args:
|
599
|
-
app_name (str): the application name.
|
600
|
-
|
601
|
-
Returns:
|
602
|
-
dict: application in JSON format.
|
603
|
-
"""
|
604
|
-
resp = self._request_http(AppMeshClient.Method.GET, path=f"/appmesh/cloud/app/{app_name}")
|
605
|
-
if resp.status_code != HTTPStatus.OK:
|
606
|
-
raise Exception(resp.text)
|
607
|
-
return resp.json()
|
608
|
-
|
609
|
-
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 = ""):
|
610
|
-
"""Get cloud application stdout/stderr from master agent
|
611
|
-
|
612
|
-
Args:
|
613
|
-
app_name (str): the application name
|
614
|
-
host_name (str): the target host name where the application is running
|
615
|
-
stdout_position (int, optional): start read position, 0 means start from beginning.
|
616
|
-
stdout_index (int, optional): index of history process stdout, 0 means get from current running process,
|
617
|
-
the stdout number depends on 'stdout_cache_size' of the application.
|
618
|
-
stdout_maxsize (int, optional): max buffer size to read.
|
619
|
-
process_uuid (str, optional): used to get the specified process.
|
620
|
-
|
621
|
-
Returns:
|
622
|
-
bool: success or failure.
|
623
|
-
str: output string.
|
624
|
-
int or None: current read position.
|
625
|
-
int or None: process exit code.
|
626
|
-
"""
|
627
|
-
resp = self._request_http(
|
628
|
-
AppMeshClient.Method.GET,
|
629
|
-
path=f"/appmesh/cloud/app/{app_name}/output/{host_name}",
|
630
|
-
query={
|
631
|
-
"stdout_position": str(stdout_position),
|
632
|
-
"stdout_index": str(stdout_index),
|
633
|
-
"stdout_maxsize": str(stdout_maxsize),
|
634
|
-
"process_uuid": process_uuid,
|
635
|
-
},
|
636
|
-
)
|
637
|
-
out_position = int(resp.headers["Output-Position"]) if "Output-Position" in resp.headers else None
|
638
|
-
exit_code = int(resp.headers["Exit-Code"]) if "Exit-Code" in resp.headers else None
|
639
|
-
return (resp.status_code == HTTPStatus.OK), resp.text, out_position, exit_code
|
640
|
-
|
641
|
-
def cloud_app_delete(self, app_name: str) -> bool:
|
642
|
-
"""Delete a cloud application
|
643
|
-
|
644
|
-
Args:
|
645
|
-
app_name (str): The application name for cloud
|
646
|
-
|
647
|
-
Returns:
|
648
|
-
bool: success or failure.
|
649
|
-
"""
|
650
|
-
resp = self._request_http(AppMeshClient.Method.DELETE, path=f"/appmesh/cloud/app/{app_name}")
|
651
|
-
if resp.status_code != HTTPStatus.OK:
|
652
|
-
raise Exception(resp.text)
|
653
|
-
return resp.status_code == HTTPStatus.OK
|
654
|
-
|
655
|
-
def cloud_app_add(self, app_json: dict) -> dict:
|
656
|
-
"""Add a cloud application
|
657
|
-
|
658
|
-
Args:
|
659
|
-
app_json (dict): the cloud application definition with replication, condition and resource requirement
|
660
|
-
|
661
|
-
Returns:
|
662
|
-
dict: cluster application json.
|
663
|
-
"""
|
664
|
-
resp = self._request_http(AppMeshClient.Method.PUT, path=f"/appmesh/cloud/app/{app_json['content']['name']}", body=app_json)
|
665
|
-
if resp.status_code != HTTPStatus.OK:
|
666
|
-
raise Exception(resp.text)
|
667
|
-
return resp.json()
|
668
|
-
|
669
|
-
def cloud_nodes(self) -> dict:
|
670
|
-
"""Get cluster node list
|
671
|
-
|
672
|
-
Returns:
|
673
|
-
dict: cluster node list json.
|
674
|
-
"""
|
675
|
-
resp = self._request_http(AppMeshClient.Method.GET, path="/appmesh/cloud/nodes")
|
676
|
-
if resp.status_code != HTTPStatus.OK:
|
677
|
-
raise Exception(resp.text)
|
678
|
-
return resp.json()
|
679
|
-
|
680
|
-
########################################
|
681
|
-
# Configuration
|
682
|
-
########################################
|
683
|
-
def host_resource(self) -> dict:
|
684
|
-
"""Get App Mesh host resource report include CPU, memory and disk
|
685
|
-
|
686
|
-
Returns:
|
687
|
-
dict: the host resource json.
|
688
|
-
"""
|
689
|
-
resp = self._request_http(AppMeshClient.Method.GET, path="/appmesh/resources")
|
690
|
-
if resp.status_code != HTTPStatus.OK:
|
691
|
-
raise Exception(resp.text)
|
692
|
-
return resp.json()
|
693
|
-
|
694
|
-
def config_view(self) -> dict:
|
695
|
-
"""Get App Mesh configuration JSON
|
696
|
-
|
697
|
-
Returns:
|
698
|
-
dict: the configuration json.
|
699
|
-
"""
|
700
|
-
resp = self._request_http(AppMeshClient.Method.GET, path="/appmesh/config")
|
701
|
-
if resp.status_code != HTTPStatus.OK:
|
702
|
-
raise Exception(resp.text)
|
703
|
-
return resp.json()
|
704
|
-
|
705
|
-
def config_set(self, cfg_json) -> dict:
|
706
|
-
"""Update configuration, the format follow 'config.yaml', support partial update
|
707
|
-
|
708
|
-
Args:
|
709
|
-
cfg_json (dict): the new configuration json.
|
710
|
-
|
711
|
-
Returns:
|
712
|
-
dict: the updated configuration json.
|
713
|
-
"""
|
714
|
-
resp = self._request_http(AppMeshClient.Method.POST, path="/appmesh/config", body=cfg_json)
|
715
|
-
if resp.status_code != HTTPStatus.OK:
|
716
|
-
raise Exception(resp.text)
|
717
|
-
return resp.json()
|
718
|
-
|
719
|
-
def log_level_set(self, level: str = "DEBUG") -> str:
|
720
|
-
"""Update App Mesh log level(DEBUG/INFO/NOTICE/WARN/ERROR), a wrapper of config_set()
|
721
|
-
|
722
|
-
Args:
|
723
|
-
level (str, optional): log level.
|
724
|
-
|
725
|
-
Returns:
|
726
|
-
str: the updated log level.
|
727
|
-
"""
|
728
|
-
resp = self._request_http(AppMeshClient.Method.POST, path="/appmesh/config", body={"BaseConfig": {"LogLevel": level}})
|
729
|
-
if resp.status_code != HTTPStatus.OK:
|
730
|
-
raise Exception(resp.text)
|
731
|
-
return resp.json()["BaseConfig"]["LogLevel"]
|
732
|
-
|
733
|
-
########################################
|
734
|
-
# User Management
|
735
|
-
########################################
|
736
|
-
def user_passwd_update(self, new_password: str, user_name: str = "self") -> bool:
|
737
|
-
"""Change user password
|
738
|
-
|
739
|
-
Args:
|
740
|
-
user_name (str): the user name.
|
741
|
-
new_password (str):the new password string
|
742
|
-
|
743
|
-
Returns:
|
744
|
-
bool: success
|
745
|
-
"""
|
746
|
-
resp = self._request_http(
|
747
|
-
method=AppMeshClient.Method.POST,
|
748
|
-
path=f"/appmesh/user/{user_name}/passwd",
|
749
|
-
header={"New-Password": base64.b64encode(new_password.encode())},
|
750
|
-
)
|
751
|
-
if resp.status_code != HTTPStatus.OK:
|
752
|
-
raise Exception(resp.text)
|
753
|
-
return True
|
754
|
-
|
755
|
-
def user_add(self, user_name: str, user_json: dict) -> bool:
|
756
|
-
"""Add a new user, not available for LDAP user
|
757
|
-
|
758
|
-
Args:
|
759
|
-
user_name (str): the user name.
|
760
|
-
user_json (dict): user definition, follow same user format from security.yaml.
|
761
|
-
|
762
|
-
Returns:
|
763
|
-
bool: success or failure.
|
764
|
-
"""
|
765
|
-
resp = self._request_http(
|
766
|
-
method=AppMeshClient.Method.PUT,
|
767
|
-
path=f"/appmesh/user/{user_name}",
|
768
|
-
body=user_json,
|
769
|
-
)
|
770
|
-
return resp.status_code == HTTPStatus.OK
|
771
|
-
|
772
|
-
def user_delete(self, user_name: str) -> bool:
|
773
|
-
"""Delete a user
|
774
|
-
|
775
|
-
Args:
|
776
|
-
user_name (str): the user name.
|
777
|
-
|
778
|
-
Returns:
|
779
|
-
bool: success or failure.
|
780
|
-
"""
|
781
|
-
resp = self._request_http(
|
782
|
-
method=AppMeshClient.Method.DELETE,
|
783
|
-
path=f"/appmesh/user/{user_name}",
|
784
|
-
)
|
785
|
-
return resp.status_code == HTTPStatus.OK
|
786
|
-
|
787
|
-
def user_lock(self, user_name: str) -> bool:
|
788
|
-
"""Lock a user
|
789
|
-
|
790
|
-
Args:
|
791
|
-
user_name (str): the user name.
|
792
|
-
|
793
|
-
Returns:
|
794
|
-
bool: success or failure.
|
795
|
-
"""
|
796
|
-
resp = self._request_http(
|
797
|
-
method=AppMeshClient.Method.POST,
|
798
|
-
path=f"/appmesh/user/{user_name}/lock",
|
799
|
-
)
|
800
|
-
if resp.status_code != HTTPStatus.OK:
|
801
|
-
raise Exception(resp.text)
|
802
|
-
return resp.status_code == HTTPStatus.OK
|
803
|
-
|
804
|
-
def user_unlock(self, user_name: str) -> bool:
|
805
|
-
"""Unlock a user
|
806
|
-
|
807
|
-
Args:
|
808
|
-
user_name (str): the user name.
|
809
|
-
|
810
|
-
Returns:
|
811
|
-
bool: success or failure.
|
812
|
-
"""
|
813
|
-
resp = self._request_http(
|
814
|
-
method=AppMeshClient.Method.POST,
|
815
|
-
path=f"/appmesh/user/{user_name}/unlock",
|
816
|
-
)
|
817
|
-
if resp.status_code != HTTPStatus.OK:
|
818
|
-
raise Exception(resp.text)
|
819
|
-
return resp.status_code == HTTPStatus.OK
|
820
|
-
|
821
|
-
def users_view(self) -> dict:
|
822
|
-
"""Get all users
|
823
|
-
|
824
|
-
Returns:
|
825
|
-
dict: all user definition
|
826
|
-
"""
|
827
|
-
resp = self._request_http(method=AppMeshClient.Method.GET, path="/appmesh/users")
|
828
|
-
if resp.status_code != HTTPStatus.OK:
|
829
|
-
raise Exception(resp.text)
|
830
|
-
return resp.json()
|
831
|
-
|
832
|
-
def user_self(self) -> dict:
|
833
|
-
"""Get current user infomation
|
834
|
-
|
835
|
-
Returns:
|
836
|
-
dict: user definition.
|
837
|
-
"""
|
838
|
-
resp = self._request_http(method=AppMeshClient.Method.GET, path="/appmesh/user/self")
|
839
|
-
if resp.status_code != HTTPStatus.OK:
|
840
|
-
raise Exception(resp.text)
|
841
|
-
return resp.json()
|
842
|
-
|
843
|
-
def groups_view(self) -> list:
|
844
|
-
"""Get all user groups
|
845
|
-
|
846
|
-
Returns:
|
847
|
-
dict: user group array.
|
848
|
-
"""
|
849
|
-
resp = self._request_http(method=AppMeshClient.Method.GET, path="/appmesh/user/groups")
|
850
|
-
if resp.status_code != HTTPStatus.OK:
|
851
|
-
raise Exception(resp.text)
|
852
|
-
return resp.json()
|
853
|
-
|
854
|
-
def permissions_view(self) -> list:
|
855
|
-
"""Get all available permissions
|
856
|
-
|
857
|
-
Returns:
|
858
|
-
dict: permission array
|
859
|
-
"""
|
860
|
-
resp = self._request_http(method=AppMeshClient.Method.GET, path="/appmesh/permissions")
|
861
|
-
if resp.status_code != HTTPStatus.OK:
|
862
|
-
raise Exception(resp.text)
|
863
|
-
return resp.json()
|
864
|
-
|
865
|
-
def permissions_for_user(self) -> list:
|
866
|
-
"""Get current user permissions
|
867
|
-
|
868
|
-
Returns:
|
869
|
-
dict: user permission array.
|
870
|
-
"""
|
871
|
-
resp = self._request_http(method=AppMeshClient.Method.GET, path="/appmesh/user/permissions")
|
872
|
-
if resp.status_code != HTTPStatus.OK:
|
873
|
-
raise Exception(resp.text)
|
874
|
-
return resp.json()
|
875
|
-
|
876
|
-
def roles_view(self) -> list:
|
877
|
-
"""Get all roles with permission definition
|
878
|
-
|
879
|
-
Returns:
|
880
|
-
dict: all role definition.
|
881
|
-
"""
|
882
|
-
resp = self._request_http(method=AppMeshClient.Method.GET, path="/appmesh/roles")
|
883
|
-
if resp.status_code != HTTPStatus.OK:
|
884
|
-
raise Exception(resp.text)
|
885
|
-
return resp.json()
|
886
|
-
|
887
|
-
def role_update(self, role_name: str, role_permission_json: dict) -> bool:
|
888
|
-
"""Update (or add) a role with defined permissions, the permission ID can be App Mesh pre-defined or other permission ID.
|
889
|
-
|
890
|
-
Args:
|
891
|
-
role_name (str): the role name.
|
892
|
-
role_permission_json (dict): role permission definition array, e.g: ["app-control", "app-delete", "cloud-app-reg", "cloud-app-delete"]
|
893
|
-
|
894
|
-
Returns:
|
895
|
-
bool: success or failure.
|
896
|
-
"""
|
897
|
-
resp = self._request_http(method=AppMeshClient.Method.POST, path=f"/appmesh/role/{role_name}", body=role_permission_json)
|
898
|
-
if resp.status_code != HTTPStatus.OK:
|
899
|
-
raise Exception(resp.text)
|
900
|
-
return resp.status_code == HTTPStatus.OK
|
901
|
-
|
902
|
-
def role_delete(self, role_name: str) -> bool:
|
903
|
-
"""Delete a user role
|
904
|
-
|
905
|
-
Args:
|
906
|
-
role_name (str): the role name.
|
907
|
-
|
908
|
-
Returns:
|
909
|
-
bool: success or failure.
|
910
|
-
"""
|
911
|
-
resp = self._request_http(
|
912
|
-
method=AppMeshClient.Method.DELETE,
|
913
|
-
path=f"/appmesh/role/{role_name}",
|
914
|
-
)
|
915
|
-
if resp.status_code != HTTPStatus.OK:
|
916
|
-
raise Exception(resp.text)
|
917
|
-
return resp.status_code == HTTPStatus.OK
|
918
|
-
|
919
|
-
########################################
|
920
|
-
# Tag management
|
921
|
-
########################################
|
922
|
-
def tag_add(self, tag_name: str, tag_value: str) -> bool:
|
923
|
-
"""Add a new label
|
924
|
-
|
925
|
-
Args:
|
926
|
-
tag_name (str): the label name.
|
927
|
-
tag_value (str): the label value.
|
928
|
-
|
929
|
-
Returns:
|
930
|
-
bool: success or failure.
|
931
|
-
"""
|
932
|
-
resp = self._request_http(
|
933
|
-
AppMeshClient.Method.PUT,
|
934
|
-
query={"value": tag_value},
|
935
|
-
path=f"/appmesh/label/{tag_name}",
|
936
|
-
)
|
937
|
-
if resp.status_code != HTTPStatus.OK:
|
938
|
-
raise Exception(resp.text)
|
939
|
-
return resp.status_code == HTTPStatus.OK
|
940
|
-
|
941
|
-
def tag_delete(self, tag_name: str) -> bool:
|
942
|
-
"""Delete a label
|
943
|
-
|
944
|
-
Args:
|
945
|
-
tag_name (str): the label name.
|
946
|
-
|
947
|
-
Returns:
|
948
|
-
bool: success or failure.
|
949
|
-
"""
|
950
|
-
resp = self._request_http(AppMeshClient.Method.DELETE, path=f"/appmesh/label/{tag_name}")
|
951
|
-
if resp.status_code != HTTPStatus.OK:
|
952
|
-
raise Exception(resp.text)
|
953
|
-
return resp.status_code == HTTPStatus.OK
|
954
|
-
|
955
|
-
def tag_view(self) -> dict:
|
956
|
-
"""Get the server labels
|
957
|
-
|
958
|
-
Returns:
|
959
|
-
dict: label data.
|
960
|
-
"""
|
961
|
-
resp = self._request_http(AppMeshClient.Method.GET, path="/appmesh/labels")
|
962
|
-
if resp.status_code != HTTPStatus.OK:
|
963
|
-
raise Exception(resp.text)
|
964
|
-
return resp.json()
|
965
|
-
|
966
|
-
########################################
|
967
|
-
# Promethus metrics
|
968
|
-
########################################
|
969
|
-
def metrics(self):
|
970
|
-
"""Prometheus metrics (this does not call Prometheus API /metrics, just copy the same metrics data)
|
971
|
-
|
972
|
-
Returns:
|
973
|
-
str: prometheus metrics texts
|
974
|
-
"""
|
975
|
-
resp = self._request_http(AppMeshClient.Method.GET, path="/appmesh/metrics")
|
976
|
-
if resp.status_code != HTTPStatus.OK:
|
977
|
-
raise Exception(resp.text)
|
978
|
-
return resp.text
|
979
|
-
|
980
|
-
########################################
|
981
|
-
# File management
|
982
|
-
########################################
|
983
|
-
def file_download(self, remote_file: str, local_file: str, apply_file_attributes: bool = True) -> None:
|
984
|
-
"""Copy a remote file to local. Optionally, the local file will have the same permission as the remote file.
|
985
|
-
|
986
|
-
Args:
|
987
|
-
remote_file (str): the remote file path.
|
988
|
-
local_file (str): the local file path to be downloaded.
|
989
|
-
apply_file_attributes (bool): whether to apply file attributes (permissions, owner, group) to the local file.
|
990
|
-
"""
|
991
|
-
resp = self._request_http(AppMeshClient.Method.GET, path="/appmesh/file/download", header={"File-Path": remote_file})
|
992
|
-
resp.raise_for_status()
|
993
|
-
|
994
|
-
# Write the file content locally
|
995
|
-
with open(local_file, "wb") as fp:
|
996
|
-
for chunk in resp.iter_content(chunk_size=8 * 1024): # 8 KB
|
997
|
-
if chunk:
|
998
|
-
fp.write(chunk)
|
999
|
-
|
1000
|
-
# Apply file attributes (permissions, owner, group) if requested
|
1001
|
-
if apply_file_attributes:
|
1002
|
-
if "File-Mode" in resp.headers:
|
1003
|
-
os.chmod(path=local_file, mode=int(resp.headers["File-Mode"]))
|
1004
|
-
if "File-User" in resp.headers and "File-Group" in resp.headers:
|
1005
|
-
file_uid = int(resp.headers["File-User"])
|
1006
|
-
file_gid = int(resp.headers["File-Group"])
|
1007
|
-
try:
|
1008
|
-
os.chown(path=local_file, uid=file_uid, gid=file_gid)
|
1009
|
-
except PermissionError:
|
1010
|
-
print(f"Warning: Unable to change owner/group of {local_file}. Operation requires elevated privileges.")
|
1011
|
-
|
1012
|
-
def file_upload(self, local_file: str, remote_file: str, apply_file_attributes: bool = True) -> None:
|
1013
|
-
"""Upload a local file to the remote server. Optionally, the remote file will have the same permission as the local file.
|
1014
|
-
|
1015
|
-
Dependency:
|
1016
|
-
sudo apt install python3-pip
|
1017
|
-
pip3 install requests_toolbelt
|
1018
|
-
|
1019
|
-
Args:
|
1020
|
-
local_file (str): the local file path.
|
1021
|
-
remote_file (str): the target remote file to be uploaded.
|
1022
|
-
apply_file_attributes (bool): whether to upload file attributes (permissions, owner, group) along with the file.
|
1023
|
-
"""
|
1024
|
-
if not os.path.exists(local_file):
|
1025
|
-
raise FileNotFoundError(f"Local file not found: {local_file}")
|
1026
|
-
|
1027
|
-
from requests_toolbelt import MultipartEncoder
|
1028
|
-
|
1029
|
-
with open(file=local_file, mode="rb") as fp:
|
1030
|
-
encoder = MultipartEncoder(fields={"filename": os.path.basename(remote_file), "file": ("filename", fp, "application/octet-stream")})
|
1031
|
-
header = {"File-Path": remote_file, "Content-Type": encoder.content_type}
|
1032
|
-
|
1033
|
-
# Include file attributes (permissions, owner, group) if requested
|
1034
|
-
if apply_file_attributes:
|
1035
|
-
file_stat = os.stat(local_file)
|
1036
|
-
header["File-Mode"] = str(file_stat.st_mode & 0o777) # Mask to keep only permission bits
|
1037
|
-
header["File-User"] = str(file_stat.st_uid)
|
1038
|
-
header["File-Group"] = str(file_stat.st_gid)
|
1039
|
-
|
1040
|
-
# Upload file with or without attributes
|
1041
|
-
# https://stackoverflow.com/questions/22567306/python-requests-file-upload
|
1042
|
-
resp = self._request_http(
|
1043
|
-
AppMeshClient.Method.POST_STREAM,
|
1044
|
-
path="/appmesh/file/upload",
|
1045
|
-
header=header,
|
1046
|
-
body=encoder,
|
1047
|
-
)
|
1048
|
-
resp.raise_for_status()
|
1049
|
-
|
1050
|
-
########################################
|
1051
|
-
# Application run
|
1052
|
-
########################################
|
1053
|
-
def _parse_duration(self, timeout) -> str:
|
1054
|
-
if isinstance(timeout, int):
|
1055
|
-
return str(timeout)
|
1056
|
-
elif isinstance(timeout, str):
|
1057
|
-
return str(int(aniso8601.parse_duration(timeout).total_seconds()))
|
1058
|
-
else:
|
1059
|
-
raise TypeError(f"Invalid timeout type: {str(timeout)}")
|
1060
|
-
|
1061
|
-
def run_async(
|
1062
|
-
self,
|
1063
|
-
app: Union[App, str],
|
1064
|
-
max_time_seconds: Union[int, str] = DURATION_TWO_DAYS_ISO,
|
1065
|
-
life_cycle_seconds: Union[int, str] = DURATION_TWO_DAYS_HALF_ISO,
|
1066
|
-
) -> AppRun:
|
1067
|
-
"""Run an application asynchronously on a remote system without blocking the API.
|
1068
|
-
|
1069
|
-
Args:
|
1070
|
-
app (Union[App, str]): An `App` instance or a shell command string.
|
1071
|
-
- If `app` is a string, it is treated as a shell command for the remote run,
|
1072
|
-
and an `App` instance is created as:
|
1073
|
-
`App({"command": "<command_string>", "shell": True})`.
|
1074
|
-
- If `app` is an `App` object, providing only the `name` attribute (without
|
1075
|
-
a command) will run an existing application; otherwise, it is treated as a new application.
|
1076
|
-
max_time_seconds (Union[int, str], optional): Maximum runtime for the remote process.
|
1077
|
-
Accepts ISO 8601 duration format (e.g., 'P1Y2M3DT4H5M6S', 'P5W'). Defaults to `P2D`.
|
1078
|
-
life_cycle_seconds (Union[int, str], optional): Maximum lifecycle time for the remote process.
|
1079
|
-
Accepts ISO 8601 duration format. Defaults to `P2DT12H`.
|
1080
|
-
|
1081
|
-
Returns:
|
1082
|
-
AppRun: An application run object that can be used to monitor and retrieve the result of the run.
|
1083
|
-
"""
|
1084
|
-
if isinstance(app, str):
|
1085
|
-
app = App({"command": app, "shell": True})
|
1086
|
-
|
1087
|
-
path = "/appmesh/app/run"
|
1088
|
-
resp = self._request_http(
|
1089
|
-
AppMeshClient.Method.POST,
|
1090
|
-
body=app.json(),
|
1091
|
-
path=path,
|
1092
|
-
query={
|
1093
|
-
"timeout": self._parse_duration(max_time_seconds),
|
1094
|
-
"lifecycle": self._parse_duration(life_cycle_seconds),
|
1095
|
-
},
|
1096
|
-
)
|
1097
|
-
if resp.status_code != HTTPStatus.OK:
|
1098
|
-
raise Exception(resp.text)
|
1099
|
-
|
1100
|
-
# Return an AppRun object with the application name and process UUID
|
1101
|
-
return AppRun(self, resp.json()["name"], resp.json()["process_uuid"])
|
1102
|
-
|
1103
|
-
def run_async_wait(self, run: AppRun, stdout_print: bool = True, timeout: int = 0) -> int:
|
1104
|
-
"""Wait for an async run to be finished
|
1105
|
-
|
1106
|
-
Args:
|
1107
|
-
run (AppRun): asyncrized run result from run_async().
|
1108
|
-
stdout_print (bool, optional): print remote stdout to local or not.
|
1109
|
-
timeout (int, optional): wait max timeout seconds and return if not finished, 0 means wait until finished
|
1110
|
-
|
1111
|
-
Returns:
|
1112
|
-
int: return exit code if process finished, return None for timeout or exception.
|
1113
|
-
"""
|
1114
|
-
if run:
|
1115
|
-
last_output_position = 0
|
1116
|
-
start = datetime.now()
|
1117
|
-
interval = 1 if self.__class__.__name__ == "AppMeshClient" else 1000
|
1118
|
-
while len(run.proc_uid) > 0:
|
1119
|
-
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)
|
1120
|
-
if app_out.output and stdout_print:
|
1121
|
-
print(app_out.output, end="")
|
1122
|
-
if app_out.out_position is not None:
|
1123
|
-
last_output_position = app_out.out_position
|
1124
|
-
if app_out.exit_code is not None:
|
1125
|
-
# success
|
1126
|
-
self.app_delete(run.app_name)
|
1127
|
-
return app_out.exit_code
|
1128
|
-
if app_out.status_code != HTTPStatus.OK:
|
1129
|
-
# failed
|
1130
|
-
break
|
1131
|
-
if timeout > 0 and (datetime.now() - start).seconds > timeout:
|
1132
|
-
# timeout
|
1133
|
-
break
|
1134
|
-
return None
|
1135
|
-
|
1136
|
-
def run_sync(
|
1137
|
-
self,
|
1138
|
-
app: Union[App, str],
|
1139
|
-
stdout_print: bool = True,
|
1140
|
-
max_time_seconds: Union[int, str] = DURATION_TWO_DAYS_ISO,
|
1141
|
-
life_cycle_seconds: Union[int, str] = DURATION_TWO_DAYS_HALF_ISO,
|
1142
|
-
) -> Tuple[Union[int, None], str]:
|
1143
|
-
"""Synchronously run an application remotely, blocking until completion, and return the result.
|
1144
|
-
|
1145
|
-
If 'app' is a string, it is treated as a shell command and converted to an App instance.
|
1146
|
-
If 'app' is App object, the name attribute is used to run an existing application if specified.
|
1147
|
-
|
1148
|
-
Args:
|
1149
|
-
app (Union[App, str]): An App instance or a shell command string.
|
1150
|
-
If a string, an App instance is created as:
|
1151
|
-
`appmesh_client.App({"command": "<command_string>", "shell": True})`
|
1152
|
-
stdout_print (bool, optional): If True, prints the remote stdout locally. Defaults to True.
|
1153
|
-
max_time_seconds (Union[int, str], optional): Maximum runtime for the remote process.
|
1154
|
-
Supports ISO 8601 duration format (e.g., 'P1Y2M3DT4H5M6S', 'P5W'). Defaults to DEFAULT_RUN_APP_TIMEOUT_SECONDS.
|
1155
|
-
life_cycle_seconds (Union[int, str], optional): Maximum lifecycle time for the remote process.
|
1156
|
-
Supports ISO 8601 duration format. Defaults to DEFAULT_RUN_APP_LIFECYCLE_SECONDS.
|
1157
|
-
|
1158
|
-
Returns:
|
1159
|
-
Tuple[Union[int, None], str]: Exit code of the process (None if unavailable) and the stdout text.
|
1160
|
-
"""
|
1161
|
-
if isinstance(app, str):
|
1162
|
-
app = App({"command": app, "shell": True})
|
1163
|
-
|
1164
|
-
path = "/appmesh/app/syncrun"
|
1165
|
-
resp = self._request_http(
|
1166
|
-
AppMeshClient.Method.POST,
|
1167
|
-
body=app.json(),
|
1168
|
-
path=path,
|
1169
|
-
query={
|
1170
|
-
"timeout": self._parse_duration(max_time_seconds),
|
1171
|
-
"lifecycle": self._parse_duration(life_cycle_seconds),
|
1172
|
-
},
|
1173
|
-
)
|
1174
|
-
exit_code = None
|
1175
|
-
if resp.status_code == HTTPStatus.OK:
|
1176
|
-
if stdout_print:
|
1177
|
-
print(resp.text, end="")
|
1178
|
-
if "Exit-Code" in resp.headers:
|
1179
|
-
exit_code = int(resp.headers.get("Exit-Code"))
|
1180
|
-
elif stdout_print:
|
1181
|
-
print(resp.text)
|
1182
|
-
|
1183
|
-
return exit_code, resp.text
|
1184
|
-
|
1185
|
-
def _request_http(self, method: Method, path: str, query: dict = None, header: dict = None, body=None) -> requests.Response:
|
1186
|
-
"""REST API
|
1187
|
-
|
1188
|
-
Args:
|
1189
|
-
method (Method): AppMeshClient.Method.
|
1190
|
-
path (str): URI patch str.
|
1191
|
-
query (dict, optional): HTTP query parameters.
|
1192
|
-
header (dict, optional): HTTP headers.
|
1193
|
-
body (_type_, optional): object to send in the body of the :class:`Request`.
|
1194
|
-
|
1195
|
-
Returns:
|
1196
|
-
requests.Response: HTTP response
|
1197
|
-
"""
|
1198
|
-
rest_url = parse.urljoin(self.server_url, path)
|
1199
|
-
|
1200
|
-
header = {} if header is None else header
|
1201
|
-
if self.jwt_token:
|
1202
|
-
header["Authorization"] = "Bearer " + self.jwt_token
|
1203
|
-
if self.forwarding_host and len(self.forwarding_host) > 0:
|
1204
|
-
if ":" in self.forwarding_host:
|
1205
|
-
header[self.HTTP_HEADER_KEY_X_TARGET_HOST] = self.forwarding_host
|
1206
|
-
else:
|
1207
|
-
header[self.HTTP_HEADER_KEY_X_TARGET_HOST] = self.forwarding_host + ":" + str(parse.urlsplit(self.server_url).port)
|
1208
|
-
header[self.HTTP_HEADER_KEY_USER_AGENT] = self.HTTP_USER_AGENT
|
1209
|
-
|
1210
|
-
if method is AppMeshClient.Method.GET:
|
1211
|
-
return requests.get(url=rest_url, params=query, headers=header, cert=self.ssl_client_cert, verify=self.ssl_verify, timeout=self.rest_timeout)
|
1212
|
-
elif method is AppMeshClient.Method.POST:
|
1213
|
-
return requests.post(
|
1214
|
-
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
|
1215
|
-
)
|
1216
|
-
elif method is AppMeshClient.Method.POST_STREAM:
|
1217
|
-
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)
|
1218
|
-
elif method is AppMeshClient.Method.DELETE:
|
1219
|
-
return requests.delete(url=rest_url, headers=header, cert=self.ssl_client_cert, verify=self.ssl_verify, timeout=self.rest_timeout)
|
1220
|
-
elif method is AppMeshClient.Method.PUT:
|
1221
|
-
return requests.put(url=rest_url, params=query, headers=header, json=body, cert=self.ssl_client_cert, verify=self.ssl_verify, timeout=self.rest_timeout)
|
1222
|
-
else:
|
1223
|
-
raise Exception("Invalid http method", method)
|
8
|
+
from .http_client import AppMeshClient, App, AppOutput
|