appmesh 1.6.12__tar.gz → 1.6.14__tar.gz
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-1.6.12 → appmesh-1.6.14}/PKG-INFO +2 -1
- {appmesh-1.6.12 → appmesh-1.6.14}/appmesh/app.py +28 -18
- {appmesh-1.6.12 → appmesh-1.6.14}/appmesh/client_http.py +47 -61
- {appmesh-1.6.12 → appmesh-1.6.14}/appmesh/client_http_oauth.py +2 -2
- appmesh-1.6.14/appmesh/client_tcp.py +293 -0
- {appmesh-1.6.12 → appmesh-1.6.14}/appmesh/server_http.py +20 -23
- {appmesh-1.6.12 → appmesh-1.6.14}/appmesh/server_tcp.py +4 -8
- {appmesh-1.6.12 → appmesh-1.6.14}/appmesh/tcp_transport.py +0 -2
- {appmesh-1.6.12 → appmesh-1.6.14}/appmesh.egg-info/PKG-INFO +2 -1
- {appmesh-1.6.12 → appmesh-1.6.14}/appmesh.egg-info/requires.txt +3 -0
- {appmesh-1.6.12 → appmesh-1.6.14}/setup.py +2 -3
- appmesh-1.6.12/appmesh/client_tcp.py +0 -246
- {appmesh-1.6.12 → appmesh-1.6.14}/appmesh/__init__.py +0 -0
- {appmesh-1.6.12 → appmesh-1.6.14}/appmesh/app_output.py +0 -0
- {appmesh-1.6.12 → appmesh-1.6.14}/appmesh/app_run.py +0 -0
- {appmesh-1.6.12 → appmesh-1.6.14}/appmesh/appmesh_client.py +0 -0
- {appmesh-1.6.12 → appmesh-1.6.14}/appmesh/tcp_messages.py +0 -0
- {appmesh-1.6.12 → appmesh-1.6.14}/appmesh.egg-info/SOURCES.txt +0 -0
- {appmesh-1.6.12 → appmesh-1.6.14}/appmesh.egg-info/dependency_links.txt +0 -0
- {appmesh-1.6.12 → appmesh-1.6.14}/appmesh.egg-info/top_level.txt +0 -0
- {appmesh-1.6.12 → appmesh-1.6.14}/pyproject.toml +0 -0
- {appmesh-1.6.12 → appmesh-1.6.14}/setup.cfg +0 -0
- {appmesh-1.6.12 → appmesh-1.6.14}/test/test_appmesh_client.py +0 -0
- {appmesh-1.6.12 → appmesh-1.6.14}/test/test_oauth2.py +0 -0
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: appmesh
|
3
|
-
Version: 1.6.
|
3
|
+
Version: 1.6.14
|
4
4
|
Summary: Client SDK for App Mesh
|
5
5
|
Home-page: https://github.com/laoshanxi/app-mesh
|
6
6
|
Author: laoshanxi
|
@@ -17,6 +17,7 @@ Requires-Dist: msgpack
|
|
17
17
|
Requires-Dist: requests_toolbelt
|
18
18
|
Requires-Dist: aniso8601
|
19
19
|
Requires-Dist: PyJWT
|
20
|
+
Requires-Dist: dataclasses; python_version < "3.7"
|
20
21
|
Dynamic: author
|
21
22
|
Dynamic: author-email
|
22
23
|
Dynamic: classifier
|
@@ -134,75 +134,85 @@ class App:
|
|
134
134
|
|
135
135
|
# Application configuration
|
136
136
|
self.name = _get_str(data, "name")
|
137
|
-
"""
|
137
|
+
"""app name (unique)"""
|
138
138
|
self.command = _get_str(data, "command")
|
139
139
|
"""full command line with arguments"""
|
140
140
|
self.shell = _get_bool(data, "shell")
|
141
|
-
"""
|
141
|
+
"""Whether run command in shell mode (enables shell syntax such as pipes and compound commands)"""
|
142
142
|
self.session_login = _get_bool(data, "session_login")
|
143
|
-
"""
|
143
|
+
"""Whether to run the app in session login mode (inheriting the user's full login environment)"""
|
144
144
|
self.description = _get_str(data, "description")
|
145
|
-
"""
|
145
|
+
"""app description string"""
|
146
146
|
self.metadata = _get_item(data, "metadata")
|
147
|
-
"""metadata string/JSON (input for
|
147
|
+
"""metadata string/JSON (input for app, pass to process stdin)"""
|
148
148
|
self.working_dir = _get_str(data, "working_dir")
|
149
149
|
"""working directory"""
|
150
150
|
self.status = _get_int(data, "status")
|
151
|
-
"""
|
151
|
+
"""app status: 1 for enabled, 0 for disabled"""
|
152
152
|
self.docker_image = _get_str(data, "docker_image")
|
153
|
-
"""
|
153
|
+
"""Docker image for containerized execution"""
|
154
154
|
self.stdout_cache_num = _get_int(data, "stdout_cache_num")
|
155
|
-
"""stdout
|
155
|
+
"""maximum number of stdout log files to retain"""
|
156
156
|
self.start_time = _get_int(data, "start_time")
|
157
157
|
"""start date time for app (ISO8601 time format, e.g., '2020-10-11T09:22:05')"""
|
158
158
|
self.end_time = _get_int(data, "end_time")
|
159
159
|
"""end date time for app (ISO8601 time format, e.g., '2020-10-11T10:22:05')"""
|
160
|
-
self.
|
160
|
+
self.start_interval_seconds = _get_int(data, "start_interval_seconds")
|
161
161
|
"""start interval seconds for short running app, support ISO 8601 durations and cron expression (e.g., 'P1Y2M3DT4H5M6S' 'P5W' '* */5 * * * *')"""
|
162
162
|
self.cron = _get_bool(data, "cron")
|
163
|
-
"""
|
163
|
+
"""Whether the interval is specified as a cron expression"""
|
164
164
|
self.daily_limitation = App.DailyLimitation(_get_item(data, "daily_limitation"))
|
165
165
|
self.retention = _get_str(data, "retention")
|
166
166
|
"""extra timeout seconds for stopping current process, support ISO 8601 durations (e.g., 'P1Y2M3DT4H5M6S' 'P5W')."""
|
167
167
|
self.health_check_cmd = _get_str(data, "health_check_cmd")
|
168
168
|
"""health check script command (e.g., sh -x 'curl host:port/health', return 0 is health)"""
|
169
169
|
self.permission = _get_int(data, "permission")
|
170
|
-
"""
|
170
|
+
"""app user permission, value is 2 bit integer: [group & other], each bit can be deny:1, read:2, write: 3."""
|
171
171
|
self.behavior = App.Behavior(_get_item(data, "behavior"))
|
172
172
|
|
173
173
|
self.env = data.get("env", {}) if data else {}
|
174
174
|
"""environment variables (e.g., -e env1=value1 -e env2=value2, APP_DOCKER_OPTS is used to input docker run parameters)"""
|
175
175
|
self.sec_env = data.get("sec_env", {}) if data else {}
|
176
|
-
"""security environment variables, encrypt in server side with
|
176
|
+
"""security environment variables, encrypt in server side with app owner's cipher"""
|
177
177
|
self.pid = _get_int(data, "pid")
|
178
178
|
"""process id used to attach to the running process"""
|
179
179
|
self.resource_limit = App.ResourceLimitation(_get_item(data, "resource_limit"))
|
180
180
|
|
181
181
|
# Read-only attributes
|
182
|
+
self.register_time = _get_int(data, "register_time")
|
183
|
+
"""app register time"""
|
184
|
+
self.starts = _get_int(data, "starts")
|
185
|
+
"""number of times started"""
|
182
186
|
self.owner = _get_str(data, "owner")
|
183
|
-
"""owner name"""
|
187
|
+
"""owner name of app mesh user who created the app"""
|
184
188
|
self.user = _get_str(data, "pid_user")
|
185
|
-
"""process user name"""
|
189
|
+
"""process OS user name"""
|
186
190
|
self.pstree = _get_str(data, "pstree")
|
187
191
|
"""process tree"""
|
188
192
|
self.container_id = _get_str(data, "container_id")
|
189
|
-
"""container id"""
|
193
|
+
"""docker container id"""
|
190
194
|
self.memory = _get_int(data, "memory")
|
191
195
|
"""memory usage"""
|
192
196
|
self.cpu = _get_int(data, "cpu")
|
193
197
|
"""cpu usage"""
|
194
198
|
self.fd = _get_int(data, "fd")
|
195
199
|
"""file descriptor usage"""
|
200
|
+
self.stdout_cache_size = _get_int(data, "stdout_cache_size")
|
201
|
+
"""number of stdout log files currently retained"""
|
196
202
|
self.last_start_time = _get_int(data, "last_start_time")
|
197
203
|
"""last start time"""
|
198
204
|
self.last_exit_time = _get_int(data, "last_exit_time")
|
199
205
|
"""last exit time"""
|
206
|
+
self.last_error = _get_str(data, "last_error")
|
207
|
+
"""last error message"""
|
208
|
+
self.next_start_time = _get_int(data, "next_start_time")
|
209
|
+
"""next start time"""
|
200
210
|
self.health = _get_int(data, "health")
|
201
|
-
"""health status"""
|
211
|
+
"""health status: 0 for healthy, 1 for unhealthy"""
|
202
212
|
self.version = _get_int(data, "version")
|
203
|
-
"""version
|
213
|
+
"""app version"""
|
204
214
|
self.return_code = _get_int(data, "return_code")
|
205
|
-
"""last exit code"""
|
215
|
+
"""last process exit code"""
|
206
216
|
self.task_id = _get_int(data, "task_id")
|
207
217
|
"""current task id"""
|
208
218
|
self.task_status = _get_str(data, "task_status")
|
@@ -12,6 +12,7 @@ import os
|
|
12
12
|
import sys
|
13
13
|
import threading
|
14
14
|
import time
|
15
|
+
from pathlib import Path
|
15
16
|
from datetime import datetime
|
16
17
|
from enum import Enum, unique
|
17
18
|
from http import HTTPStatus
|
@@ -39,17 +40,6 @@ class AppMeshClient(metaclass=abc.ABCMeta):
|
|
39
40
|
|
40
41
|
This client is designed for direct usage in applications that require access to App Mesh services over HTTP-based REST.
|
41
42
|
|
42
|
-
Usage:
|
43
|
-
- Install the App Mesh Python package:
|
44
|
-
python3 -m pip install --upgrade appmesh
|
45
|
-
- Import the client module:
|
46
|
-
from appmesh import AppMeshClient
|
47
|
-
|
48
|
-
Example:
|
49
|
-
client = AppMeshClient()
|
50
|
-
client.login("your-name", "your-password")
|
51
|
-
response = client.app_view(app_name='ping')
|
52
|
-
|
53
43
|
Attributes:
|
54
44
|
- TLS (Transport Layer Security): Supports secure connections between the client and App Mesh service,
|
55
45
|
ensuring encrypted communication.
|
@@ -112,6 +102,13 @@ class AppMeshClient(metaclass=abc.ABCMeta):
|
|
112
102
|
- update_role()
|
113
103
|
- view_roles()
|
114
104
|
- view_groups()
|
105
|
+
|
106
|
+
Example:
|
107
|
+
>>> python -m pip install --upgrade appmesh
|
108
|
+
>>> from appmesh import AppMeshClient
|
109
|
+
>>> client = AppMeshClient()
|
110
|
+
>>> client.login("your-name", "your-password")
|
111
|
+
>>> response = client.app_view(app_name='ping')
|
115
112
|
"""
|
116
113
|
|
117
114
|
DURATION_ONE_WEEK_ISO = "P1W"
|
@@ -121,10 +118,10 @@ class AppMeshClient(metaclass=abc.ABCMeta):
|
|
121
118
|
TOKEN_REFRESH_OFFSET = 30 # 30s before token expire to refresh token
|
122
119
|
|
123
120
|
# Platform-aware default SSL paths
|
124
|
-
_DEFAULT_SSL_DIR = "
|
125
|
-
DEFAULT_SSL_CA_CERT_PATH =
|
126
|
-
DEFAULT_SSL_CLIENT_CERT_PATH =
|
127
|
-
DEFAULT_SSL_CLIENT_KEY_PATH =
|
121
|
+
_DEFAULT_SSL_DIR = Path("C:/local/appmesh/ssl") if os.name == "nt" else Path("/opt/appmesh/ssl")
|
122
|
+
DEFAULT_SSL_CA_CERT_PATH = str(_DEFAULT_SSL_DIR / "ca.pem")
|
123
|
+
DEFAULT_SSL_CLIENT_CERT_PATH = str(_DEFAULT_SSL_DIR / "client.pem")
|
124
|
+
DEFAULT_SSL_CLIENT_KEY_PATH = str(_DEFAULT_SSL_DIR / "client-key.pem")
|
128
125
|
|
129
126
|
DEFAULT_JWT_AUDIENCE = "appmesh-service"
|
130
127
|
|
@@ -199,39 +196,28 @@ class AppMeshClient(metaclass=abc.ABCMeta):
|
|
199
196
|
def __init__(
|
200
197
|
self,
|
201
198
|
rest_url: str = "https://127.0.0.1:6060",
|
202
|
-
rest_ssl_verify
|
203
|
-
rest_ssl_client_cert=(DEFAULT_SSL_CLIENT_CERT_PATH, DEFAULT_SSL_CLIENT_KEY_PATH)
|
204
|
-
rest_timeout=(60, 300),
|
199
|
+
rest_ssl_verify: Union[bool, str] = DEFAULT_SSL_CA_CERT_PATH,
|
200
|
+
rest_ssl_client_cert: Optional[Union[str, Tuple[str, str]]] = (DEFAULT_SSL_CLIENT_CERT_PATH, DEFAULT_SSL_CLIENT_KEY_PATH),
|
201
|
+
rest_timeout: Tuple[float, float] = (60, 300),
|
205
202
|
jwt_token: Optional[str] = None,
|
206
203
|
rest_cookie_file: Optional[str] = None,
|
207
|
-
auto_refresh_token=False,
|
204
|
+
auto_refresh_token: bool = False,
|
208
205
|
):
|
209
206
|
"""Initialize an App Mesh HTTP client for interacting with the App Mesh server via secure HTTPS.
|
210
207
|
|
211
208
|
Args:
|
212
|
-
rest_url
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
rest_timeout (tuple, optional): HTTP connection timeouts for API requests, as `(connect_timeout, read_timeout)`.
|
226
|
-
The default is `(60, 300)`, where `60` seconds is the maximum time to establish a connection and `300` seconds for the maximum read duration.
|
227
|
-
|
228
|
-
rest_cookie_file (str, optional): Path to a file for storing session cookies.
|
229
|
-
If provided, cookies will be saved to and loaded from this file to maintain session state across client instances instead of keep jwt_token.
|
230
|
-
|
231
|
-
jwt_token (str, optional): JWT token for API authentication, used in headers to authorize requests where required.
|
232
|
-
auto_refresh_token (bool, optional): Enable automatic token refresh before expiration.
|
233
|
-
When enabled, a background timer will monitor token expiration and attempt to refresh
|
234
|
-
the token before it expires. This works with both native App Mesh tokens and Keycloak tokens.
|
209
|
+
rest_url: Base server URI (protocol, host, port). Default: `"https://127.0.0.1:6060"`.
|
210
|
+
rest_ssl_verify: SSL verification mode for HTTPS requests:
|
211
|
+
- True: Use system CAs.
|
212
|
+
- False: Disable verification (insecure).
|
213
|
+
- 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: Path to single PEM with cert+key
|
216
|
+
- tuple: (cert_path, key_path)
|
217
|
+
rest_timeout: Timeouts `(connect, read)` in seconds. Default `(60, 300)`.
|
218
|
+
rest_cookie_file: Cookie file path for session persistence (alternative to jwt_token).
|
219
|
+
jwt_token: JWT token for API authentication, overrides cookie file if both provided.
|
220
|
+
auto_refresh_token: Auto-refresh JWT before expiry (supports App Mesh and Keycloak tokens).
|
235
221
|
"""
|
236
222
|
self._ensure_logging_configured()
|
237
223
|
self.auth_server_url = rest_url
|
@@ -1202,13 +1188,13 @@ class AppMeshClient(metaclass=abc.ABCMeta):
|
|
1202
1188
|
########################################
|
1203
1189
|
# File management
|
1204
1190
|
########################################
|
1205
|
-
def download_file(self, remote_file: str, local_file: str,
|
1191
|
+
def download_file(self, remote_file: str, local_file: str, preserve_permissions: bool = True) -> None:
|
1206
1192
|
"""Download a remote file to the local system. Optionally, the local file will have the same permission as the remote file.
|
1207
1193
|
|
1208
1194
|
Args:
|
1209
1195
|
remote_file (str): the remote file path.
|
1210
1196
|
local_file (str): the local file path to be downloaded.
|
1211
|
-
|
1197
|
+
preserve_permissions (bool): whether to apply file attributes (permissions, owner, group) to the local file.
|
1212
1198
|
"""
|
1213
1199
|
resp = self._request_http(AppMeshClient.Method.GET, path="/appmesh/file/download", header={self.HTTP_HEADER_KEY_X_FILE_PATH: remote_file})
|
1214
1200
|
resp.raise_for_status()
|
@@ -1220,7 +1206,7 @@ class AppMeshClient(metaclass=abc.ABCMeta):
|
|
1220
1206
|
fp.write(chunk)
|
1221
1207
|
|
1222
1208
|
# Apply file attributes (permissions, owner, group) if requested
|
1223
|
-
if
|
1209
|
+
if preserve_permissions and sys.platform != "win32":
|
1224
1210
|
if "X-File-Mode" in resp.headers:
|
1225
1211
|
os.chmod(path=local_file, mode=int(resp.headers["X-File-Mode"]))
|
1226
1212
|
if "X-File-User" in resp.headers and "X-File-Group" in resp.headers:
|
@@ -1231,7 +1217,7 @@ class AppMeshClient(metaclass=abc.ABCMeta):
|
|
1231
1217
|
except PermissionError:
|
1232
1218
|
logging.warning(f"Warning: Unable to change owner/group of {local_file}. Operation requires elevated privileges.")
|
1233
1219
|
|
1234
|
-
def upload_file(self, local_file: str, remote_file: str,
|
1220
|
+
def upload_file(self, local_file: str, remote_file: str, preserve_permissions: bool = True) -> None:
|
1235
1221
|
"""Upload a local file to the remote server. Optionally, the remote file will have the same permission as the local file.
|
1236
1222
|
|
1237
1223
|
Dependency:
|
@@ -1241,7 +1227,7 @@ class AppMeshClient(metaclass=abc.ABCMeta):
|
|
1241
1227
|
Args:
|
1242
1228
|
local_file (str): the local file path.
|
1243
1229
|
remote_file (str): the target remote file to be uploaded.
|
1244
|
-
|
1230
|
+
preserve_permissions (bool): whether to upload file attributes (permissions, owner, group) along with the file.
|
1245
1231
|
"""
|
1246
1232
|
if not os.path.exists(local_file):
|
1247
1233
|
raise FileNotFoundError(f"Local file not found: {local_file}")
|
@@ -1253,7 +1239,7 @@ class AppMeshClient(metaclass=abc.ABCMeta):
|
|
1253
1239
|
header = {self.HTTP_HEADER_KEY_X_FILE_PATH: parse.quote(remote_file), "Content-Type": encoder.content_type}
|
1254
1240
|
|
1255
1241
|
# Include file attributes (permissions, owner, group) if requested
|
1256
|
-
if
|
1242
|
+
if preserve_permissions:
|
1257
1243
|
file_stat = os.stat(local_file)
|
1258
1244
|
header["X-File-Mode"] = str(file_stat.st_mode & 0o777) # Mask to keep only permission bits
|
1259
1245
|
header["X-File-User"] = str(file_stat.st_uid)
|
@@ -1486,24 +1472,24 @@ class AppMeshClient(metaclass=abc.ABCMeta):
|
|
1486
1472
|
header.setdefault("Content-Type", "application/json")
|
1487
1473
|
|
1488
1474
|
try:
|
1475
|
+
request_kwargs = {
|
1476
|
+
"url": rest_url,
|
1477
|
+
"headers": header,
|
1478
|
+
"cert": self.ssl_client_cert,
|
1479
|
+
"verify": self.ssl_verify,
|
1480
|
+
"timeout": self.rest_timeout,
|
1481
|
+
}
|
1482
|
+
|
1489
1483
|
if method is AppMeshClient.Method.GET:
|
1490
|
-
resp = self.session.get(
|
1484
|
+
resp = self.session.get(params=query, **request_kwargs)
|
1491
1485
|
elif method is AppMeshClient.Method.POST:
|
1492
|
-
resp = self.session.post(
|
1493
|
-
url=rest_url,
|
1494
|
-
params=query,
|
1495
|
-
headers=header,
|
1496
|
-
data=body,
|
1497
|
-
cert=self.ssl_client_cert,
|
1498
|
-
verify=self.ssl_verify,
|
1499
|
-
timeout=self.rest_timeout,
|
1500
|
-
)
|
1486
|
+
resp = self.session.post(params=query, data=body, **request_kwargs)
|
1501
1487
|
elif method is AppMeshClient.Method.POST_STREAM:
|
1502
|
-
resp = self.session.post(
|
1488
|
+
resp = self.session.post(params=query, data=body, stream=True, **request_kwargs)
|
1503
1489
|
elif method is AppMeshClient.Method.DELETE:
|
1504
|
-
resp = self.session.delete(
|
1490
|
+
resp = self.session.delete(**request_kwargs)
|
1505
1491
|
elif method is AppMeshClient.Method.PUT:
|
1506
|
-
resp = self.session.put(
|
1492
|
+
resp = self.session.put(params=query, data=body, **request_kwargs)
|
1507
1493
|
else:
|
1508
1494
|
raise Exception("Invalid http method", method)
|
1509
1495
|
|
@@ -18,8 +18,8 @@ class AppMeshClientOAuth(AppMeshClient):
|
|
18
18
|
self,
|
19
19
|
oauth2: dict, # Required for Keycloak
|
20
20
|
rest_url: str = "https://127.0.0.1:6060",
|
21
|
-
rest_ssl_verify=AppMeshClient.DEFAULT_SSL_CA_CERT_PATH
|
22
|
-
rest_ssl_client_cert=
|
21
|
+
rest_ssl_verify=AppMeshClient.DEFAULT_SSL_CA_CERT_PATH,
|
22
|
+
rest_ssl_client_cert=AppMeshClient.DEFAULT_SSL_CLIENT_CERT_PATH,
|
23
23
|
rest_timeout=(60, 300),
|
24
24
|
jwt_token=None, # Keycloak dict
|
25
25
|
auto_refresh_token: bool = True, # Default to True for Keycloak
|
@@ -0,0 +1,293 @@
|
|
1
|
+
# client_tcp.py
|
2
|
+
# pylint: disable=line-too-long,broad-exception-raised,broad-exception-caught,import-outside-toplevel,protected-access
|
3
|
+
|
4
|
+
# Standard library imports
|
5
|
+
import json
|
6
|
+
import os
|
7
|
+
import socket
|
8
|
+
import sys
|
9
|
+
import uuid
|
10
|
+
from typing import Optional, Tuple, Union
|
11
|
+
|
12
|
+
# Third-party imports
|
13
|
+
import requests
|
14
|
+
|
15
|
+
# Local imports
|
16
|
+
from .client_http import AppMeshClient
|
17
|
+
from .tcp_messages import RequestMessage, ResponseMessage
|
18
|
+
from .tcp_transport import TCPTransport
|
19
|
+
|
20
|
+
|
21
|
+
class AppMeshClientTCP(AppMeshClient):
|
22
|
+
"""Client SDK for interacting with the App Mesh service over TCP.
|
23
|
+
|
24
|
+
The `AppMeshClientTCP` class extends the functionality of `AppMeshClient` by offering a TCP-based communication layer
|
25
|
+
for the App Mesh REST API. It overrides the file download and upload methods to support large file transfers with
|
26
|
+
improved performance, leveraging TCP for lower latency and higher throughput compared to HTTP.
|
27
|
+
|
28
|
+
This client is suitable for applications requiring efficient data transfers and high-throughput operations within the
|
29
|
+
App Mesh ecosystem, while maintaining compatibility with all other attributes and methods from `AppMeshClient`.
|
30
|
+
|
31
|
+
Attributes:
|
32
|
+
Inherits all attributes from `AppMeshClient`, including TLS secure connections and JWT-based authentication.
|
33
|
+
|
34
|
+
Methods:
|
35
|
+
- download_file()
|
36
|
+
- upload_file()
|
37
|
+
- Inherits all other methods from `AppMeshClient`, providing a consistent interface for managing applications within App Mesh.
|
38
|
+
|
39
|
+
Example:
|
40
|
+
>>> from appmesh import AppMeshClientTCP
|
41
|
+
>>> client = AppMeshClientTCP()
|
42
|
+
>>> client.login("your-name", "your-password")
|
43
|
+
>>> client.download_file("/tmp/os-release", "os-release")
|
44
|
+
"""
|
45
|
+
|
46
|
+
# TLS-optimized chunk size, leaves room for TLS overhead within the 16 KB limit
|
47
|
+
TCP_BLOCK_SIZE = 16 * 1024 - 128
|
48
|
+
ENCODING_UTF8 = "utf-8"
|
49
|
+
HTTP_USER_AGENT_TCP = "appmesh/python/tcp"
|
50
|
+
HTTP_HEADER_KEY_X_SEND_FILE_SOCKET = "X-Send-File-Socket"
|
51
|
+
HTTP_HEADER_KEY_X_RECV_FILE_SOCKET = "X-Recv-File-Socket"
|
52
|
+
|
53
|
+
def __init__(
|
54
|
+
self,
|
55
|
+
rest_ssl_verify: Union[bool, str] = AppMeshClient.DEFAULT_SSL_CA_CERT_PATH,
|
56
|
+
rest_ssl_client_cert: Optional[Union[str, Tuple[str, str]]] = None,
|
57
|
+
jwt_token: Optional[str] = None,
|
58
|
+
tcp_address: Tuple[str, int] = ("127.0.0.1", 6059),
|
59
|
+
):
|
60
|
+
"""Construct an App Mesh client TCP object to communicate securely with an App Mesh server over TLS.
|
61
|
+
|
62
|
+
Args:
|
63
|
+
rest_ssl_verify: SSL certificate verification behavior. Can be True, False, or a path to CA bundle.
|
64
|
+
- True: Use system CA certificates (e.g., /etc/ssl/certs/ on Linux)
|
65
|
+
- False: Disable verification (insecure)
|
66
|
+
- str: Path to custom CA bundle or directory
|
67
|
+
ssl_client_cert: SSL client certificate:
|
68
|
+
- str: Path to single PEM with cert+key
|
69
|
+
- tuple: (cert_path, key_path)
|
70
|
+
jwt_token: Pre-configured JWT Token for authenticating requests.
|
71
|
+
tcp_address: Server address as (host, port) tuple, defaults to ("127.0.0.1", 6059).
|
72
|
+
|
73
|
+
Note:
|
74
|
+
TCP connections require an explicit full-chain CA specification for certificate validation,
|
75
|
+
unlike HTTP, which can retrieve intermediate certificates automatically.
|
76
|
+
"""
|
77
|
+
self.tcp_transport = TCPTransport(address=tcp_address, ssl_verify=rest_ssl_verify, ssl_client_cert=rest_ssl_client_cert)
|
78
|
+
super().__init__(rest_ssl_verify=rest_ssl_verify, rest_ssl_client_cert=rest_ssl_client_cert, jwt_token=jwt_token)
|
79
|
+
|
80
|
+
def close(self):
|
81
|
+
"""Close the connection and release resources."""
|
82
|
+
if hasattr(self, "tcp_transport") and self.tcp_transport:
|
83
|
+
self.tcp_transport.close()
|
84
|
+
self.tcp_transport = None
|
85
|
+
return super().close()
|
86
|
+
|
87
|
+
def __del__(self):
|
88
|
+
"""Ensure resources are properly released when the object is garbage collected."""
|
89
|
+
try:
|
90
|
+
self.close()
|
91
|
+
except Exception:
|
92
|
+
pass # Never raise in __del__
|
93
|
+
|
94
|
+
def _covert_bytes(self, body) -> bytes:
|
95
|
+
"""Prepare request body for transmission."""
|
96
|
+
if body is None:
|
97
|
+
return b""
|
98
|
+
|
99
|
+
if isinstance(body, (bytes, bytearray, memoryview)):
|
100
|
+
return body
|
101
|
+
|
102
|
+
if isinstance(body, str):
|
103
|
+
return body.encode(self.ENCODING_UTF8)
|
104
|
+
|
105
|
+
if isinstance(body, (dict, list)):
|
106
|
+
return json.dumps(body).encode(self.ENCODING_UTF8)
|
107
|
+
|
108
|
+
raise TypeError(f"Unsupported body type: {type(body)}")
|
109
|
+
|
110
|
+
def _request_http(self, method: AppMeshClient.Method, path: str, query: Optional[dict] = None, header: Optional[dict] = None, body=None) -> requests.Response:
|
111
|
+
"""Send HTTP request over TCP transport.
|
112
|
+
|
113
|
+
Args:
|
114
|
+
method: HTTP method.
|
115
|
+
path: URI path.
|
116
|
+
query: Query parameters.
|
117
|
+
header: HTTP headers.
|
118
|
+
body: Request body.
|
119
|
+
|
120
|
+
Returns:
|
121
|
+
Simulated HTTP response.
|
122
|
+
"""
|
123
|
+
|
124
|
+
# Check for unsupported features
|
125
|
+
if super().forward_to:
|
126
|
+
raise RuntimeError("Forward request is not supported in TCP mode")
|
127
|
+
|
128
|
+
if not self.tcp_transport.connected():
|
129
|
+
self.tcp_transport.connect()
|
130
|
+
|
131
|
+
# Prepare request message (ensure no fields are assigned None!)
|
132
|
+
appmesh_request = RequestMessage()
|
133
|
+
appmesh_request.uuid = str(uuid.uuid1())
|
134
|
+
appmesh_request.http_method = method.value
|
135
|
+
appmesh_request.request_uri = path
|
136
|
+
appmesh_request.client_addr = socket.gethostname()
|
137
|
+
appmesh_request.headers[self.HTTP_HEADER_KEY_USER_AGENT] = self.HTTP_USER_AGENT_TCP
|
138
|
+
|
139
|
+
# Add authentication token
|
140
|
+
token = self._get_access_token()
|
141
|
+
if token:
|
142
|
+
appmesh_request.headers[self.HTTP_HEADER_KEY_AUTH] = token
|
143
|
+
|
144
|
+
# Add custom headers
|
145
|
+
if header:
|
146
|
+
appmesh_request.headers.update(header)
|
147
|
+
|
148
|
+
# Add query parameters
|
149
|
+
if query:
|
150
|
+
appmesh_request.query.update(query)
|
151
|
+
|
152
|
+
# Prepare body
|
153
|
+
body_bytes = self._covert_bytes(body)
|
154
|
+
if body_bytes:
|
155
|
+
appmesh_request.body = body_bytes
|
156
|
+
|
157
|
+
# Send request
|
158
|
+
data = appmesh_request.serialize()
|
159
|
+
self.tcp_transport.send_message(data)
|
160
|
+
|
161
|
+
# Receive response
|
162
|
+
resp_data = self.tcp_transport.receive_message()
|
163
|
+
if not resp_data: # Covers None and empty bytes
|
164
|
+
self.tcp_transport.close()
|
165
|
+
raise ConnectionError("Socket connection broken")
|
166
|
+
|
167
|
+
# Parse response
|
168
|
+
appmesh_resp = ResponseMessage().deserialize(resp_data)
|
169
|
+
response = requests.Response()
|
170
|
+
response.status_code = appmesh_resp.http_status
|
171
|
+
response.headers = appmesh_resp.headers
|
172
|
+
|
173
|
+
# Set response content
|
174
|
+
# response.encoding = self.ENCODING_UTF8 # only need when charset not in appmesh_resp.body_msg_type
|
175
|
+
if isinstance(appmesh_resp.body, bytes):
|
176
|
+
response._content = appmesh_resp.body
|
177
|
+
else:
|
178
|
+
response._content = str(appmesh_resp.body).encode(self.ENCODING_UTF8)
|
179
|
+
|
180
|
+
# Set content type
|
181
|
+
if appmesh_resp.body_msg_type:
|
182
|
+
response.headers["Content-Type"] = appmesh_resp.body_msg_type
|
183
|
+
|
184
|
+
return AppMeshClient.EncodingResponse(response)
|
185
|
+
|
186
|
+
def _apply_file_attributes(self, local_file: str, headers: dict) -> None:
|
187
|
+
"""Apply file attributes from headers to local file."""
|
188
|
+
if sys.platform == "win32":
|
189
|
+
return
|
190
|
+
|
191
|
+
# Apply file mode
|
192
|
+
if "X-File-Mode" in headers:
|
193
|
+
try:
|
194
|
+
os.chmod(local_file, int(headers["X-File-Mode"]))
|
195
|
+
except (ValueError, OSError) as e:
|
196
|
+
self._logger.warning("Failed to set file mode: %s", e)
|
197
|
+
|
198
|
+
# Apply file ownership
|
199
|
+
if "X-File-User" in headers and "X-File-Group" in headers:
|
200
|
+
try:
|
201
|
+
file_uid = int(headers["X-File-User"])
|
202
|
+
file_gid = int(headers["X-File-Group"])
|
203
|
+
os.chown(local_file, uid=file_uid, gid=file_gid)
|
204
|
+
except (ValueError, PermissionError, OSError) as e:
|
205
|
+
if isinstance(e, PermissionError):
|
206
|
+
print(f"Warning: Unable to change owner/group of {local_file}. " "Operation requires elevated privileges.")
|
207
|
+
else:
|
208
|
+
self._logger.warning("Failed to set file ownership: %s", e)
|
209
|
+
|
210
|
+
def _get_file_attributes(self, local_file: str) -> dict:
|
211
|
+
"""Get file attributes as header dictionary."""
|
212
|
+
if sys.platform == "win32":
|
213
|
+
return {}
|
214
|
+
|
215
|
+
try:
|
216
|
+
file_stat = os.stat(local_file)
|
217
|
+
return {
|
218
|
+
"X-File-Mode": str(file_stat.st_mode & 0o777),
|
219
|
+
"X-File-User": str(file_stat.st_uid),
|
220
|
+
"X-File-Group": str(file_stat.st_gid),
|
221
|
+
}
|
222
|
+
except OSError as e:
|
223
|
+
self._logger.warning("Failed to get file attributes: %s", e)
|
224
|
+
return {}
|
225
|
+
|
226
|
+
def download_file(self, remote_file: str, local_file: str, preserve_permissions: bool = True) -> None:
|
227
|
+
"""Copy a remote file to local, preserving file attributes if requested.
|
228
|
+
|
229
|
+
Args:
|
230
|
+
remote_file: Remote file path.
|
231
|
+
local_file: Local destination path.
|
232
|
+
preserve_permissions: Apply remote file permissions/ownership locally.
|
233
|
+
"""
|
234
|
+
header = {
|
235
|
+
AppMeshClient.HTTP_HEADER_KEY_X_FILE_PATH: remote_file,
|
236
|
+
self.HTTP_HEADER_KEY_X_RECV_FILE_SOCKET: "true",
|
237
|
+
}
|
238
|
+
|
239
|
+
resp = self._request_http(AppMeshClient.Method.GET, path="/appmesh/file/download", header=header)
|
240
|
+
resp.raise_for_status()
|
241
|
+
|
242
|
+
if self.HTTP_HEADER_KEY_X_RECV_FILE_SOCKET not in resp.headers:
|
243
|
+
raise ValueError(f"Server did not respond with socket transfer option: " f"{self.HTTP_HEADER_KEY_X_RECV_FILE_SOCKET}")
|
244
|
+
|
245
|
+
# Download file chunks
|
246
|
+
with open(local_file, "wb") as fp:
|
247
|
+
while True:
|
248
|
+
chunk_data = self.tcp_transport.receive_message()
|
249
|
+
if not chunk_data:
|
250
|
+
break
|
251
|
+
fp.write(chunk_data)
|
252
|
+
|
253
|
+
# Apply file attributes if requested
|
254
|
+
if preserve_permissions:
|
255
|
+
self._apply_file_attributes(local_file, resp.headers)
|
256
|
+
|
257
|
+
def upload_file(self, local_file: str, remote_file: str, preserve_permissions: bool = True) -> None:
|
258
|
+
"""Upload a local file to remote server, preserving file attributes if requested.
|
259
|
+
|
260
|
+
Args:
|
261
|
+
local_file: Local file path.
|
262
|
+
remote_file: Remote destination path.
|
263
|
+
preserve_permissions: Upload file permissions/ownership metadata.
|
264
|
+
"""
|
265
|
+
if not os.path.exists(local_file):
|
266
|
+
raise FileNotFoundError(f"Local file not found: {local_file}")
|
267
|
+
|
268
|
+
# Prepare headers
|
269
|
+
header = {
|
270
|
+
AppMeshClient.HTTP_HEADER_KEY_X_FILE_PATH: remote_file,
|
271
|
+
"Content-Type": "text/plain",
|
272
|
+
self.HTTP_HEADER_KEY_X_SEND_FILE_SOCKET: "true",
|
273
|
+
}
|
274
|
+
|
275
|
+
# Add file attributes if requested
|
276
|
+
if preserve_permissions:
|
277
|
+
header.update(self._get_file_attributes(local_file))
|
278
|
+
|
279
|
+
# Initiate upload
|
280
|
+
resp = self._request_http(AppMeshClient.Method.POST, path="/appmesh/file/upload", header=header)
|
281
|
+
resp.raise_for_status()
|
282
|
+
|
283
|
+
if self.HTTP_HEADER_KEY_X_SEND_FILE_SOCKET not in resp.headers:
|
284
|
+
raise ValueError(f"Server did not respond with socket transfer option: " f"{self.HTTP_HEADER_KEY_X_SEND_FILE_SOCKET}")
|
285
|
+
|
286
|
+
# Upload file chunks
|
287
|
+
with open(local_file, "rb") as fp:
|
288
|
+
while True:
|
289
|
+
chunk_data = fp.read(self.TCP_BLOCK_SIZE)
|
290
|
+
if not chunk_data:
|
291
|
+
self.tcp_transport.send_message([]) # EOF signal
|
292
|
+
break
|
293
|
+
self.tcp_transport.send_message(chunk_data)
|
@@ -1,6 +1,8 @@
|
|
1
1
|
# server_http.py
|
2
2
|
# pylint: disable=line-too-long,broad-exception-raised,broad-exception-caught,import-outside-toplevel,protected-access
|
3
3
|
|
4
|
+
"""HTTP server SDK implementation for App Mesh."""
|
5
|
+
|
4
6
|
# Standard library imports
|
5
7
|
import abc
|
6
8
|
import logging
|
@@ -16,8 +18,7 @@ logger = logging.getLogger(__name__)
|
|
16
18
|
|
17
19
|
|
18
20
|
class AppMeshServer(metaclass=abc.ABCMeta):
|
19
|
-
"""
|
20
|
-
Server SDK for App Mesh application interacting with the local App Mesh REST service over HTTPS.
|
21
|
+
"""Server SDK for App Mesh application interacting with the local App Mesh REST service over HTTPS.
|
21
22
|
|
22
23
|
Build-in runtime environment variables required:
|
23
24
|
- APP_MESH_PROCESS_KEY
|
@@ -34,18 +35,13 @@ class AppMeshServer(metaclass=abc.ABCMeta):
|
|
34
35
|
context.task_return(result)
|
35
36
|
"""
|
36
37
|
|
38
|
+
_RETRY_DELAY_SECONDS = 0.1
|
39
|
+
|
37
40
|
def __init__(
|
38
41
|
self,
|
39
42
|
rest_url: str = "https://127.0.0.1:6060",
|
40
|
-
rest_ssl_verify=
|
41
|
-
rest_ssl_client_cert=
|
42
|
-
(
|
43
|
-
AppMeshClient.DEFAULT_SSL_CLIENT_CERT_PATH,
|
44
|
-
AppMeshClient.DEFAULT_SSL_CLIENT_KEY_PATH,
|
45
|
-
)
|
46
|
-
if os.path.exists(AppMeshClient.DEFAULT_SSL_CLIENT_CERT_PATH)
|
47
|
-
else None
|
48
|
-
),
|
43
|
+
rest_ssl_verify: Union[bool, str] = AppMeshClient.DEFAULT_SSL_CA_CERT_PATH,
|
44
|
+
rest_ssl_client_cert: Optional[Union[str, Tuple[str, str]]] = None,
|
49
45
|
rest_timeout: Tuple[float, float] = (60, 300),
|
50
46
|
*,
|
51
47
|
logger_: Optional[logging.Logger] = None,
|
@@ -63,6 +59,7 @@ class AppMeshServer(metaclass=abc.ABCMeta):
|
|
63
59
|
"""Read and validate required runtime environment variables."""
|
64
60
|
process_key = os.getenv("APP_MESH_PROCESS_KEY")
|
65
61
|
app_name = os.getenv("APP_MESH_APPLICATION_NAME")
|
62
|
+
|
66
63
|
if not process_key:
|
67
64
|
raise Exception("Missing environment variable: APP_MESH_PROCESS_KEY. This must be set by App Mesh service.")
|
68
65
|
if not app_name:
|
@@ -75,43 +72,43 @@ class AppMeshServer(metaclass=abc.ABCMeta):
|
|
75
72
|
Used by App Mesh application process to obtain the payload from App Mesh service
|
76
73
|
that a client pushed to it.
|
77
74
|
|
78
|
-
|
79
75
|
Returns:
|
80
|
-
|
76
|
+
The payload provided by the client as returned by the service.
|
81
77
|
"""
|
82
78
|
pkey, app_name = self._get_runtime_env()
|
83
79
|
path = f"/appmesh/app/{app_name}/task"
|
80
|
+
query_params = {"process_key": pkey}
|
84
81
|
|
85
82
|
while True:
|
86
83
|
resp = self._client._request_http(
|
87
84
|
AppMeshClient.Method.GET,
|
88
85
|
path=path,
|
89
|
-
query=
|
86
|
+
query=query_params,
|
90
87
|
)
|
91
88
|
|
92
|
-
if resp.status_code
|
93
|
-
|
94
|
-
time.sleep(0.1)
|
95
|
-
continue
|
89
|
+
if resp.status_code == HTTPStatus.OK:
|
90
|
+
return resp.content
|
96
91
|
|
97
|
-
|
92
|
+
self._logger.warning("task_fetch failed with status %d: %s, retrying...", resp.status_code, resp.text)
|
93
|
+
time.sleep(self._RETRY_DELAY_SECONDS)
|
98
94
|
|
99
95
|
def task_return(self, result: Union[str, bytes]) -> None:
|
100
96
|
"""Return the result of a server-side invocation back to the original client.
|
101
97
|
|
102
|
-
Used by App Mesh application process to
|
103
|
-
after
|
98
|
+
Used by App Mesh application process to post the `result` to App Mesh service
|
99
|
+
after processing payload data so the invoking client can retrieve it.
|
104
100
|
|
105
101
|
Args:
|
106
|
-
result
|
102
|
+
result: Result payload to be delivered back to the client.
|
107
103
|
"""
|
108
104
|
pkey, app_name = self._get_runtime_env()
|
109
105
|
path = f"/appmesh/app/{app_name}/task"
|
106
|
+
query_params = {"process_key": pkey}
|
110
107
|
|
111
108
|
resp = self._client._request_http(
|
112
109
|
AppMeshClient.Method.PUT,
|
113
110
|
path=path,
|
114
|
-
query=
|
111
|
+
query=query_params,
|
115
112
|
body=result,
|
116
113
|
)
|
117
114
|
|
@@ -3,8 +3,7 @@
|
|
3
3
|
|
4
4
|
# Standard library imports
|
5
5
|
import logging
|
6
|
-
import
|
7
|
-
from typing import Optional, Tuple
|
6
|
+
from typing import Optional, Tuple, Union
|
8
7
|
|
9
8
|
# Local imports
|
10
9
|
from .client_http import AppMeshClient
|
@@ -15,14 +14,12 @@ logger = logging.getLogger(__name__)
|
|
15
14
|
|
16
15
|
|
17
16
|
class AppMeshServerTCP(AppMeshServer):
|
18
|
-
"""
|
19
|
-
Server SDK for interacting with the local App Mesh service over TCP (TLS).
|
20
|
-
"""
|
17
|
+
"""Server SDK for interacting with the local App Mesh service over TCP (TLS)."""
|
21
18
|
|
22
19
|
def __init__(
|
23
20
|
self,
|
24
|
-
rest_ssl_verify=
|
25
|
-
rest_ssl_client_cert=None,
|
21
|
+
rest_ssl_verify: Union[bool, str] = AppMeshClient.DEFAULT_SSL_CA_CERT_PATH,
|
22
|
+
rest_ssl_client_cert: Optional[Union[str, Tuple[str, str]]] = None,
|
26
23
|
tcp_address: Tuple[str, int] = ("127.0.0.1", 6059),
|
27
24
|
*,
|
28
25
|
logger_: Optional[logging.Logger] = None,
|
@@ -34,6 +31,5 @@ class AppMeshServerTCP(AppMeshServer):
|
|
34
31
|
"""
|
35
32
|
# Deliberately avoid calling super().__init__ to inject a TCP client while keeping the same public API.
|
36
33
|
object.__init__(self)
|
37
|
-
# super().__init__(rest_ssl_verify=rest_ssl_verify, rest_ssl_client_cert=rest_ssl_client_cert)
|
38
34
|
self._client = AppMeshClientTCP(rest_ssl_verify=rest_ssl_verify, rest_ssl_client_cert=rest_ssl_client_cert, tcp_address=tcp_address)
|
39
35
|
self._logger = logger_ or logger
|
@@ -29,12 +29,10 @@ class TCPTransport:
|
|
29
29
|
|
30
30
|
Args:
|
31
31
|
address: Server address as (host, port) tuple.
|
32
|
-
|
33
32
|
ssl_verify: SSL server verification mode:
|
34
33
|
- True: Use system CA certificates
|
35
34
|
- False: Disable verification (insecure)
|
36
35
|
- str: Path to custom CA bundle or directory
|
37
|
-
|
38
36
|
ssl_client_cert: SSL client certificate:
|
39
37
|
- str: Path to PEM file with cert and key
|
40
38
|
- tuple: (cert_path, key_path)
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: appmesh
|
3
|
-
Version: 1.6.
|
3
|
+
Version: 1.6.14
|
4
4
|
Summary: Client SDK for App Mesh
|
5
5
|
Home-page: https://github.com/laoshanxi/app-mesh
|
6
6
|
Author: laoshanxi
|
@@ -17,6 +17,7 @@ Requires-Dist: msgpack
|
|
17
17
|
Requires-Dist: requests_toolbelt
|
18
18
|
Requires-Dist: aniso8601
|
19
19
|
Requires-Dist: PyJWT
|
20
|
+
Requires-Dist: dataclasses; python_version < "3.7"
|
20
21
|
Dynamic: author
|
21
22
|
Dynamic: author-email
|
22
23
|
Dynamic: classifier
|
@@ -10,7 +10,7 @@ with io.open(os.path.abspath(readme_path), mode="r", encoding="utf-8") as fh:
|
|
10
10
|
|
11
11
|
def get_version():
|
12
12
|
"""PyPI package version"""
|
13
|
-
return "1.6.
|
13
|
+
return "1.6.14"
|
14
14
|
|
15
15
|
|
16
16
|
# Dependencies
|
@@ -20,9 +20,8 @@ install_requires = [
|
|
20
20
|
"requests_toolbelt",
|
21
21
|
"aniso8601",
|
22
22
|
"PyJWT",
|
23
|
+
"dataclasses; python_version < '3.7'",
|
23
24
|
]
|
24
|
-
if sys.version_info < (3, 7):
|
25
|
-
install_requires.append("dataclasses")
|
26
25
|
|
27
26
|
setuptools.setup(
|
28
27
|
name="appmesh",
|
@@ -1,246 +0,0 @@
|
|
1
|
-
# client_tcp.py
|
2
|
-
# pylint: disable=line-too-long,broad-exception-raised,broad-exception-caught,import-outside-toplevel,protected-access
|
3
|
-
|
4
|
-
# Standard library imports
|
5
|
-
import json
|
6
|
-
import os
|
7
|
-
import socket
|
8
|
-
import sys
|
9
|
-
import uuid
|
10
|
-
|
11
|
-
# Third-party imports
|
12
|
-
import requests
|
13
|
-
|
14
|
-
# Local imports
|
15
|
-
from .client_http import AppMeshClient
|
16
|
-
from .tcp_messages import RequestMessage, ResponseMessage
|
17
|
-
from .tcp_transport import TCPTransport
|
18
|
-
|
19
|
-
|
20
|
-
class AppMeshClientTCP(AppMeshClient):
|
21
|
-
"""
|
22
|
-
Client SDK for interacting with the App Mesh service over TCP, with enhanced support for large file transfers.
|
23
|
-
|
24
|
-
The `AppMeshClientTCP` class extends the functionality of `AppMeshClient` by offering a TCP-based communication layer
|
25
|
-
for the App Mesh REST API. It overrides the file download and upload methods to support large file transfers with
|
26
|
-
improved performance, leveraging TCP for lower latency and higher throughput compared to HTTP.
|
27
|
-
|
28
|
-
This client is suitable for applications requiring efficient data transfers and high-throughput operations within the
|
29
|
-
App Mesh ecosystem, while maintaining compatibility with all other attributes and methods from `AppMeshClient`.
|
30
|
-
|
31
|
-
Dependency:
|
32
|
-
- Install the required package for message serialization:
|
33
|
-
pip3 install msgpack
|
34
|
-
|
35
|
-
Usage:
|
36
|
-
- Import the client module:
|
37
|
-
from appmesh import AppMeshClientTCP
|
38
|
-
|
39
|
-
Example:
|
40
|
-
client = AppMeshClientTCP()
|
41
|
-
client.login("your-name", "your-password")
|
42
|
-
client.file_download("/tmp/os-release", "os-release")
|
43
|
-
|
44
|
-
Attributes:
|
45
|
-
- Inherits all attributes from `AppMeshClient`, including TLS secure connections and JWT-based authentication.
|
46
|
-
- Optimized for TCP-based communication to provide better performance for large file transfers.
|
47
|
-
|
48
|
-
Methods:
|
49
|
-
- file_download()
|
50
|
-
- file_upload()
|
51
|
-
- Inherits all other methods from `AppMeshClient`, providing a consistent interface for managing applications within App Mesh.
|
52
|
-
"""
|
53
|
-
|
54
|
-
TCP_BLOCK_SIZE = 16 * 1024 - 128 # TLS-optimized chunk size, leaves some room for TLS overhead (like headers) within the 16 KB limit.
|
55
|
-
ENCODING_UTF8 = "utf-8"
|
56
|
-
HTTP_USER_AGENT_TCP = "appmesh/python/tcp"
|
57
|
-
HTTP_HEADER_KEY_X_SEND_FILE_SOCKET = "X-Send-File-Socket"
|
58
|
-
HTTP_HEADER_KEY_X_RECV_FILE_SOCKET = "X-Recv-File-Socket"
|
59
|
-
|
60
|
-
def __init__(
|
61
|
-
self,
|
62
|
-
rest_ssl_verify=AppMeshClient.DEFAULT_SSL_CA_CERT_PATH if os.path.exists(AppMeshClient.DEFAULT_SSL_CA_CERT_PATH) else False,
|
63
|
-
rest_ssl_client_cert=None,
|
64
|
-
jwt_token=None,
|
65
|
-
tcp_address=("127.0.0.1", 6059),
|
66
|
-
):
|
67
|
-
"""Construct an App Mesh client TCP object to communicate securely with an App Mesh server over TLS.
|
68
|
-
|
69
|
-
Args:
|
70
|
-
rest_ssl_verify (Union[bool, str], optional): Specifies SSL certificate verification behavior. Can be:
|
71
|
-
- `True`: Uses the system’s default CA certificates to verify the server’s identity.
|
72
|
-
- `False`: Disables SSL certificate verification (insecure, intended for development).
|
73
|
-
- `str`: Specifies a custom CA bundle or directory for server certificate verification. If a string is provided,
|
74
|
-
it should either be a file path to a custom CA certificate (CA bundle) or a directory path containing multiple
|
75
|
-
certificates (CA directory).
|
76
|
-
|
77
|
-
**Note**: Unlike HTTP requests, TCP connections cannot automatically retrieve intermediate or public CA certificates.
|
78
|
-
When `rest_ssl_verify` is a path, it explicitly identifies a CA issuer to ensure certificate validation.
|
79
|
-
|
80
|
-
rest_ssl_client_cert (Union[str, Tuple[str, str]], optional): Path to the SSL client certificate and key. If a `str`,
|
81
|
-
it should be the path to a PEM file containing both the client certificate and private key. If a `tuple`, it should
|
82
|
-
be a pair of paths: (`cert`, `key`), where `cert` is the client certificate file and `key` is the private key file.
|
83
|
-
|
84
|
-
jwt_token (str, optional): JWT token for authentication. Used in methods requiring login and user authorization.
|
85
|
-
|
86
|
-
tcp_address (Tuple[str, int], optional): Address and port for establishing a TCP connection to the server.
|
87
|
-
Defaults to `("127.0.0.1", 6059)`.
|
88
|
-
"""
|
89
|
-
self.tcp_transport = TCPTransport(address=tcp_address, ssl_verify=rest_ssl_verify, ssl_client_cert=rest_ssl_client_cert)
|
90
|
-
super().__init__(rest_ssl_verify=rest_ssl_verify, rest_ssl_client_cert=rest_ssl_client_cert, jwt_token=jwt_token)
|
91
|
-
|
92
|
-
def close(self):
|
93
|
-
"""Close the connection and release resources."""
|
94
|
-
if hasattr(self, "tcp_transport") and self.tcp_transport:
|
95
|
-
self.tcp_transport.close()
|
96
|
-
self.tcp_transport = None
|
97
|
-
return super().close()
|
98
|
-
|
99
|
-
def __del__(self):
|
100
|
-
"""Ensure resources are properly released when the object is garbage collected."""
|
101
|
-
try:
|
102
|
-
self.close()
|
103
|
-
except Exception:
|
104
|
-
pass # Never raise in __del__
|
105
|
-
super().__del__()
|
106
|
-
|
107
|
-
def _request_http(self, method: AppMeshClient.Method, path: str, query: dict = None, header: dict = None, body=None) -> requests.Response:
|
108
|
-
"""Send HTTP request over TCP transport.
|
109
|
-
|
110
|
-
Args:
|
111
|
-
method (Method): HTTP method.
|
112
|
-
path (str): URI path.
|
113
|
-
query (dict, optional): Query parameters.
|
114
|
-
header (dict, optional): HTTP headers.
|
115
|
-
body: Request body.
|
116
|
-
|
117
|
-
Returns:
|
118
|
-
requests.Response: Simulated HTTP response.
|
119
|
-
"""
|
120
|
-
if not self.tcp_transport.connected():
|
121
|
-
self.tcp_transport.connect()
|
122
|
-
|
123
|
-
appmesh_request = RequestMessage()
|
124
|
-
token = self._get_access_token()
|
125
|
-
if token:
|
126
|
-
appmesh_request.headers[self.HTTP_HEADER_KEY_AUTH] = token
|
127
|
-
if super().forward_to and len(super().forward_to) > 0:
|
128
|
-
raise Exception("Not support forward request in TCP mode")
|
129
|
-
appmesh_request.headers[self.HTTP_HEADER_KEY_USER_AGENT] = self.HTTP_USER_AGENT_TCP
|
130
|
-
appmesh_request.uuid = str(uuid.uuid1())
|
131
|
-
appmesh_request.http_method = method.value
|
132
|
-
appmesh_request.request_uri = path
|
133
|
-
appmesh_request.client_addr = socket.gethostname()
|
134
|
-
|
135
|
-
if body:
|
136
|
-
if isinstance(body, (dict, list)):
|
137
|
-
appmesh_request.body = bytes(json.dumps(body, indent=2), self.ENCODING_UTF8)
|
138
|
-
elif isinstance(body, str):
|
139
|
-
appmesh_request.body = bytes(body, self.ENCODING_UTF8)
|
140
|
-
elif isinstance(body, bytes):
|
141
|
-
appmesh_request.body = body
|
142
|
-
else:
|
143
|
-
raise Exception(f"UnSupported body type: {type(body)}")
|
144
|
-
|
145
|
-
if header:
|
146
|
-
for k, v in header.items():
|
147
|
-
appmesh_request.headers[k] = v
|
148
|
-
if query:
|
149
|
-
for k, v in query.items():
|
150
|
-
appmesh_request.query[k] = v
|
151
|
-
|
152
|
-
data = appmesh_request.serialize()
|
153
|
-
self.tcp_transport.send_message(data)
|
154
|
-
|
155
|
-
resp_data = self.tcp_transport.receive_message()
|
156
|
-
if not resp_data: # Covers None and empty bytes
|
157
|
-
self.tcp_transport.close()
|
158
|
-
raise Exception("socket connection broken")
|
159
|
-
|
160
|
-
appmesh_resp = ResponseMessage().deserialize(resp_data)
|
161
|
-
response = requests.Response()
|
162
|
-
response.status_code = appmesh_resp.http_status
|
163
|
-
# response.encoding = self.ENCODING_UTF8 # only need when charset not in appmesh_resp.body_msg_type
|
164
|
-
response._content = appmesh_resp.body if isinstance(appmesh_resp.body, bytes) else str(appmesh_resp.body).encode(self.ENCODING_UTF8)
|
165
|
-
response.headers = appmesh_resp.headers
|
166
|
-
if appmesh_resp.body_msg_type:
|
167
|
-
response.headers["Content-Type"] = appmesh_resp.body_msg_type
|
168
|
-
|
169
|
-
return AppMeshClient.EncodingResponse(response)
|
170
|
-
|
171
|
-
########################################
|
172
|
-
# File management
|
173
|
-
########################################
|
174
|
-
def download_file(self, remote_file: str, local_file: str, apply_file_attributes: bool = True) -> None:
|
175
|
-
"""Copy a remote file to local, preserving file attributes if requested.
|
176
|
-
|
177
|
-
Args:
|
178
|
-
remote_file (str): Remote file path.
|
179
|
-
local_file (str): Local destination path.
|
180
|
-
apply_file_attributes (bool): Apply remote file permissions/ownership locally.
|
181
|
-
"""
|
182
|
-
header = {
|
183
|
-
AppMeshClient.HTTP_HEADER_KEY_X_FILE_PATH: remote_file,
|
184
|
-
self.HTTP_HEADER_KEY_X_RECV_FILE_SOCKET: "true",
|
185
|
-
}
|
186
|
-
resp = self._request_http(AppMeshClient.Method.GET, path="/appmesh/file/download", header=header)
|
187
|
-
resp.raise_for_status()
|
188
|
-
|
189
|
-
if self.HTTP_HEADER_KEY_X_RECV_FILE_SOCKET not in resp.headers:
|
190
|
-
raise ValueError(f"Server did not respond with socket transfer option: {self.HTTP_HEADER_KEY_X_RECV_FILE_SOCKET}")
|
191
|
-
|
192
|
-
with open(local_file, "wb") as fp:
|
193
|
-
while True:
|
194
|
-
chunk_data = self.tcp_transport.receive_message()
|
195
|
-
if not chunk_data:
|
196
|
-
break
|
197
|
-
fp.write(chunk_data)
|
198
|
-
|
199
|
-
if apply_file_attributes and sys.platform != "win32":
|
200
|
-
if "X-File-Mode" in resp.headers:
|
201
|
-
os.chmod(path=local_file, mode=int(resp.headers["X-File-Mode"]))
|
202
|
-
if "X-File-User" in resp.headers and "X-File-Group" in resp.headers:
|
203
|
-
file_uid = int(resp.headers["X-File-User"])
|
204
|
-
file_gid = int(resp.headers["X-File-Group"])
|
205
|
-
try:
|
206
|
-
os.chown(path=local_file, uid=file_uid, gid=file_gid)
|
207
|
-
except PermissionError:
|
208
|
-
print(f"Warning: Unable to change owner/group of {local_file}. Operation requires elevated privileges.")
|
209
|
-
|
210
|
-
def upload_file(self, local_file: str, remote_file: str, apply_file_attributes: bool = True) -> None:
|
211
|
-
"""Upload a local file to remote server, preserving file attributes if requested.
|
212
|
-
|
213
|
-
Args:
|
214
|
-
local_file (str): Local file path.
|
215
|
-
remote_file (str): Remote destination path.
|
216
|
-
apply_file_attributes (bool): Upload file permissions/ownership metadata.
|
217
|
-
"""
|
218
|
-
if not os.path.exists(local_file):
|
219
|
-
raise FileNotFoundError(f"Local file not found: {local_file}")
|
220
|
-
|
221
|
-
with open(file=local_file, mode="rb") as fp:
|
222
|
-
header = {
|
223
|
-
AppMeshClient.HTTP_HEADER_KEY_X_FILE_PATH: remote_file,
|
224
|
-
"Content-Type": "text/plain",
|
225
|
-
self.HTTP_HEADER_KEY_X_SEND_FILE_SOCKET: "true",
|
226
|
-
}
|
227
|
-
|
228
|
-
if apply_file_attributes:
|
229
|
-
file_stat = os.stat(local_file)
|
230
|
-
header["X-File-Mode"] = str(file_stat.st_mode & 0o777) # Mask to keep only permission bits
|
231
|
-
header["X-File-User"] = str(file_stat.st_uid)
|
232
|
-
header["X-File-Group"] = str(file_stat.st_gid)
|
233
|
-
|
234
|
-
resp = self._request_http(AppMeshClient.Method.POST, path="/appmesh/file/upload", header=header)
|
235
|
-
resp.raise_for_status()
|
236
|
-
|
237
|
-
if self.HTTP_HEADER_KEY_X_SEND_FILE_SOCKET not in resp.headers:
|
238
|
-
raise ValueError(f"Server did not respond with socket transfer option: {self.HTTP_HEADER_KEY_X_SEND_FILE_SOCKET}")
|
239
|
-
|
240
|
-
chunk_size = self.TCP_BLOCK_SIZE
|
241
|
-
while True:
|
242
|
-
chunk_data = fp.read(chunk_size)
|
243
|
-
if not chunk_data:
|
244
|
-
self.tcp_transport.send_message([]) # EOF signal
|
245
|
-
break
|
246
|
-
self.tcp_transport.send_message(chunk_data)
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|