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