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

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