appmesh 1.6.13__py3-none-any.whl → 1.6.15__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/app.py +28 -18
- appmesh/client_http.py +434 -676
- appmesh/client_http_oauth.py +2 -2
- appmesh/client_tcp.py +158 -111
- appmesh/server_http.py +20 -23
- appmesh/server_tcp.py +4 -8
- appmesh/tcp_transport.py +0 -2
- {appmesh-1.6.13.dist-info → appmesh-1.6.15.dist-info}/METADATA +1 -1
- appmesh-1.6.15.dist-info/RECORD +16 -0
- appmesh-1.6.13.dist-info/RECORD +0 -16
- {appmesh-1.6.13.dist-info → appmesh-1.6.15.dist-info}/WHEEL +0 -0
- {appmesh-1.6.13.dist-info → appmesh-1.6.15.dist-info}/top_level.txt +0 -0
appmesh/client_http.py
CHANGED
@@ -1,6 +1,8 @@
|
|
1
1
|
# client_http.py
|
2
2
|
# pylint: disable=broad-exception-raised,line-too-long,broad-exception-caught,too-many-lines,import-outside-toplevel
|
3
3
|
|
4
|
+
"""App Mesh HTTP Client SDK for REST API interactions."""
|
5
|
+
|
4
6
|
# Standard library imports
|
5
7
|
import abc
|
6
8
|
import base64
|
@@ -12,9 +14,11 @@ import os
|
|
12
14
|
import sys
|
13
15
|
import threading
|
14
16
|
import time
|
17
|
+
from contextlib import suppress
|
15
18
|
from datetime import datetime
|
16
19
|
from enum import Enum, unique
|
17
20
|
from http import HTTPStatus
|
21
|
+
from pathlib import Path
|
18
22
|
from typing import Optional, Tuple, Union
|
19
23
|
from urllib import parse
|
20
24
|
|
@@ -39,17 +43,6 @@ class AppMeshClient(metaclass=abc.ABCMeta):
|
|
39
43
|
|
40
44
|
This client is designed for direct usage in applications that require access to App Mesh services over HTTP-based REST.
|
41
45
|
|
42
|
-
Usage:
|
43
|
-
- Install the App Mesh Python package:
|
44
|
-
python3 -m pip install --upgrade appmesh
|
45
|
-
- Import the client module:
|
46
|
-
from appmesh import AppMeshClient
|
47
|
-
|
48
|
-
Example:
|
49
|
-
client = AppMeshClient()
|
50
|
-
client.login("your-name", "your-password")
|
51
|
-
response = client.app_view(app_name='ping')
|
52
|
-
|
53
46
|
Attributes:
|
54
47
|
- TLS (Transport Layer Security): Supports secure connections between the client and App Mesh service,
|
55
48
|
ensuring encrypted communication.
|
@@ -112,8 +105,16 @@ class AppMeshClient(metaclass=abc.ABCMeta):
|
|
112
105
|
- update_role()
|
113
106
|
- view_roles()
|
114
107
|
- view_groups()
|
108
|
+
|
109
|
+
Example:
|
110
|
+
>>> python -m pip install --upgrade appmesh
|
111
|
+
>>> from appmesh import AppMeshClient
|
112
|
+
>>> client = AppMeshClient()
|
113
|
+
>>> client.login("your-name", "your-password")
|
114
|
+
>>> response = client.app_view(app_name='ping')
|
115
115
|
"""
|
116
116
|
|
117
|
+
# Duration constants
|
117
118
|
DURATION_ONE_WEEK_ISO = "P1W"
|
118
119
|
DURATION_TWO_DAYS_ISO = "P2D"
|
119
120
|
DURATION_TWO_DAYS_HALF_ISO = "P2DT12H"
|
@@ -121,20 +122,22 @@ class AppMeshClient(metaclass=abc.ABCMeta):
|
|
121
122
|
TOKEN_REFRESH_OFFSET = 30 # 30s before token expire to refresh token
|
122
123
|
|
123
124
|
# Platform-aware default SSL paths
|
124
|
-
_DEFAULT_SSL_DIR = "c:/local/appmesh/ssl" if os.name == "nt" else "/opt/appmesh/ssl"
|
125
|
-
DEFAULT_SSL_CA_CERT_PATH =
|
126
|
-
DEFAULT_SSL_CLIENT_CERT_PATH =
|
127
|
-
DEFAULT_SSL_CLIENT_KEY_PATH =
|
125
|
+
_DEFAULT_SSL_DIR = Path("c:/local/appmesh/ssl" if os.name == "nt" else "/opt/appmesh/ssl")
|
126
|
+
DEFAULT_SSL_CA_CERT_PATH = str(_DEFAULT_SSL_DIR / "ca.pem")
|
127
|
+
DEFAULT_SSL_CLIENT_CERT_PATH = str(_DEFAULT_SSL_DIR / "client.pem")
|
128
|
+
DEFAULT_SSL_CLIENT_KEY_PATH = str(_DEFAULT_SSL_DIR / "client-key.pem")
|
128
129
|
|
130
|
+
# JWT constants
|
129
131
|
DEFAULT_JWT_AUDIENCE = "appmesh-service"
|
130
132
|
|
133
|
+
# HTTP headers and constants
|
131
134
|
JSON_KEY_MESSAGE = "message"
|
132
135
|
HTTP_USER_AGENT = "appmesh/python"
|
133
136
|
HTTP_HEADER_KEY_AUTH = "Authorization"
|
134
137
|
HTTP_HEADER_KEY_USER_AGENT = "User-Agent"
|
135
138
|
HTTP_HEADER_KEY_X_TARGET_HOST = "X-Target-Host"
|
136
139
|
HTTP_HEADER_KEY_X_FILE_PATH = "X-File-Path"
|
137
|
-
|
140
|
+
HTTP_HEADER_JWT_SET_COOKIE = "X-Set-Cookie"
|
138
141
|
HTTP_HEADER_NAME_CSRF_TOKEN = "X-CSRF-Token"
|
139
142
|
COOKIE_TOKEN = "appmesh_auth_token"
|
140
143
|
COOKIE_CSRF_TOKEN = "appmesh_csrf_token"
|
@@ -154,8 +157,6 @@ class AppMeshClient(metaclass=abc.ABCMeta):
|
|
154
157
|
|
155
158
|
def __init__(self, response: requests.Response):
|
156
159
|
super().__init__()
|
157
|
-
|
158
|
-
# copy essential fields from response
|
159
160
|
self.__dict__.update(response.__dict__)
|
160
161
|
|
161
162
|
self._converted_text = None
|
@@ -164,28 +165,24 @@ class AppMeshClient(metaclass=abc.ABCMeta):
|
|
164
165
|
# Check if we need to convert encoding on Windows
|
165
166
|
if sys.platform == "win32":
|
166
167
|
content_type = response.headers.get("Content-Type", "").lower()
|
167
|
-
|
168
|
+
is_ok = response.status_code == HTTPStatus.OK
|
169
|
+
is_utf8_text = "text/plain" in content_type and "utf-8" in content_type
|
170
|
+
|
171
|
+
if is_ok and is_utf8_text:
|
168
172
|
try:
|
169
173
|
local_encoding = locale.getpreferredencoding()
|
170
|
-
|
171
|
-
if local_encoding.lower() not in ["utf-8", "utf8"]:
|
174
|
+
if local_encoding.lower() not in {"utf-8", "utf8"}:
|
172
175
|
# Ensure response is decoded as UTF-8 first
|
173
176
|
self.encoding = "utf-8"
|
174
177
|
utf8_text = self.text # This gives us proper Unicode string
|
175
178
|
|
176
|
-
|
177
|
-
|
178
|
-
try:
|
179
|
+
with suppress(UnicodeEncodeError, LookupError):
|
180
|
+
# Convert Unicode to local encoding, then back to Unicode
|
179
181
|
local_bytes = utf8_text.encode(local_encoding, errors="replace")
|
180
182
|
self._converted_text = local_bytes.decode(local_encoding)
|
181
183
|
self._should_convert = True
|
182
|
-
except (UnicodeEncodeError, LookupError):
|
183
|
-
# If local encoding can't handle the characters, fall back to UTF-8
|
184
|
-
self._converted_text = utf8_text
|
185
|
-
self._should_convert = True
|
186
184
|
|
187
185
|
except (UnicodeError, LookupError):
|
188
|
-
# If any conversion fails, keep original UTF-8
|
189
186
|
self.encoding = "utf-8"
|
190
187
|
|
191
188
|
@property
|
@@ -193,45 +190,33 @@ class AppMeshClient(metaclass=abc.ABCMeta):
|
|
193
190
|
"""Return converted text if needed, otherwise original text."""
|
194
191
|
if self._should_convert and self._converted_text is not None:
|
195
192
|
return self._converted_text
|
196
|
-
# return the original text from _response without modification
|
197
193
|
return super().text
|
198
194
|
|
199
195
|
def __init__(
|
200
196
|
self,
|
201
197
|
rest_url: str = "https://127.0.0.1:6060",
|
202
|
-
rest_ssl_verify
|
203
|
-
rest_ssl_client_cert=(DEFAULT_SSL_CLIENT_CERT_PATH, DEFAULT_SSL_CLIENT_KEY_PATH)
|
204
|
-
rest_timeout=(60, 300),
|
198
|
+
rest_ssl_verify: Union[bool, str] = DEFAULT_SSL_CA_CERT_PATH,
|
199
|
+
rest_ssl_client_cert: Optional[Union[str, Tuple[str, str]]] = (DEFAULT_SSL_CLIENT_CERT_PATH, DEFAULT_SSL_CLIENT_KEY_PATH),
|
200
|
+
rest_timeout: Tuple[float, float] = (60, 300),
|
205
201
|
jwt_token: Optional[str] = None,
|
206
202
|
rest_cookie_file: Optional[str] = None,
|
207
|
-
auto_refresh_token=False,
|
203
|
+
auto_refresh_token: bool = False,
|
208
204
|
):
|
209
205
|
"""Initialize an App Mesh HTTP client for interacting with the App Mesh server via secure HTTPS.
|
210
206
|
|
211
207
|
Args:
|
212
|
-
rest_url
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
rest_timeout (tuple, optional): HTTP connection timeouts for API requests, as `(connect_timeout, read_timeout)`.
|
226
|
-
The default is `(60, 300)`, where `60` seconds is the maximum time to establish a connection and `300` seconds for the maximum read duration.
|
227
|
-
|
228
|
-
rest_cookie_file (str, optional): Path to a file for storing session cookies.
|
229
|
-
If provided, cookies will be saved to and loaded from this file to maintain session state across client instances instead of keep jwt_token.
|
230
|
-
|
231
|
-
jwt_token (str, optional): JWT token for API authentication, used in headers to authorize requests where required.
|
232
|
-
auto_refresh_token (bool, optional): Enable automatic token refresh before expiration.
|
233
|
-
When enabled, a background timer will monitor token expiration and attempt to refresh
|
234
|
-
the token before it expires. This works with both native App Mesh tokens and Keycloak tokens.
|
208
|
+
rest_url: The server's base URI. Defaults to "https://127.0.0.1:6060".
|
209
|
+
rest_ssl_verify: SSL server verification mode:
|
210
|
+
- True: Use system CAs.
|
211
|
+
- False: Disable verification (insecure).
|
212
|
+
- str: Path to custom CA or directory. To include system CAs, combine them into one file (e.g., cat custom_ca.pem /etc/ssl/certs/ca-certificates.crt > combined_ca.pem).
|
213
|
+
rest_ssl_client_cert: SSL client certificate file(s):
|
214
|
+
- str: Single PEM file with cert+key
|
215
|
+
- tuple: (cert_path, key_path)
|
216
|
+
rest_timeout: Timeouts `(connect_timeout, read_timeout)` in seconds. Default `(60, 300)`.
|
217
|
+
rest_cookie_file: Path to a file for storing session cookies (alternative to jwt_token).
|
218
|
+
jwt_token: JWT token for API authentication, overrides cookie file if both provided.
|
219
|
+
auto_refresh_token: Enable automatic token refresh before expiration (supports App Mesh and Keycloak tokens).
|
235
220
|
"""
|
236
221
|
self._ensure_logging_configured()
|
237
222
|
self.auth_server_url = rest_url
|
@@ -244,86 +229,83 @@ class AppMeshClient(metaclass=abc.ABCMeta):
|
|
244
229
|
# Token auto-refresh
|
245
230
|
self._token_refresh_timer = None
|
246
231
|
self._auto_refresh_token = auto_refresh_token
|
247
|
-
self.jwt_token = jwt_token # Set property last after all dependencies are initialized to setup refresh timer
|
248
232
|
|
249
233
|
# Session and cookie management
|
250
234
|
self._lock = threading.Lock()
|
251
235
|
self.session = requests.Session()
|
252
|
-
self.cookie_file =
|
236
|
+
self.cookie_file = rest_cookie_file
|
237
|
+
loaded = self._load_cookies(rest_cookie_file)
|
238
|
+
cookie_token = self._get_cookie_value(self.session.cookies, self.COOKIE_TOKEN) if loaded else None
|
239
|
+
|
240
|
+
# Set property last after all dependencies are initialized to setup refresh timer
|
241
|
+
self.jwt_token = jwt_token or cookie_token
|
253
242
|
|
254
243
|
@staticmethod
|
255
|
-
def _ensure_logging_configured():
|
256
|
-
"""Ensure logging is configured
|
244
|
+
def _ensure_logging_configured() -> None:
|
245
|
+
"""Ensure logging is configured with a default console handler if needed."""
|
257
246
|
if not logging.root.handlers:
|
258
247
|
logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", datefmt="%Y-%m-%d %H:%M:%S")
|
259
248
|
|
260
249
|
def _get_access_token(self) -> str:
|
261
|
-
|
250
|
+
"""Get the current access token."""
|
251
|
+
return self.jwt_token or ""
|
262
252
|
|
263
|
-
def _load_cookies(self, cookie_file: Optional[str]) ->
|
264
|
-
"""Load cookies from
|
253
|
+
def _load_cookies(self, cookie_file: Optional[str]) -> bool:
|
254
|
+
""" "Load cookies from a Mozilla-format file into the session"""
|
265
255
|
if not cookie_file:
|
266
|
-
return
|
256
|
+
return False
|
267
257
|
|
258
|
+
cookie_path = Path(cookie_file)
|
268
259
|
self.session.cookies = cookiejar.MozillaCookieJar(cookie_file)
|
269
|
-
|
260
|
+
|
261
|
+
if cookie_path.exists():
|
270
262
|
self.session.cookies.load(ignore_discard=True, ignore_expires=True)
|
271
|
-
self.jwt_token = self._get_cookie_value(self.session.cookies, self.COOKIE_TOKEN)
|
272
263
|
else:
|
273
|
-
|
264
|
+
cookie_path.parent.mkdir(parents=True, exist_ok=True)
|
274
265
|
self.session.cookies.save(ignore_discard=True, ignore_expires=True)
|
275
266
|
if os.name == "posix":
|
276
|
-
|
277
|
-
|
267
|
+
cookie_path.chmod(0o600) # User read/write only
|
268
|
+
|
269
|
+
return True
|
278
270
|
|
279
271
|
@staticmethod
|
280
|
-
def _get_cookie_value(cookies, name, check_expiry=True) -> Optional[str]:
|
272
|
+
def _get_cookie_value(cookies, name: str, check_expiry: bool = True) -> Optional[str]:
|
281
273
|
"""Get cookie value by name, checking expiry if requested."""
|
282
274
|
# If it's a RequestsCookieJar, use .get() but check expiry manually if requested
|
283
275
|
if hasattr(cookies, "get") and not isinstance(cookies, list):
|
284
276
|
cookie = cookies.get(name)
|
285
277
|
if cookie is None:
|
286
278
|
return None
|
287
|
-
if check_expiry and
|
279
|
+
if check_expiry and hasattr(cookie, "expires") and cookie.expires:
|
288
280
|
if cookie.expires < time.time():
|
289
281
|
return None # expired
|
290
282
|
return cookie.value if hasattr(cookie, "value") else cookie
|
291
283
|
|
292
284
|
# Otherwise, assume it's a MozillaCookieJar — iterate manually
|
293
|
-
for
|
294
|
-
if
|
295
|
-
if check_expiry and
|
296
|
-
if
|
285
|
+
for cookie in cookies:
|
286
|
+
if cookie.name == name:
|
287
|
+
if check_expiry and hasattr(cookie, "expires") and cookie.expires:
|
288
|
+
if cookie.expires < time.time():
|
297
289
|
return None # expired
|
298
|
-
return
|
290
|
+
return cookie.value
|
299
291
|
|
300
292
|
return None
|
301
293
|
|
302
|
-
def _check_and_refresh_token(self):
|
303
|
-
"""Check and refresh token if needed, then schedule next check.
|
304
|
-
|
305
|
-
This method is triggered by the refresh timer and will:
|
306
|
-
1. Check if token needs refresh based on expiration time
|
307
|
-
2. Refresh the token if needed
|
308
|
-
3. Schedule the next refresh check
|
309
|
-
"""
|
294
|
+
def _check_and_refresh_token(self) -> None:
|
295
|
+
"""Check and refresh token if needed, then schedule next check."""
|
310
296
|
if not self.jwt_token:
|
311
297
|
return
|
312
298
|
|
313
|
-
# Check if token needs refresh
|
314
299
|
needs_refresh = True
|
315
300
|
time_to_expiry = float("inf")
|
316
301
|
|
317
302
|
# Check token expiration directly from JWT
|
318
|
-
|
303
|
+
with suppress(Exception):
|
319
304
|
decoded_token = jwt.decode(self._get_access_token(), options={"verify_signature": False})
|
320
305
|
expiry = decoded_token.get("exp", 0)
|
321
306
|
current_time = time.time()
|
322
307
|
time_to_expiry = expiry - current_time
|
323
|
-
# Refresh if token expires within 5 minutes
|
324
308
|
needs_refresh = time_to_expiry < self.TOKEN_REFRESH_OFFSET
|
325
|
-
except Exception as e:
|
326
|
-
logging.debug("Failed to parse JWT token for expiration check: %s", str(e))
|
327
309
|
|
328
310
|
# Refresh token if needed
|
329
311
|
if needs_refresh:
|
@@ -331,25 +313,14 @@ class AppMeshClient(metaclass=abc.ABCMeta):
|
|
331
313
|
self.renew_token()
|
332
314
|
logging.info("Token successfully refreshed")
|
333
315
|
except Exception as e:
|
334
|
-
logging.error("Token refresh failed: %s",
|
316
|
+
logging.error("Token refresh failed: %s", e)
|
335
317
|
|
336
318
|
# Schedule next check if auto-refresh is still enabled
|
337
319
|
if self._auto_refresh_token and self.jwt_token:
|
338
320
|
self._schedule_token_refresh(time_to_expiry)
|
339
321
|
|
340
|
-
def _schedule_token_refresh(self, time_to_expiry=None):
|
341
|
-
"""Schedule next token refresh check.
|
342
|
-
|
343
|
-
Args:
|
344
|
-
time_to_expiry (float, optional): Time in seconds until token expiration.
|
345
|
-
When provided, helps calculate optimal refresh timing.
|
346
|
-
|
347
|
-
Calculates appropriate check interval:
|
348
|
-
- If token expires soon (within 5 minutes), refresh immediately
|
349
|
-
- Otherwise schedule refresh for the earlier of:
|
350
|
-
1. 5 minutes before expiration
|
351
|
-
2. 60 seconds from now
|
352
|
-
"""
|
322
|
+
def _schedule_token_refresh(self, time_to_expiry: Optional[float] = None) -> None:
|
323
|
+
"""Schedule next token refresh check."""
|
353
324
|
# Cancel existing timer if any
|
354
325
|
if self._token_refresh_timer:
|
355
326
|
self._token_refresh_timer.cancel()
|
@@ -373,23 +344,22 @@ class AppMeshClient(metaclass=abc.ABCMeta):
|
|
373
344
|
self._token_refresh_timer.start()
|
374
345
|
logging.debug("Auto-refresh: Next token check scheduled in %.1f seconds", check_interval)
|
375
346
|
except Exception as e:
|
376
|
-
logging.error("Auto-refresh: Failed to schedule token refresh: %s",
|
347
|
+
logging.error("Auto-refresh: Failed to schedule token refresh: %s", e)
|
377
348
|
|
378
349
|
def close(self):
|
379
350
|
"""Close the session and release resources."""
|
380
351
|
# Cancel token refresh timer
|
381
|
-
if
|
352
|
+
if self._token_refresh_timer:
|
382
353
|
self._token_refresh_timer.cancel()
|
383
354
|
self._token_refresh_timer = None
|
384
355
|
|
385
356
|
# Close the session
|
386
|
-
if
|
357
|
+
if self.session:
|
387
358
|
self.session.close()
|
388
359
|
self.session = None
|
389
360
|
|
390
361
|
# Clean token
|
391
|
-
|
392
|
-
self._jwt_token = None
|
362
|
+
self._jwt_token = None
|
393
363
|
|
394
364
|
def __enter__(self):
|
395
365
|
"""Support for context manager protocol."""
|
@@ -399,114 +369,61 @@ class AppMeshClient(metaclass=abc.ABCMeta):
|
|
399
369
|
"""Support for context manager protocol, ensuring resources are released."""
|
400
370
|
self.close()
|
401
371
|
|
402
|
-
def __del__(self):
|
403
|
-
"""Ensure resources are properly released when the object is garbage collected."""
|
404
|
-
try:
|
405
|
-
self.close()
|
406
|
-
except Exception:
|
407
|
-
pass # Never raise in __del__
|
408
|
-
|
409
372
|
@property
|
410
373
|
def jwt_token(self) -> str:
|
411
|
-
"""Get the current JWT (JSON Web Token) used for authentication.
|
412
|
-
|
413
|
-
This property manages the authentication token used for securing API requests.
|
414
|
-
The token is used to authenticate and authorize requests to the service.
|
415
|
-
|
416
|
-
Returns:
|
417
|
-
str: The current JWT token string.
|
418
|
-
Returns empty string if no token is set.
|
419
|
-
|
420
|
-
Notes:
|
421
|
-
- The token typically includes claims for identity and permissions
|
422
|
-
- Token format: "header.payload.signature"
|
423
|
-
- Tokens are time-sensitive and may expire
|
424
|
-
"""
|
425
|
-
return self._jwt_token
|
374
|
+
"""Get the current JWT (JSON Web Token) used for authentication."""
|
375
|
+
return self._jwt_token or ""
|
426
376
|
|
427
377
|
@jwt_token.setter
|
428
|
-
def jwt_token(self, token: str) -> None:
|
378
|
+
def jwt_token(self, token: Optional[str]) -> None:
|
429
379
|
"""Set the JWT token for authentication.
|
430
380
|
|
431
|
-
|
432
|
-
|
433
|
-
|
434
|
-
Args:
|
435
|
-
token (str): JWT token string in standard JWT format
|
436
|
-
(e.g., "eyJhbGci...payload...signature")
|
437
|
-
Pass empty string to clear the token.
|
438
|
-
|
439
|
-
Example:
|
440
|
-
>>> client.jwt_token = "eyJhbGci..." # Set new token
|
441
|
-
>>> client.jwt_token = "" # Clear token
|
442
|
-
|
443
|
-
Notes:
|
444
|
-
Security best practices:
|
445
|
-
- Store tokens securely
|
446
|
-
- Never log or expose complete tokens
|
447
|
-
- Refresh tokens before expiration
|
448
|
-
- Validate token format before setting
|
381
|
+
Note:
|
382
|
+
This setter has no effect when cookie-based authentication is enabled (i.e., when a cookie file is being used).
|
449
383
|
"""
|
450
384
|
if self._jwt_token == token:
|
451
385
|
return # No change
|
452
386
|
self._jwt_token = token
|
453
387
|
|
454
|
-
#
|
388
|
+
# Handle refresh
|
455
389
|
if self._jwt_token and self._auto_refresh_token:
|
456
390
|
self._schedule_token_refresh()
|
457
391
|
elif self._token_refresh_timer:
|
458
392
|
self._token_refresh_timer.cancel()
|
459
393
|
self._token_refresh_timer = None
|
460
394
|
|
461
|
-
#
|
395
|
+
# Handle session persistence
|
462
396
|
with self._lock:
|
463
|
-
if
|
397
|
+
if self.cookie_file:
|
464
398
|
self.session.cookies.save(ignore_discard=True, ignore_expires=True)
|
465
399
|
|
466
400
|
@property
|
467
401
|
def forward_to(self) -> str:
|
468
|
-
"""
|
402
|
+
"""Target host for request forwarding in a cluster.
|
469
403
|
|
470
|
-
|
471
|
-
|
472
|
-
|
473
|
-
2. hostname/IP with port: will use the specified port
|
404
|
+
Supports:
|
405
|
+
- "hostname" or "IP" → uses current service port
|
406
|
+
- "hostname:port" or "IP:port" → uses specified port
|
474
407
|
|
475
408
|
Returns:
|
476
|
-
str:
|
477
|
-
- "hostname" or "IP" (using current service port)
|
478
|
-
- "hostname:port" or "IP:port" (using specified port)
|
479
|
-
Returns empty string if no forwarding host is set.
|
409
|
+
str: Target host (e.g., "node" or "node:6060"), or empty string if unset.
|
480
410
|
|
481
411
|
Notes:
|
482
|
-
For
|
483
|
-
- All nodes must
|
484
|
-
-
|
485
|
-
- When port is omitted, current service port will be used
|
412
|
+
For JWT sharing across the cluster:
|
413
|
+
- All nodes must use the same `JWTSalt` and `Issuer` for JWT settings
|
414
|
+
- If port is omitted, current service port is used
|
486
415
|
"""
|
487
|
-
return self._forward_to
|
416
|
+
return self._forward_to or ""
|
488
417
|
|
489
418
|
@forward_to.setter
|
490
419
|
def forward_to(self, host: str) -> None:
|
491
|
-
"""Set
|
492
|
-
|
493
|
-
Configure the destination host where requests should be forwarded to. This is
|
494
|
-
used in cluster setups for request routing and load distribution.
|
495
|
-
|
496
|
-
Args:
|
497
|
-
host (str): Target host address in one of two formats:
|
498
|
-
1. "hostname" or "IP" - will use current service port
|
499
|
-
(e.g., "backend-node" or "192.168.1.100")
|
500
|
-
2. "hostname:port" or "IP:port" - will use specified port
|
501
|
-
(e.g., "backend-node:6060" or "192.168.1.100:6060")
|
502
|
-
Pass empty string to disable forwarding.
|
420
|
+
"""Set target host for forwarding.
|
503
421
|
|
504
422
|
Examples:
|
505
423
|
>>> client.forward_to = "backend-node:6060" # Use specific port
|
506
424
|
>>> client.forward_to = "backend-node" # Use current service port
|
507
425
|
>>> client.forward_to = None # Disable forwarding
|
508
426
|
"""
|
509
|
-
|
510
427
|
self._forward_to = host
|
511
428
|
|
512
429
|
########################################
|
@@ -520,37 +437,48 @@ class AppMeshClient(metaclass=abc.ABCMeta):
|
|
520
437
|
timeout_seconds: Union[str, int] = DURATION_ONE_WEEK_ISO,
|
521
438
|
audience: Optional[str] = None,
|
522
439
|
) -> str:
|
523
|
-
"""Login with user name and password
|
440
|
+
"""Login with user name and password.
|
524
441
|
|
525
442
|
Args:
|
526
|
-
user_name
|
527
|
-
user_pwd
|
528
|
-
totp_code
|
529
|
-
timeout_seconds
|
530
|
-
audience
|
443
|
+
user_name: The name of the user.
|
444
|
+
user_pwd: The password of the user.
|
445
|
+
totp_code: The TOTP code if enabled for the user.
|
446
|
+
timeout_seconds: Token expire timeout. Supports ISO 8601 durations (e.g., 'P1Y2M3DT4H5M6S' 'P1W').
|
447
|
+
audience: The audience of the JWT token, should be available by JWT service configuration (default is 'appmesh-service').
|
531
448
|
|
532
449
|
Returns:
|
533
|
-
|
450
|
+
JWT token.
|
534
451
|
"""
|
535
452
|
# Standard App Mesh authentication
|
536
453
|
self.jwt_token = None
|
454
|
+
|
455
|
+
credentials = f"{user_name}:{user_pwd}".encode()
|
456
|
+
headers = {
|
457
|
+
self.HTTP_HEADER_KEY_AUTH: f"Basic {base64.b64encode(credentials).decode()}",
|
458
|
+
"X-Expire-Seconds": str(self._parse_duration(timeout_seconds)),
|
459
|
+
}
|
460
|
+
if audience:
|
461
|
+
headers["X-Audience"] = audience
|
462
|
+
if self.cookie_file:
|
463
|
+
headers[self.HTTP_HEADER_JWT_SET_COOKIE] = "true"
|
464
|
+
# if totp_code:
|
465
|
+
# headers["X-Totp-Code"] = totp_code
|
466
|
+
|
537
467
|
resp = self._request_http(
|
538
468
|
AppMeshClient.Method.POST,
|
539
469
|
path="/appmesh/login",
|
540
|
-
header=
|
541
|
-
self.HTTP_HEADER_KEY_AUTH: "Basic " + base64.b64encode(f"{user_name}:{user_pwd}".encode()).decode(),
|
542
|
-
"X-Expire-Seconds": str(self._parse_duration(timeout_seconds)),
|
543
|
-
**({"X-Audience": audience} if audience else {}),
|
544
|
-
**({self.HTTP_HEADER_JWT_set_cookie: "true"} if self.cookie_file else {}),
|
545
|
-
# **({"X-Totp-Code": totp_code} if totp_code else {}),
|
546
|
-
},
|
470
|
+
header=headers,
|
547
471
|
)
|
472
|
+
|
548
473
|
if resp.status_code == HTTPStatus.OK:
|
549
474
|
if "access_token" in resp.json():
|
550
475
|
self.jwt_token = resp.json()["access_token"]
|
551
|
-
elif resp.status_code == HTTPStatus.PRECONDITION_REQUIRED
|
552
|
-
|
553
|
-
|
476
|
+
elif resp.status_code == HTTPStatus.PRECONDITION_REQUIRED:
|
477
|
+
if not totp_code:
|
478
|
+
raise Exception("TOTP code required")
|
479
|
+
if "totp_challenge" in resp.json():
|
480
|
+
challenge = resp.json()["totp_challenge"]
|
481
|
+
self.validate_totp(user_name, challenge, totp_code, timeout_seconds)
|
554
482
|
else:
|
555
483
|
raise Exception(resp.text)
|
556
484
|
|
@@ -560,49 +488,46 @@ class AppMeshClient(metaclass=abc.ABCMeta):
|
|
560
488
|
"""Validate TOTP challenge and obtain a new JWT token.
|
561
489
|
|
562
490
|
Args:
|
563
|
-
username
|
564
|
-
challenge
|
565
|
-
code
|
566
|
-
timeout
|
567
|
-
Accepts
|
568
|
-
|
491
|
+
username: Username to validate.
|
492
|
+
challenge: Challenge string from server.
|
493
|
+
code: TOTP code to validate.
|
494
|
+
timeout: Token expiration duration, defaults to `DURATION_ONE_WEEK_ISO` (1 week).
|
495
|
+
Accepts either:
|
496
|
+
- **ISO 8601 duration string** (e.g., `'P1Y2M3DT4H5M6S'`, `'P1W'`)
|
497
|
+
- **Numeric value (seconds)** for simpler cases.
|
569
498
|
|
570
499
|
Returns:
|
571
|
-
|
572
|
-
|
573
|
-
Raises:
|
574
|
-
Exception: If validation fails or server returns error
|
500
|
+
New JWT token if validation succeeds.
|
575
501
|
"""
|
502
|
+
body = {
|
503
|
+
"user_name": username,
|
504
|
+
"totp_code": code,
|
505
|
+
"totp_challenge": challenge,
|
506
|
+
"expire_seconds": self._parse_duration(timeout),
|
507
|
+
}
|
508
|
+
|
509
|
+
headers = {self.HTTP_HEADER_JWT_SET_COOKIE: "true"} if self.cookie_file else {}
|
510
|
+
|
576
511
|
resp = self._request_http(
|
577
512
|
AppMeshClient.Method.POST,
|
578
513
|
path="/appmesh/totp/validate",
|
579
|
-
body=
|
580
|
-
|
581
|
-
"totp_code": code,
|
582
|
-
"totp_challenge": challenge,
|
583
|
-
"expire_seconds": self._parse_duration(timeout),
|
584
|
-
},
|
585
|
-
header={self.HTTP_HEADER_JWT_set_cookie: "true"} if self.cookie_file else {},
|
514
|
+
body=body,
|
515
|
+
header=headers,
|
586
516
|
)
|
517
|
+
|
587
518
|
if resp.status_code == HTTPStatus.OK and "access_token" in resp.json():
|
588
519
|
self.jwt_token = resp.json()["access_token"]
|
589
520
|
return self.jwt_token
|
590
521
|
raise Exception(resp.text)
|
591
522
|
|
592
523
|
def logoff(self) -> bool:
|
593
|
-
"""Log out of the current session from the server.
|
594
|
-
|
595
|
-
|
596
|
-
bool: logoff success or failure.
|
597
|
-
"""
|
598
|
-
result = False
|
599
|
-
# Standard App Mesh logout
|
600
|
-
if self.jwt_token and isinstance(self.jwt_token, str):
|
601
|
-
resp = self._request_http(AppMeshClient.Method.POST, path="/appmesh/self/logoff")
|
602
|
-
self.jwt_token = None
|
603
|
-
result = resp.status_code == HTTPStatus.OK
|
524
|
+
"""Log out of the current session from the server."""
|
525
|
+
if not self.jwt_token or not isinstance(self.jwt_token, str):
|
526
|
+
return False
|
604
527
|
|
605
|
-
|
528
|
+
resp = self._request_http(AppMeshClient.Method.POST, path="/appmesh/self/logoff")
|
529
|
+
self.jwt_token = None
|
530
|
+
return resp.status_code == HTTPStatus.OK
|
606
531
|
|
607
532
|
def authentication(self, token: str, permission: Optional[str] = None, audience: Optional[str] = None) -> bool:
|
608
533
|
"""Deprecated: Use authenticate() instead."""
|
@@ -612,191 +537,158 @@ class AppMeshClient(metaclass=abc.ABCMeta):
|
|
612
537
|
"""Authenticate with a token and verify permission if specified.
|
613
538
|
|
614
539
|
Args:
|
615
|
-
token
|
616
|
-
permission
|
617
|
-
|
618
|
-
|
619
|
-
|
620
|
-
audience
|
540
|
+
token: JWT token returned from login().
|
541
|
+
permission: Permission ID to verify the token user.
|
542
|
+
Can be one of:
|
543
|
+
- pre-defined by App Mesh from security.yaml (e.g 'app-view', 'app-delete')
|
544
|
+
- defined by input from role_update() or security.yaml
|
545
|
+
audience: The audience of the JWT token.
|
621
546
|
|
622
547
|
Returns:
|
623
|
-
|
548
|
+
True if authentication succeeds.
|
624
549
|
"""
|
625
550
|
old_token = self.jwt_token
|
626
551
|
self.jwt_token = token
|
627
|
-
|
628
|
-
|
629
|
-
|
630
|
-
|
552
|
+
|
553
|
+
headers = {}
|
554
|
+
if audience:
|
555
|
+
headers["X-Audience"] = audience
|
556
|
+
if permission:
|
557
|
+
headers["X-Permission"] = permission
|
558
|
+
|
631
559
|
resp = self._request_http(AppMeshClient.Method.POST, path="/appmesh/auth", header=headers)
|
560
|
+
|
632
561
|
if resp.status_code != HTTPStatus.OK:
|
633
562
|
self.jwt_token = old_token
|
634
563
|
raise Exception(resp.text)
|
635
|
-
|
564
|
+
|
565
|
+
return True
|
636
566
|
|
637
567
|
def renew_token(self, timeout: Union[int, str] = DURATION_ONE_WEEK_ISO) -> str:
|
638
568
|
"""Renew the current token.
|
639
569
|
|
640
570
|
Args:
|
641
|
-
|
571
|
+
timeout: Token expire timeout.
|
642
572
|
|
643
573
|
Returns:
|
644
|
-
|
645
|
-
|
646
|
-
Raises:
|
647
|
-
Exception: If token renewal fails or no token exists to renew
|
574
|
+
The new JWT token.
|
648
575
|
"""
|
649
|
-
# Ensure token exists
|
650
576
|
if not self.jwt_token:
|
651
577
|
raise Exception("No token to renew")
|
652
578
|
|
653
|
-
|
654
|
-
|
655
|
-
if isinstance(self.jwt_token, str):
|
656
|
-
resp = self._request_http(
|
657
|
-
AppMeshClient.Method.POST,
|
658
|
-
path="/appmesh/token/renew",
|
659
|
-
header={"X-Expire-Seconds": str(self._parse_duration(timeout))},
|
660
|
-
)
|
661
|
-
if resp.status_code == HTTPStatus.OK:
|
662
|
-
if "access_token" in resp.json():
|
663
|
-
new_token = resp.json()["access_token"]
|
664
|
-
self.jwt_token = new_token
|
665
|
-
else:
|
666
|
-
raise Exception("Token renewal response missing access_token")
|
667
|
-
else:
|
668
|
-
raise Exception(resp.text)
|
669
|
-
else:
|
670
|
-
raise Exception("Unsupported token format")
|
579
|
+
if not isinstance(self.jwt_token, str):
|
580
|
+
raise Exception("Unsupported token format")
|
671
581
|
|
672
|
-
|
582
|
+
resp = self._request_http(
|
583
|
+
AppMeshClient.Method.POST,
|
584
|
+
path="/appmesh/token/renew",
|
585
|
+
header={"X-Expire-Seconds": str(self._parse_duration(timeout))},
|
586
|
+
)
|
673
587
|
|
674
|
-
|
675
|
-
|
676
|
-
|
588
|
+
if resp.status_code == HTTPStatus.OK:
|
589
|
+
response_data = resp.json()
|
590
|
+
if "access_token" not in response_data:
|
591
|
+
raise Exception("Token renewal response missing access_token")
|
592
|
+
self.jwt_token = response_data["access_token"]
|
593
|
+
else:
|
594
|
+
raise Exception(resp.text)
|
677
595
|
|
678
|
-
|
679
|
-
"""
|
680
|
-
Generate TOTP secret for the current user and return a secret.
|
596
|
+
return self.jwt_token
|
681
597
|
|
682
|
-
|
683
|
-
|
684
|
-
"""
|
598
|
+
def get_totp_secret(self) -> str:
|
599
|
+
"""Generate TOTP secret for the current user."""
|
685
600
|
resp = self._request_http(method=AppMeshClient.Method.POST, path="/appmesh/totp/secret")
|
686
|
-
if resp.status_code == HTTPStatus.OK:
|
687
|
-
totp_uri = base64.b64decode(resp.json()["mfa_uri"]).decode()
|
688
|
-
return self._parse_totp_uri(totp_uri).get("secret")
|
689
601
|
|
690
|
-
|
602
|
+
if resp.status_code != HTTPStatus.OK:
|
603
|
+
raise Exception(resp.text)
|
604
|
+
|
605
|
+
totp_uri = base64.b64decode(resp.json()["mfa_uri"]).decode()
|
606
|
+
parsed_uri = self._parse_totp_uri(totp_uri)
|
607
|
+
secret = parsed_uri.get("secret")
|
608
|
+
if secret is None:
|
609
|
+
raise Exception("TOTP URI does not contain a 'secret' field")
|
610
|
+
return secret
|
691
611
|
|
692
612
|
def setup_totp(self, totp_code: str) -> str:
|
693
613
|
"""Set up 2FA for the current user.
|
694
614
|
|
695
615
|
Args:
|
696
|
-
totp_code
|
616
|
+
totp_code: TOTP code.
|
697
617
|
|
698
618
|
Returns:
|
699
|
-
|
619
|
+
The new JWT token if setup succeeds.
|
700
620
|
"""
|
701
621
|
resp = self._request_http(
|
702
622
|
method=AppMeshClient.Method.POST,
|
703
623
|
path="/appmesh/totp/setup",
|
704
624
|
header={"X-Totp-Code": totp_code},
|
705
625
|
)
|
626
|
+
|
706
627
|
if resp.status_code == HTTPStatus.OK:
|
707
628
|
if "access_token" in resp.json():
|
708
629
|
self.jwt_token = resp.json()["access_token"]
|
709
630
|
return self.jwt_token
|
710
|
-
else:
|
711
|
-
raise Exception(resp.text)
|
712
631
|
|
713
|
-
|
714
|
-
"""Disable 2FA for the specified user.
|
715
|
-
|
716
|
-
Args:
|
717
|
-
user (str, optional): user name for disable TOTP.
|
632
|
+
raise Exception(resp.text)
|
718
633
|
|
719
|
-
|
720
|
-
|
721
|
-
"""
|
634
|
+
def disable_totp(self, user: str = "self") -> None:
|
635
|
+
"""Disable 2FA for the specified user."""
|
722
636
|
resp = self._request_http(
|
723
637
|
method=AppMeshClient.Method.POST,
|
724
638
|
path=f"/appmesh/totp/{user}/disable",
|
725
639
|
)
|
640
|
+
|
726
641
|
if resp.status_code != HTTPStatus.OK:
|
727
642
|
raise Exception(resp.text)
|
728
|
-
return resp.status_code == HTTPStatus.OK
|
729
643
|
|
730
644
|
@staticmethod
|
731
645
|
def _parse_totp_uri(totp_uri: str) -> dict:
|
732
|
-
"""Extract TOTP parameters
|
733
|
-
|
734
|
-
Args:
|
735
|
-
totp_uri (str): TOTP uri
|
736
|
-
|
737
|
-
Returns:
|
738
|
-
dict: eextract parameters
|
739
|
-
"""
|
646
|
+
"""Extract TOTP parameters from URI."""
|
740
647
|
parsed_info = {}
|
741
648
|
parsed_uri = parse.urlparse(totp_uri)
|
742
649
|
|
743
650
|
# Extract label from the path
|
744
|
-
parsed_info["label"] = parsed_uri.path[1:] # Remove
|
651
|
+
parsed_info["label"] = parsed_uri.path[1:] # Remove leading slash
|
745
652
|
|
746
653
|
# Extract parameters from the query string
|
747
654
|
query_params = parse.parse_qs(parsed_uri.query)
|
748
655
|
for key, value in query_params.items():
|
749
656
|
parsed_info[key] = value[0]
|
657
|
+
|
750
658
|
return parsed_info
|
751
659
|
|
752
660
|
########################################
|
753
661
|
# Application view
|
754
662
|
########################################
|
755
663
|
def view_app(self, app_name: str) -> App:
|
756
|
-
"""Get information about a specific application.
|
757
|
-
|
758
|
-
Args:
|
759
|
-
app_name (str): the application name.
|
760
|
-
|
761
|
-
Returns:
|
762
|
-
App: the application object both contain static configuration and runtime information.
|
763
|
-
|
764
|
-
Exception:
|
765
|
-
failed request or no such application
|
766
|
-
"""
|
664
|
+
"""Get information about a specific application."""
|
767
665
|
resp = self._request_http(AppMeshClient.Method.GET, path=f"/appmesh/app/{app_name}")
|
666
|
+
|
768
667
|
if resp.status_code != HTTPStatus.OK:
|
769
668
|
raise Exception(resp.text)
|
770
|
-
return App(resp.json())
|
771
|
-
|
772
|
-
def view_all_apps(self):
|
773
|
-
"""Get information about all applications.
|
774
669
|
|
775
|
-
|
776
|
-
list: the application object both contain static configuration and runtime information, only return applications that the user has permissions.
|
670
|
+
return App(resp.json())
|
777
671
|
|
778
|
-
|
779
|
-
|
780
|
-
"""
|
672
|
+
def view_all_apps(self) -> list:
|
673
|
+
"""Get information about all applications."""
|
781
674
|
resp = self._request_http(AppMeshClient.Method.GET, path="/appmesh/applications")
|
675
|
+
|
782
676
|
if resp.status_code != HTTPStatus.OK:
|
783
677
|
raise Exception(resp.text)
|
784
|
-
|
785
|
-
for app in resp.json()
|
786
|
-
apps.append(App(app))
|
787
|
-
return apps
|
678
|
+
|
679
|
+
return [App(app) for app in resp.json()]
|
788
680
|
|
789
681
|
def get_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:
|
790
682
|
"""Get the stdout/stderr of an application.
|
791
683
|
|
792
684
|
Args:
|
793
|
-
app_name
|
794
|
-
stdout_position
|
795
|
-
stdout_index
|
685
|
+
app_name: the application name
|
686
|
+
stdout_position: start read position, 0 means start from beginning.
|
687
|
+
stdout_index: index of history process stdout, 0 means get from current running process,
|
796
688
|
the stdout number depends on 'stdout_cache_size' of the application.
|
797
|
-
stdout_maxsize
|
798
|
-
process_uuid
|
799
|
-
timeout
|
689
|
+
stdout_maxsize: max buffer size to read.
|
690
|
+
process_uuid: used to get the specified process.
|
691
|
+
timeout: wait for the running process for some time(seconds) to get the output.
|
800
692
|
|
801
693
|
Returns:
|
802
694
|
AppOutput object.
|
@@ -812,450 +704,312 @@ class AppMeshClient(metaclass=abc.ABCMeta):
|
|
812
704
|
"timeout": str(timeout),
|
813
705
|
},
|
814
706
|
)
|
707
|
+
|
815
708
|
out_position = int(resp.headers["X-Output-Position"]) if "X-Output-Position" in resp.headers else None
|
816
709
|
exit_code = int(resp.headers["X-Exit-Code"]) if "X-Exit-Code" in resp.headers else None
|
710
|
+
|
817
711
|
return AppOutput(status_code=resp.status_code, output=resp.text, out_position=out_position, exit_code=exit_code)
|
818
712
|
|
819
713
|
def check_app_health(self, app_name: str) -> bool:
|
820
|
-
"""Check the health status of an application.
|
821
|
-
|
822
|
-
Args:
|
823
|
-
app_name (str): the application name.
|
824
|
-
|
825
|
-
Returns:
|
826
|
-
bool: healthy or not
|
827
|
-
"""
|
714
|
+
"""Check the health status of an application."""
|
828
715
|
resp = self._request_http(AppMeshClient.Method.GET, path=f"/appmesh/app/{app_name}/health")
|
716
|
+
|
829
717
|
if resp.status_code != HTTPStatus.OK:
|
830
718
|
raise Exception(resp.text)
|
719
|
+
|
831
720
|
return int(resp.text) == 0
|
832
721
|
|
833
722
|
########################################
|
834
723
|
# Application manage
|
835
724
|
########################################
|
836
725
|
def add_app(self, app: App) -> App:
|
837
|
-
|
838
|
-
|
839
|
-
Args:
|
840
|
-
app (App): the application definition.
|
841
|
-
|
842
|
-
Returns:
|
843
|
-
App: resigtered application object.
|
844
|
-
|
845
|
-
Exception:
|
846
|
-
failed request
|
847
|
-
"""
|
726
|
+
# type: (App) -> App
|
727
|
+
"""Register a new application."""
|
848
728
|
resp = self._request_http(AppMeshClient.Method.PUT, path=f"/appmesh/app/{app.name}", body=app.json())
|
729
|
+
|
849
730
|
if resp.status_code != HTTPStatus.OK:
|
850
731
|
raise Exception(resp.text)
|
732
|
+
|
851
733
|
return App(resp.json())
|
852
734
|
|
853
735
|
def delete_app(self, app_name: str) -> bool:
|
854
|
-
"""Remove an application.
|
855
|
-
|
856
|
-
Args:
|
857
|
-
app_name (str): the application name.
|
858
|
-
|
859
|
-
Returns:
|
860
|
-
bool: True for delete success, Flase for not exist anymore.
|
861
|
-
"""
|
736
|
+
"""Remove an application."""
|
862
737
|
resp = self._request_http(AppMeshClient.Method.DELETE, path=f"/appmesh/app/{app_name}")
|
738
|
+
|
863
739
|
if resp.status_code == HTTPStatus.OK:
|
864
740
|
return True
|
865
|
-
|
741
|
+
if resp.status_code == HTTPStatus.NOT_FOUND:
|
866
742
|
return False
|
867
|
-
else:
|
868
|
-
raise Exception(resp.text)
|
869
|
-
|
870
|
-
def enable_app(self, app_name: str) -> bool:
|
871
|
-
"""Enable an application.
|
872
743
|
|
873
|
-
|
874
|
-
app_name (str): the application name.
|
744
|
+
raise Exception(resp.text)
|
875
745
|
|
876
|
-
|
877
|
-
|
878
|
-
"""
|
746
|
+
def enable_app(self, app_name: str) -> None:
|
747
|
+
"""Enable an application."""
|
879
748
|
resp = self._request_http(AppMeshClient.Method.POST, path=f"/appmesh/app/{app_name}/enable")
|
749
|
+
|
880
750
|
if resp.status_code != HTTPStatus.OK:
|
881
751
|
raise Exception(resp.text)
|
882
|
-
return resp.status_code == HTTPStatus.OK
|
883
|
-
|
884
|
-
def disable_app(self, app_name: str) -> bool:
|
885
|
-
"""Disable an application.
|
886
752
|
|
887
|
-
|
888
|
-
|
889
|
-
|
890
|
-
Returns:
|
891
|
-
bool: success or failure.
|
892
|
-
"""
|
753
|
+
def disable_app(self, app_name: str) -> None:
|
754
|
+
"""Disable an application."""
|
893
755
|
resp = self._request_http(AppMeshClient.Method.POST, path=f"/appmesh/app/{app_name}/disable")
|
756
|
+
|
894
757
|
if resp.status_code != HTTPStatus.OK:
|
895
758
|
raise Exception(resp.text)
|
896
|
-
return resp.status_code == HTTPStatus.OK
|
897
759
|
|
898
760
|
########################################
|
899
761
|
# Configuration
|
900
762
|
########################################
|
901
763
|
def view_host_resources(self) -> dict:
|
902
|
-
"""Get a report of host resources including CPU, memory, and disk.
|
903
|
-
|
904
|
-
Returns:
|
905
|
-
dict: the host resource json.
|
906
|
-
"""
|
764
|
+
"""Get a report of host resources including CPU, memory, and disk."""
|
907
765
|
resp = self._request_http(AppMeshClient.Method.GET, path="/appmesh/resources")
|
766
|
+
|
908
767
|
if resp.status_code != HTTPStatus.OK:
|
909
768
|
raise Exception(resp.text)
|
769
|
+
|
910
770
|
return resp.json()
|
911
771
|
|
912
772
|
def view_config(self) -> dict:
|
913
|
-
"""Get the App Mesh configuration in JSON format.
|
914
|
-
|
915
|
-
Returns:
|
916
|
-
dict: the configuration json.
|
917
|
-
"""
|
773
|
+
"""Get the App Mesh configuration in JSON format."""
|
918
774
|
resp = self._request_http(AppMeshClient.Method.GET, path="/appmesh/config")
|
775
|
+
|
919
776
|
if resp.status_code != HTTPStatus.OK:
|
920
777
|
raise Exception(resp.text)
|
778
|
+
|
921
779
|
return resp.json()
|
922
780
|
|
923
781
|
def set_config(self, config_json: dict) -> dict:
|
924
|
-
"""Update the configuration.
|
925
|
-
|
926
|
-
Args:
|
927
|
-
cfg_json (dict): the new configuration json.
|
928
|
-
|
929
|
-
Returns:
|
930
|
-
dict: the updated configuration json.
|
931
|
-
"""
|
782
|
+
"""Update the configuration."""
|
932
783
|
resp = self._request_http(AppMeshClient.Method.POST, path="/appmesh/config", body=config_json)
|
784
|
+
|
933
785
|
if resp.status_code != HTTPStatus.OK:
|
934
786
|
raise Exception(resp.text)
|
787
|
+
|
935
788
|
return resp.json()
|
936
789
|
|
937
790
|
def set_log_level(self, level: str = "DEBUG") -> str:
|
938
|
-
"""Update the log level.
|
939
|
-
|
940
|
-
Args:
|
941
|
-
level (str, optional): log level.
|
942
|
-
|
943
|
-
Returns:
|
944
|
-
str: the updated log level.
|
945
|
-
"""
|
791
|
+
"""Update the log level."""
|
946
792
|
resp = self._request_http(AppMeshClient.Method.POST, path="/appmesh/config", body={"BaseConfig": {"LogLevel": level}})
|
793
|
+
|
947
794
|
if resp.status_code != HTTPStatus.OK:
|
948
795
|
raise Exception(resp.text)
|
796
|
+
|
949
797
|
return resp.json()["BaseConfig"]["LogLevel"]
|
950
798
|
|
951
799
|
########################################
|
952
800
|
# User Management
|
953
801
|
########################################
|
954
|
-
def update_user_password(self, old_password: str, new_password: str, user_name: str = "self") ->
|
955
|
-
"""Change the password of a user.
|
956
|
-
|
957
|
-
|
958
|
-
|
959
|
-
|
960
|
-
new_password (str):the new password string.
|
802
|
+
def update_user_password(self, old_password: str, new_password: str, user_name: str = "self") -> None:
|
803
|
+
"""Change the password of a user."""
|
804
|
+
body = {
|
805
|
+
"old_password": base64.b64encode(old_password.encode()).decode(),
|
806
|
+
"new_password": base64.b64encode(new_password.encode()).decode(),
|
807
|
+
}
|
961
808
|
|
962
|
-
Returns:
|
963
|
-
bool: success
|
964
|
-
"""
|
965
809
|
resp = self._request_http(
|
966
810
|
method=AppMeshClient.Method.POST,
|
967
811
|
path=f"/appmesh/user/{user_name}/passwd",
|
968
|
-
body=
|
969
|
-
"old_password": base64.b64encode(old_password.encode()).decode(),
|
970
|
-
"new_password": base64.b64encode(new_password.encode()).decode(),
|
971
|
-
},
|
812
|
+
body=body,
|
972
813
|
)
|
814
|
+
|
973
815
|
if resp.status_code != HTTPStatus.OK:
|
974
816
|
raise Exception(resp.text)
|
975
|
-
return True
|
976
|
-
|
977
|
-
def add_user(self, user_name: str, user_json: dict) -> bool:
|
978
|
-
"""Add a new user.
|
979
|
-
|
980
|
-
Args:
|
981
|
-
user_name (str): the user name.
|
982
|
-
user_json (dict): user definition, follow same user format from security.yaml.
|
983
817
|
|
984
|
-
|
985
|
-
|
986
|
-
"""
|
818
|
+
def add_user(self, user_name: str, user_json: dict) -> None:
|
819
|
+
"""Add a new user."""
|
987
820
|
resp = self._request_http(
|
988
821
|
method=AppMeshClient.Method.PUT,
|
989
822
|
path=f"/appmesh/user/{user_name}",
|
990
823
|
body=user_json,
|
991
824
|
)
|
992
|
-
|
993
|
-
|
994
|
-
def delete_user(self, user_name: str) -> bool:
|
995
|
-
"""Delete a user.
|
996
|
-
|
997
|
-
Args:
|
998
|
-
user_name (str): the user name.
|
825
|
+
if resp.status_code != HTTPStatus.OK:
|
826
|
+
raise Exception(resp.text)
|
999
827
|
|
1000
|
-
|
1001
|
-
|
1002
|
-
"""
|
828
|
+
def delete_user(self, user_name: str):
|
829
|
+
"""Delete a user."""
|
1003
830
|
resp = self._request_http(
|
1004
831
|
method=AppMeshClient.Method.DELETE,
|
1005
832
|
path=f"/appmesh/user/{user_name}",
|
1006
833
|
)
|
1007
|
-
|
1008
|
-
|
1009
|
-
def lock_user(self, user_name: str) -> bool:
|
1010
|
-
"""Lock a user.
|
1011
|
-
|
1012
|
-
Args:
|
1013
|
-
user_name (str): the user name.
|
834
|
+
if resp.status_code != HTTPStatus.OK:
|
835
|
+
raise Exception(resp.text)
|
1014
836
|
|
1015
|
-
|
1016
|
-
|
1017
|
-
"""
|
837
|
+
def lock_user(self, user_name: str) -> None:
|
838
|
+
"""Lock a user."""
|
1018
839
|
resp = self._request_http(
|
1019
840
|
method=AppMeshClient.Method.POST,
|
1020
841
|
path=f"/appmesh/user/{user_name}/lock",
|
1021
842
|
)
|
843
|
+
|
1022
844
|
if resp.status_code != HTTPStatus.OK:
|
1023
845
|
raise Exception(resp.text)
|
1024
|
-
return resp.status_code == HTTPStatus.OK
|
1025
|
-
|
1026
|
-
def unlock_user(self, user_name: str) -> bool:
|
1027
|
-
"""Unlock a user.
|
1028
846
|
|
1029
|
-
|
1030
|
-
|
1031
|
-
|
1032
|
-
Returns:
|
1033
|
-
bool: success or failure.
|
1034
|
-
"""
|
847
|
+
def unlock_user(self, user_name: str) -> None:
|
848
|
+
"""Unlock a user."""
|
1035
849
|
resp = self._request_http(
|
1036
850
|
method=AppMeshClient.Method.POST,
|
1037
851
|
path=f"/appmesh/user/{user_name}/unlock",
|
1038
852
|
)
|
853
|
+
|
1039
854
|
if resp.status_code != HTTPStatus.OK:
|
1040
855
|
raise Exception(resp.text)
|
1041
|
-
return resp.status_code == HTTPStatus.OK
|
1042
856
|
|
1043
857
|
def view_users(self) -> dict:
|
1044
|
-
"""Get information about all users.
|
1045
|
-
|
1046
|
-
Returns:
|
1047
|
-
dict: all user definition
|
1048
|
-
"""
|
858
|
+
"""Get information about all users."""
|
1049
859
|
resp = self._request_http(method=AppMeshClient.Method.GET, path="/appmesh/users")
|
860
|
+
|
1050
861
|
if resp.status_code != HTTPStatus.OK:
|
1051
862
|
raise Exception(resp.text)
|
863
|
+
|
1052
864
|
return resp.json()
|
1053
865
|
|
1054
866
|
def view_self(self) -> dict:
|
1055
|
-
"""Get information about the current user.
|
1056
|
-
|
1057
|
-
Returns:
|
1058
|
-
dict: user definition.
|
1059
|
-
"""
|
867
|
+
"""Get information about the current user."""
|
1060
868
|
resp = self._request_http(method=AppMeshClient.Method.GET, path="/appmesh/user/self")
|
869
|
+
|
1061
870
|
if resp.status_code != HTTPStatus.OK:
|
1062
871
|
raise Exception(resp.text)
|
872
|
+
|
1063
873
|
return resp.json()
|
1064
874
|
|
1065
875
|
def view_groups(self) -> list:
|
1066
|
-
"""Get information about all user groups.
|
1067
|
-
|
1068
|
-
Returns:
|
1069
|
-
dict: user group array.
|
1070
|
-
"""
|
876
|
+
"""Get information about all user groups."""
|
1071
877
|
resp = self._request_http(method=AppMeshClient.Method.GET, path="/appmesh/user/groups")
|
878
|
+
|
1072
879
|
if resp.status_code != HTTPStatus.OK:
|
1073
880
|
raise Exception(resp.text)
|
881
|
+
|
1074
882
|
return resp.json()
|
1075
883
|
|
1076
884
|
def view_permissions(self) -> list:
|
1077
|
-
"""Get information about all available permissions.
|
1078
|
-
|
1079
|
-
Returns:
|
1080
|
-
dict: permission array
|
1081
|
-
"""
|
885
|
+
"""Get information about all available permissions."""
|
1082
886
|
resp = self._request_http(method=AppMeshClient.Method.GET, path="/appmesh/permissions")
|
887
|
+
|
1083
888
|
if resp.status_code != HTTPStatus.OK:
|
1084
889
|
raise Exception(resp.text)
|
890
|
+
|
1085
891
|
return resp.json()
|
1086
892
|
|
1087
893
|
def view_user_permissions(self) -> list:
|
1088
|
-
"""Get information about the permissions of the current user.
|
1089
|
-
|
1090
|
-
Returns:
|
1091
|
-
dict: user permission array.
|
1092
|
-
"""
|
894
|
+
"""Get information about the permissions of the current user."""
|
1093
895
|
resp = self._request_http(method=AppMeshClient.Method.GET, path="/appmesh/user/permissions")
|
896
|
+
|
1094
897
|
if resp.status_code != HTTPStatus.OK:
|
1095
898
|
raise Exception(resp.text)
|
899
|
+
|
1096
900
|
return resp.json()
|
1097
901
|
|
1098
902
|
def view_roles(self) -> list:
|
1099
|
-
"""Get information about all roles with permission definitions.
|
1100
|
-
|
1101
|
-
Returns:
|
1102
|
-
dict: all role definition.
|
1103
|
-
"""
|
903
|
+
"""Get information about all roles with permission definitions."""
|
1104
904
|
resp = self._request_http(method=AppMeshClient.Method.GET, path="/appmesh/roles")
|
905
|
+
|
1105
906
|
if resp.status_code != HTTPStatus.OK:
|
1106
907
|
raise Exception(resp.text)
|
1107
|
-
return resp.json()
|
1108
908
|
|
1109
|
-
|
1110
|
-
"""Update or add a role with defined permissions.
|
1111
|
-
|
1112
|
-
Args:
|
1113
|
-
role_name (str): the role name.
|
1114
|
-
role_permission_json (dict): role permission definition array, e.g: ["app-control", "app-delete"]
|
909
|
+
return resp.json()
|
1115
910
|
|
1116
|
-
|
1117
|
-
|
1118
|
-
"""
|
911
|
+
def update_role(self, role_name: str, role_permission_json: dict) -> None:
|
912
|
+
"""Update or add a role with defined permissions."""
|
1119
913
|
resp = self._request_http(method=AppMeshClient.Method.POST, path=f"/appmesh/role/{role_name}", body=role_permission_json)
|
914
|
+
|
1120
915
|
if resp.status_code != HTTPStatus.OK:
|
1121
916
|
raise Exception(resp.text)
|
1122
|
-
return resp.status_code == HTTPStatus.OK
|
1123
|
-
|
1124
|
-
def delete_role(self, role_name: str) -> bool:
|
1125
|
-
"""Delete a user role.
|
1126
|
-
|
1127
|
-
Args:
|
1128
|
-
role_name (str): the role name.
|
1129
917
|
|
1130
|
-
|
1131
|
-
|
1132
|
-
"""
|
918
|
+
def delete_role(self, role_name: str) -> None:
|
919
|
+
"""Delete a user role."""
|
1133
920
|
resp = self._request_http(
|
1134
921
|
method=AppMeshClient.Method.DELETE,
|
1135
922
|
path=f"/appmesh/role/{role_name}",
|
1136
923
|
)
|
924
|
+
|
1137
925
|
if resp.status_code != HTTPStatus.OK:
|
1138
926
|
raise Exception(resp.text)
|
1139
|
-
return resp.status_code == HTTPStatus.OK
|
1140
927
|
|
1141
928
|
########################################
|
1142
929
|
# Tag management
|
1143
930
|
########################################
|
1144
|
-
def add_tag(self, tag_name: str, tag_value: str) ->
|
1145
|
-
"""Add a new label.
|
1146
|
-
|
1147
|
-
Args:
|
1148
|
-
tag_name (str): the label name.
|
1149
|
-
tag_value (str): the label value.
|
1150
|
-
|
1151
|
-
Returns:
|
1152
|
-
bool: success or failure.
|
1153
|
-
"""
|
931
|
+
def add_tag(self, tag_name: str, tag_value: str) -> None:
|
932
|
+
"""Add a new label."""
|
1154
933
|
resp = self._request_http(
|
1155
934
|
AppMeshClient.Method.PUT,
|
1156
935
|
query={"value": tag_value},
|
1157
936
|
path=f"/appmesh/label/{tag_name}",
|
1158
937
|
)
|
938
|
+
|
1159
939
|
if resp.status_code != HTTPStatus.OK:
|
1160
940
|
raise Exception(resp.text)
|
1161
|
-
return resp.status_code == HTTPStatus.OK
|
1162
|
-
|
1163
|
-
def delete_tag(self, tag_name: str) -> bool:
|
1164
|
-
"""Delete a label.
|
1165
941
|
|
1166
|
-
|
1167
|
-
|
1168
|
-
|
1169
|
-
Returns:
|
1170
|
-
bool: success or failure.
|
1171
|
-
"""
|
942
|
+
def delete_tag(self, tag_name: str) -> None:
|
943
|
+
"""Delete a label."""
|
1172
944
|
resp = self._request_http(AppMeshClient.Method.DELETE, path=f"/appmesh/label/{tag_name}")
|
945
|
+
|
1173
946
|
if resp.status_code != HTTPStatus.OK:
|
1174
947
|
raise Exception(resp.text)
|
1175
|
-
return resp.status_code == HTTPStatus.OK
|
1176
948
|
|
1177
949
|
def view_tags(self) -> dict:
|
1178
|
-
"""Get information about all labels.
|
1179
|
-
|
1180
|
-
Returns:
|
1181
|
-
dict: label data.
|
1182
|
-
"""
|
950
|
+
"""Get information about all labels."""
|
1183
951
|
resp = self._request_http(AppMeshClient.Method.GET, path="/appmesh/labels")
|
952
|
+
|
1184
953
|
if resp.status_code != HTTPStatus.OK:
|
1185
954
|
raise Exception(resp.text)
|
955
|
+
|
1186
956
|
return resp.json()
|
1187
957
|
|
1188
958
|
########################################
|
1189
|
-
#
|
959
|
+
# Prometheus metrics
|
1190
960
|
########################################
|
1191
|
-
def get_metrics(self):
|
1192
|
-
"""Get Prometheus metrics.
|
1193
|
-
|
1194
|
-
Returns:
|
1195
|
-
str: prometheus metrics texts
|
1196
|
-
"""
|
961
|
+
def get_metrics(self) -> str:
|
962
|
+
"""Get Prometheus metrics."""
|
1197
963
|
resp = self._request_http(AppMeshClient.Method.GET, path="/appmesh/metrics")
|
964
|
+
|
1198
965
|
if resp.status_code != HTTPStatus.OK:
|
1199
966
|
raise Exception(resp.text)
|
967
|
+
|
1200
968
|
return resp.text
|
1201
969
|
|
1202
970
|
########################################
|
1203
971
|
# File management
|
1204
972
|
########################################
|
1205
|
-
def download_file(self, remote_file: str, local_file: str,
|
1206
|
-
"""Download a remote file to the local system.
|
1207
|
-
|
1208
|
-
Args:
|
1209
|
-
remote_file (str): the remote file path.
|
1210
|
-
local_file (str): the local file path to be downloaded.
|
1211
|
-
apply_file_attributes (bool): whether to apply file attributes (permissions, owner, group) to the local file.
|
1212
|
-
"""
|
973
|
+
def download_file(self, remote_file: str, local_file: str, preserve_permissions: bool = True) -> None:
|
974
|
+
"""Download a remote file to the local system."""
|
1213
975
|
resp = self._request_http(AppMeshClient.Method.GET, path="/appmesh/file/download", header={self.HTTP_HEADER_KEY_X_FILE_PATH: remote_file})
|
1214
976
|
resp.raise_for_status()
|
1215
977
|
|
1216
978
|
# Write the file content locally
|
1217
|
-
|
1218
|
-
|
979
|
+
local_path = Path(local_file)
|
980
|
+
with local_path.open("wb") as fp:
|
981
|
+
for chunk in resp.iter_content(chunk_size=8 * 1024):
|
1219
982
|
if chunk:
|
1220
983
|
fp.write(chunk)
|
1221
984
|
|
1222
985
|
# Apply file attributes (permissions, owner, group) if requested
|
1223
|
-
if
|
986
|
+
if preserve_permissions and sys.platform != "win32":
|
1224
987
|
if "X-File-Mode" in resp.headers:
|
1225
|
-
|
988
|
+
local_path.chmod(int(resp.headers["X-File-Mode"]))
|
989
|
+
|
1226
990
|
if "X-File-User" in resp.headers and "X-File-Group" in resp.headers:
|
1227
991
|
file_uid = int(resp.headers["X-File-User"])
|
1228
992
|
file_gid = int(resp.headers["X-File-Group"])
|
1229
|
-
|
993
|
+
with suppress(PermissionError):
|
1230
994
|
os.chown(path=local_file, uid=file_uid, gid=file_gid)
|
1231
|
-
except PermissionError:
|
1232
|
-
logging.warning(f"Warning: Unable to change owner/group of {local_file}. Operation requires elevated privileges.")
|
1233
|
-
|
1234
|
-
def upload_file(self, local_file: str, remote_file: str, apply_file_attributes: bool = True) -> None:
|
1235
|
-
"""Upload a local file to the remote server. Optionally, the remote file will have the same permission as the local file.
|
1236
995
|
|
1237
|
-
|
1238
|
-
|
1239
|
-
|
1240
|
-
|
1241
|
-
Args:
|
1242
|
-
local_file (str): the local file path.
|
1243
|
-
remote_file (str): the target remote file to be uploaded.
|
1244
|
-
apply_file_attributes (bool): whether to upload file attributes (permissions, owner, group) along with the file.
|
1245
|
-
"""
|
1246
|
-
if not os.path.exists(local_file):
|
996
|
+
def upload_file(self, local_file: str, remote_file: str, preserve_permissions: bool = True) -> None:
|
997
|
+
"""Upload a local file to the remote server."""
|
998
|
+
local_path = Path(local_file)
|
999
|
+
if not local_path.exists():
|
1247
1000
|
raise FileNotFoundError(f"Local file not found: {local_file}")
|
1248
1001
|
|
1249
1002
|
from requests_toolbelt import MultipartEncoder
|
1250
1003
|
|
1251
|
-
with open(
|
1004
|
+
with local_path.open("rb") as fp:
|
1252
1005
|
encoder = MultipartEncoder(fields={"filename": os.path.basename(remote_file), "file": ("filename", fp, "application/octet-stream")})
|
1006
|
+
|
1253
1007
|
header = {self.HTTP_HEADER_KEY_X_FILE_PATH: parse.quote(remote_file), "Content-Type": encoder.content_type}
|
1254
1008
|
|
1255
|
-
# Include file attributes
|
1256
|
-
if
|
1257
|
-
file_stat =
|
1258
|
-
header["X-File-Mode"] = str(file_stat.st_mode & 0o777)
|
1009
|
+
# Include file attributes if requested
|
1010
|
+
if preserve_permissions:
|
1011
|
+
file_stat = local_path.stat()
|
1012
|
+
header["X-File-Mode"] = str(file_stat.st_mode & 0o777) # Mask to keep only permission bits
|
1259
1013
|
header["X-File-User"] = str(file_stat.st_uid)
|
1260
1014
|
header["X-File-Group"] = str(file_stat.st_gid)
|
1261
1015
|
|
@@ -1272,13 +1026,14 @@ class AppMeshClient(metaclass=abc.ABCMeta):
|
|
1272
1026
|
########################################
|
1273
1027
|
# Application run
|
1274
1028
|
########################################
|
1275
|
-
|
1029
|
+
@staticmethod
|
1030
|
+
def _parse_duration(timeout: Union[int, str]) -> int:
|
1031
|
+
"""Parse duration from int or ISO 8601 string."""
|
1276
1032
|
if isinstance(timeout, int):
|
1277
1033
|
return timeout
|
1278
|
-
|
1034
|
+
if isinstance(timeout, str):
|
1279
1035
|
return int(aniso8601.parse_duration(timeout).total_seconds())
|
1280
|
-
|
1281
|
-
raise TypeError(f"Invalid timeout type: {str(timeout)}")
|
1036
|
+
raise TypeError(f"Invalid timeout type: {timeout}")
|
1282
1037
|
|
1283
1038
|
def run_task(self, app_name: str, data: str, timeout: int = 300) -> str:
|
1284
1039
|
"""Client send an invocation message to a running App Mesh application and wait for result.
|
@@ -1287,20 +1042,20 @@ class AppMeshClient(metaclass=abc.ABCMeta):
|
|
1287
1042
|
forward it to the specified running application instance.
|
1288
1043
|
|
1289
1044
|
Args:
|
1290
|
-
app_name
|
1291
|
-
data
|
1292
|
-
timeout
|
1045
|
+
app_name: Name of the target application (as registered in App Mesh).
|
1046
|
+
data: Payload to deliver to the application. Typically a string.
|
1047
|
+
timeout: Maximum time in seconds to wait for a response from the application. Defaults to 60 seconds.
|
1293
1048
|
|
1294
1049
|
Returns:
|
1295
1050
|
str: The HTTP response body returned by the remote application/service.
|
1296
1051
|
"""
|
1297
|
-
path = f"/appmesh/app/{app_name}/task"
|
1298
1052
|
resp = self._request_http(
|
1299
1053
|
AppMeshClient.Method.POST,
|
1300
|
-
path=
|
1054
|
+
path=f"/appmesh/app/{app_name}/task",
|
1301
1055
|
body=data,
|
1302
1056
|
query={"timeout": str(timeout)},
|
1303
1057
|
)
|
1058
|
+
|
1304
1059
|
if resp.status_code != HTTPStatus.OK:
|
1305
1060
|
raise Exception(resp.text)
|
1306
1061
|
|
@@ -1310,15 +1065,14 @@ class AppMeshClient(metaclass=abc.ABCMeta):
|
|
1310
1065
|
"""Client cancle a running task to a App Mesh application.
|
1311
1066
|
|
1312
1067
|
Args:
|
1313
|
-
app_name
|
1068
|
+
app_name: Name of the target application (as registered in App Mesh).
|
1314
1069
|
|
1315
1070
|
Returns:
|
1316
1071
|
bool: Task exist and cancled status.
|
1317
1072
|
"""
|
1318
|
-
path = f"/appmesh/app/{app_name}/task"
|
1319
1073
|
resp = self._request_http(
|
1320
1074
|
AppMeshClient.Method.DELETE,
|
1321
|
-
path=
|
1075
|
+
path=f"/appmesh/app/{app_name}/task",
|
1322
1076
|
)
|
1323
1077
|
return resp.status_code == HTTPStatus.OK
|
1324
1078
|
|
@@ -1331,15 +1085,15 @@ class AppMeshClient(metaclass=abc.ABCMeta):
|
|
1331
1085
|
"""Run an application asynchronously on a remote system without blocking the API.
|
1332
1086
|
|
1333
1087
|
Args:
|
1334
|
-
app
|
1088
|
+
app: An `App` instance or a shell command string.
|
1335
1089
|
- If `app` is a string, it is treated as a shell command for the remote run,
|
1336
1090
|
and an `App` instance is created as:
|
1337
1091
|
`App({"command": "<command_string>", "shell": True})`.
|
1338
1092
|
- If `app` is an `App` object, providing only the `name` attribute (without
|
1339
1093
|
a command) will run an existing application; otherwise, it is treated as a new application.
|
1340
|
-
max_time_seconds
|
1094
|
+
max_time_seconds: Maximum runtime for the remote process.
|
1341
1095
|
Accepts ISO 8601 duration format (e.g., 'P1Y2M3DT4H5M6S', 'P5W'). Defaults to `P2D`.
|
1342
|
-
life_cycle_seconds
|
1096
|
+
life_cycle_seconds: Maximum lifecycle time for the remote process.
|
1343
1097
|
Accepts ISO 8601 duration format. Defaults to `P2DT12H`.
|
1344
1098
|
|
1345
1099
|
Returns:
|
@@ -1348,56 +1102,63 @@ class AppMeshClient(metaclass=abc.ABCMeta):
|
|
1348
1102
|
if isinstance(app, str):
|
1349
1103
|
app = App({"command": app, "shell": True})
|
1350
1104
|
|
1351
|
-
path = "/appmesh/app/run"
|
1352
1105
|
resp = self._request_http(
|
1353
1106
|
AppMeshClient.Method.POST,
|
1354
1107
|
body=app.json(),
|
1355
|
-
path=
|
1108
|
+
path="/appmesh/app/run",
|
1356
1109
|
query={
|
1357
1110
|
"timeout": str(self._parse_duration(max_time_seconds)),
|
1358
1111
|
"lifecycle": str(self._parse_duration(life_cycle_seconds)),
|
1359
1112
|
},
|
1360
1113
|
)
|
1114
|
+
|
1361
1115
|
if resp.status_code != HTTPStatus.OK:
|
1362
1116
|
raise Exception(resp.text)
|
1363
1117
|
|
1364
|
-
|
1365
|
-
return AppRun(self,
|
1118
|
+
response_data = resp.json()
|
1119
|
+
return AppRun(self, response_data["name"], response_data["process_uuid"])
|
1366
1120
|
|
1367
|
-
def wait_for_async_run(self, run: AppRun, stdout_print: bool = True, timeout: int = 0) -> int:
|
1121
|
+
def wait_for_async_run(self, run: AppRun, stdout_print: bool = True, timeout: int = 0) -> Optional[int]:
|
1368
1122
|
"""Wait for an asynchronous run to finish.
|
1369
1123
|
|
1370
1124
|
Args:
|
1371
|
-
run
|
1372
|
-
stdout_print
|
1373
|
-
timeout
|
1125
|
+
run: asyncrized run result from run_async().
|
1126
|
+
stdout_print: print remote stdout to local or not.
|
1127
|
+
timeout : wait max timeout seconds and return if not finished, 0 means wait until finished
|
1374
1128
|
|
1375
1129
|
Returns:
|
1376
|
-
|
1130
|
+
return exit code if process finished, return None for timeout or exception.
|
1377
1131
|
"""
|
1378
|
-
if run:
|
1379
|
-
|
1380
|
-
|
1381
|
-
|
1382
|
-
|
1383
|
-
|
1384
|
-
|
1385
|
-
|
1386
|
-
|
1387
|
-
|
1388
|
-
|
1389
|
-
|
1390
|
-
|
1391
|
-
|
1392
|
-
|
1393
|
-
|
1394
|
-
|
1395
|
-
|
1396
|
-
|
1397
|
-
|
1398
|
-
|
1399
|
-
|
1400
|
-
|
1132
|
+
if not run:
|
1133
|
+
return None
|
1134
|
+
|
1135
|
+
last_output_position = 0
|
1136
|
+
start = datetime.now()
|
1137
|
+
interval = 1 if self.__class__.__name__ == "AppMeshClient" else 1000
|
1138
|
+
|
1139
|
+
while run.proc_uid:
|
1140
|
+
app_out = self.get_app_output(app_name=run.app_name, stdout_position=last_output_position, stdout_index=0, process_uuid=run.proc_uid, timeout=interval)
|
1141
|
+
|
1142
|
+
if app_out.output and stdout_print:
|
1143
|
+
print(app_out.output, end="", flush=True)
|
1144
|
+
|
1145
|
+
if app_out.out_position is not None:
|
1146
|
+
last_output_position = app_out.out_position
|
1147
|
+
|
1148
|
+
if app_out.exit_code is not None:
|
1149
|
+
# success
|
1150
|
+
with suppress(Exception):
|
1151
|
+
self.delete_app(run.app_name)
|
1152
|
+
return app_out.exit_code
|
1153
|
+
|
1154
|
+
if app_out.status_code != HTTPStatus.OK:
|
1155
|
+
# failed
|
1156
|
+
break
|
1157
|
+
|
1158
|
+
if timeout > 0 and (datetime.now() - start).seconds > timeout:
|
1159
|
+
# timeout
|
1160
|
+
break
|
1161
|
+
|
1401
1162
|
return None
|
1402
1163
|
|
1403
1164
|
def run_app_sync(
|
@@ -1413,101 +1174,98 @@ class AppMeshClient(metaclass=abc.ABCMeta):
|
|
1413
1174
|
If 'app' is App object, the name attribute is used to run an existing application if specified.
|
1414
1175
|
|
1415
1176
|
Args:
|
1416
|
-
app
|
1177
|
+
app: An App instance or a shell command string.
|
1417
1178
|
If a string, an App instance is created as:
|
1418
1179
|
`appmesh.App({"command": "<command_string>", "shell": True})`
|
1419
|
-
stdout_print
|
1420
|
-
max_time_seconds
|
1180
|
+
stdout_print: If True, prints the remote stdout locally. Defaults to True.
|
1181
|
+
max_time_seconds: Maximum runtime for the remote process.
|
1421
1182
|
Supports ISO 8601 duration format (e.g., 'P1Y2M3DT4H5M6S', 'P5W'). Defaults to DEFAULT_RUN_APP_TIMEOUT_SECONDS.
|
1422
|
-
life_cycle_seconds
|
1183
|
+
life_cycle_seconds: Maximum lifecycle time for the remote process.
|
1423
1184
|
Supports ISO 8601 duration format. Defaults to DEFAULT_RUN_APP_LIFECYCLE_SECONDS.
|
1424
1185
|
|
1425
1186
|
Returns:
|
1426
|
-
|
1187
|
+
Exit code of the process (None if unavailable) and the stdout text.
|
1427
1188
|
"""
|
1428
1189
|
if isinstance(app, str):
|
1429
1190
|
app = App({"command": app, "shell": True})
|
1430
1191
|
|
1431
|
-
path = "/appmesh/app/syncrun"
|
1432
1192
|
resp = self._request_http(
|
1433
1193
|
AppMeshClient.Method.POST,
|
1434
1194
|
body=app.json(),
|
1435
|
-
path=
|
1195
|
+
path="/appmesh/app/syncrun",
|
1436
1196
|
query={
|
1437
1197
|
"timeout": str(self._parse_duration(max_time_seconds)),
|
1438
1198
|
"lifecycle": str(self._parse_duration(life_cycle_seconds)),
|
1439
1199
|
},
|
1440
1200
|
)
|
1201
|
+
|
1441
1202
|
exit_code = None
|
1442
1203
|
if resp.status_code == HTTPStatus.OK:
|
1443
1204
|
if stdout_print:
|
1444
1205
|
print(resp.text, end="")
|
1445
1206
|
if "X-Exit-Code" in resp.headers:
|
1446
|
-
exit_code = int(resp.headers
|
1207
|
+
exit_code = int(resp.headers["X-Exit-Code"])
|
1447
1208
|
elif stdout_print:
|
1448
1209
|
print(resp.text)
|
1449
1210
|
|
1450
1211
|
return exit_code, resp.text
|
1451
1212
|
|
1452
|
-
def _request_http(self, method: Method, path: str, query: dict = None, header: dict = None, body=None) -> requests.Response:
|
1453
|
-
"""Make an HTTP request.
|
1454
|
-
|
1455
|
-
Args:
|
1456
|
-
method (Method): AppMeshClient.Method.
|
1457
|
-
path (str): URI patch str.
|
1458
|
-
query (dict, optional): HTTP query parameters.
|
1459
|
-
header (dict, optional): HTTP headers.
|
1460
|
-
body (_type_, optional): object to send in the body of the :class:`Request`.
|
1461
|
-
|
1462
|
-
Returns:
|
1463
|
-
requests.Response: HTTP response
|
1464
|
-
"""
|
1213
|
+
def _request_http(self, method: Method, path: str, query: Optional[dict] = None, header: Optional[dict] = None, body=None) -> requests.Response:
|
1214
|
+
"""Make an HTTP request."""
|
1465
1215
|
rest_url = parse.urljoin(self.auth_server_url, path)
|
1466
1216
|
|
1467
1217
|
# Prepare headers
|
1468
|
-
|
1218
|
+
headers = header.copy() if header else {}
|
1469
1219
|
|
1470
|
-
|
1471
|
-
|
1472
|
-
|
1473
|
-
|
1474
|
-
|
1220
|
+
if self.cookie_file:
|
1221
|
+
# Cookie-based token
|
1222
|
+
csrf_token = self._get_cookie_value(self.session.cookies, self.COOKIE_CSRF_TOKEN)
|
1223
|
+
if csrf_token:
|
1224
|
+
headers[self.HTTP_HEADER_NAME_CSRF_TOKEN] = csrf_token
|
1225
|
+
else:
|
1226
|
+
# Api-based token
|
1227
|
+
access_token = self._get_access_token()
|
1228
|
+
if access_token:
|
1229
|
+
headers[self.HTTP_HEADER_KEY_AUTH] = f"Bearer {access_token}"
|
1475
1230
|
|
1476
|
-
if self.forward_to
|
1477
|
-
|
1478
|
-
|
1479
|
-
|
1480
|
-
|
1481
|
-
|
1231
|
+
if self.forward_to:
|
1232
|
+
target_host = self.forward_to
|
1233
|
+
if ":" not in target_host:
|
1234
|
+
port = parse.urlsplit(self.auth_server_url).port
|
1235
|
+
target_host = f"{target_host}:{port}"
|
1236
|
+
headers[self.HTTP_HEADER_KEY_X_TARGET_HOST] = target_host
|
1237
|
+
|
1238
|
+
headers[self.HTTP_HEADER_KEY_USER_AGENT] = self.HTTP_USER_AGENT
|
1482
1239
|
|
1483
1240
|
# Convert body to JSON string if it's a dict or list
|
1484
1241
|
if isinstance(body, (dict, list)):
|
1485
1242
|
body = json.dumps(body)
|
1486
|
-
|
1243
|
+
headers.setdefault("Content-Type", "application/json")
|
1487
1244
|
|
1488
1245
|
try:
|
1489
|
-
|
1490
|
-
|
1491
|
-
|
1492
|
-
|
1493
|
-
|
1494
|
-
|
1495
|
-
|
1496
|
-
|
1497
|
-
|
1498
|
-
|
1499
|
-
|
1500
|
-
)
|
1501
|
-
elif method
|
1502
|
-
resp = self.session.post(
|
1503
|
-
elif method
|
1504
|
-
resp = self.session.delete(
|
1505
|
-
elif method
|
1506
|
-
resp = self.session.put(
|
1246
|
+
request_kwargs = {
|
1247
|
+
"url": rest_url,
|
1248
|
+
"headers": headers,
|
1249
|
+
"cert": self.ssl_client_cert,
|
1250
|
+
"verify": self.ssl_verify,
|
1251
|
+
"timeout": self.rest_timeout,
|
1252
|
+
}
|
1253
|
+
|
1254
|
+
if method == AppMeshClient.Method.GET:
|
1255
|
+
resp = self.session.get(params=query, **request_kwargs)
|
1256
|
+
elif method == AppMeshClient.Method.POST:
|
1257
|
+
resp = self.session.post(params=query, data=body, **request_kwargs)
|
1258
|
+
elif method == AppMeshClient.Method.POST_STREAM:
|
1259
|
+
resp = self.session.post(params=query, data=body, stream=True, **request_kwargs)
|
1260
|
+
elif method == AppMeshClient.Method.DELETE:
|
1261
|
+
resp = self.session.delete(**request_kwargs)
|
1262
|
+
elif method == AppMeshClient.Method.PUT:
|
1263
|
+
resp = self.session.put(params=query, data=body, **request_kwargs)
|
1507
1264
|
else:
|
1508
1265
|
raise Exception("Invalid http method", method)
|
1509
1266
|
|
1510
1267
|
# Wrap the response for encoding handling
|
1511
1268
|
return AppMeshClient.EncodingResponse(resp)
|
1269
|
+
|
1512
1270
|
except requests.exceptions.RequestException as e:
|
1513
|
-
raise Exception(f"HTTP request failed: {
|
1271
|
+
raise Exception(f"HTTP request failed: {e}") from e
|