appmesh 1.3.7__py3-none-any.whl → 1.3.8__py3-none-any.whl

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