plato-sdk-v2 2.7.1__py3-none-any.whl → 2.7.2__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.
- plato/v2/utils/db_cleanup.py +7 -9
- plato/v2/utils/gateway_tunnel.py +221 -0
- {plato_sdk_v2-2.7.1.dist-info → plato_sdk_v2-2.7.2.dist-info}/METADATA +1 -1
- {plato_sdk_v2-2.7.1.dist-info → plato_sdk_v2-2.7.2.dist-info}/RECORD +6 -5
- {plato_sdk_v2-2.7.1.dist-info → plato_sdk_v2-2.7.2.dist-info}/WHEEL +0 -0
- {plato_sdk_v2-2.7.1.dist-info → plato_sdk_v2-2.7.2.dist-info}/entry_points.txt +0 -0
plato/v2/utils/db_cleanup.py
CHANGED
|
@@ -12,6 +12,7 @@ from sqlalchemy.ext.asyncio import create_async_engine
|
|
|
12
12
|
|
|
13
13
|
from plato._generated.api.v1.simulator import get_db_config
|
|
14
14
|
from plato._generated.models import DbConfigResponse
|
|
15
|
+
from plato.v2.utils.gateway_tunnel import GatewayTunnel, find_free_port
|
|
15
16
|
from plato.v2.utils.models import (
|
|
16
17
|
ApiCleanupResult,
|
|
17
18
|
DatabaseCleanupResult,
|
|
@@ -19,12 +20,10 @@ from plato.v2.utils.models import (
|
|
|
19
20
|
EnvironmentInfo,
|
|
20
21
|
SessionCleanupResult,
|
|
21
22
|
)
|
|
22
|
-
from plato.v2.utils.proxy_tunnel import
|
|
23
|
+
from plato.v2.utils.proxy_tunnel import make_db_url
|
|
23
24
|
|
|
24
25
|
logger = logging.getLogger(__name__)
|
|
25
26
|
|
|
26
|
-
TEMP_PASSWORD = "newpass"
|
|
27
|
-
|
|
28
27
|
|
|
29
28
|
class DatabaseCleaner:
|
|
30
29
|
"""Handles database audit_log cleanup operations."""
|
|
@@ -145,14 +144,13 @@ class DatabaseCleaner:
|
|
|
145
144
|
config: DbConfigResponse,
|
|
146
145
|
local_port: int,
|
|
147
146
|
) -> DatabaseCleanupResult:
|
|
148
|
-
"""Connect to a single DB via tunnel and truncate audit_log tables."""
|
|
147
|
+
"""Connect to a single DB via gateway tunnel and truncate audit_log tables."""
|
|
149
148
|
db_port = config.db_port
|
|
150
149
|
|
|
151
|
-
tunnel =
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
host_port=local_port,
|
|
150
|
+
tunnel = GatewayTunnel(
|
|
151
|
+
job_id=job_id,
|
|
152
|
+
remote_port=db_port,
|
|
153
|
+
local_port=local_port,
|
|
156
154
|
)
|
|
157
155
|
|
|
158
156
|
try:
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
"""TLS + SNI gateway tunnel for database connections.
|
|
2
|
+
|
|
3
|
+
Routes traffic through gateway.plato.so:443 using SNI-based routing,
|
|
4
|
+
replacing the deprecated HTTP CONNECT proxy approach.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import asyncio
|
|
10
|
+
import logging
|
|
11
|
+
import os
|
|
12
|
+
import socket
|
|
13
|
+
import ssl
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
# Default gateway configuration
|
|
18
|
+
DEFAULT_GATEWAY_HOST = "gateway.plato.so"
|
|
19
|
+
DEFAULT_GATEWAY_PORT = 443
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def get_gateway_config() -> tuple[str, int]:
|
|
23
|
+
"""Get gateway host and port from environment or defaults.
|
|
24
|
+
|
|
25
|
+
Returns:
|
|
26
|
+
Tuple of (host, port) for the gateway.
|
|
27
|
+
"""
|
|
28
|
+
host = os.environ.get("PLATO_GATEWAY_HOST", DEFAULT_GATEWAY_HOST)
|
|
29
|
+
port = int(os.environ.get("PLATO_GATEWAY_PORT", str(DEFAULT_GATEWAY_PORT)))
|
|
30
|
+
return host, port
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def find_free_port(start_port: int = 55432) -> int:
|
|
34
|
+
"""Find the first available TCP port starting from start_port."""
|
|
35
|
+
port = start_port
|
|
36
|
+
while port < 65535:
|
|
37
|
+
try:
|
|
38
|
+
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
|
39
|
+
s.bind(("127.0.0.1", port))
|
|
40
|
+
return port
|
|
41
|
+
except OSError:
|
|
42
|
+
port += 1
|
|
43
|
+
raise RuntimeError(f"No free port found starting from {start_port}")
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class GatewayTunnel:
|
|
47
|
+
"""Async TLS + SNI gateway tunnel for database connections.
|
|
48
|
+
|
|
49
|
+
Routes local connections through gateway.plato.so using SNI-based routing.
|
|
50
|
+
This replaces the deprecated HTTP CONNECT proxy tunnel.
|
|
51
|
+
|
|
52
|
+
Usage:
|
|
53
|
+
tunnel = GatewayTunnel(job_id="abc123", remote_port=5432, local_port=55432)
|
|
54
|
+
await tunnel.start()
|
|
55
|
+
# Connect to localhost:55432 to reach the VM's port 5432
|
|
56
|
+
await tunnel.stop()
|
|
57
|
+
"""
|
|
58
|
+
|
|
59
|
+
def __init__(
|
|
60
|
+
self,
|
|
61
|
+
job_id: str,
|
|
62
|
+
remote_port: int,
|
|
63
|
+
local_port: int,
|
|
64
|
+
gateway_host: str | None = None,
|
|
65
|
+
gateway_port: int | None = None,
|
|
66
|
+
verify_ssl: bool = True,
|
|
67
|
+
):
|
|
68
|
+
"""Initialize the gateway tunnel.
|
|
69
|
+
|
|
70
|
+
Args:
|
|
71
|
+
job_id: The job/environment ID to connect to.
|
|
72
|
+
remote_port: Port on the VM to forward to.
|
|
73
|
+
local_port: Local port to listen on.
|
|
74
|
+
gateway_host: Gateway hostname (default: from env or gateway.plato.so).
|
|
75
|
+
gateway_port: Gateway port (default: from env or 443).
|
|
76
|
+
verify_ssl: Whether to verify SSL certificates.
|
|
77
|
+
"""
|
|
78
|
+
self.job_id = job_id
|
|
79
|
+
self.remote_port = remote_port
|
|
80
|
+
self.local_port = local_port
|
|
81
|
+
self.verify_ssl = verify_ssl
|
|
82
|
+
|
|
83
|
+
# Get gateway config
|
|
84
|
+
default_host, default_port = get_gateway_config()
|
|
85
|
+
self.gateway_host = gateway_host or default_host
|
|
86
|
+
self.gateway_port = gateway_port or default_port
|
|
87
|
+
|
|
88
|
+
# SNI for routing: {job_id}--{port}.gateway.plato.so
|
|
89
|
+
self.sni = f"{job_id}--{remote_port}.{self.gateway_host}"
|
|
90
|
+
|
|
91
|
+
self._server: asyncio.AbstractServer | None = None
|
|
92
|
+
self._client_tasks: set[asyncio.Task] = set()
|
|
93
|
+
|
|
94
|
+
async def _open_gateway_connection(
|
|
95
|
+
self,
|
|
96
|
+
timeout: float = 30.0,
|
|
97
|
+
) -> tuple[asyncio.StreamReader, asyncio.StreamWriter]:
|
|
98
|
+
"""Open a TLS connection to the gateway with SNI routing.
|
|
99
|
+
|
|
100
|
+
Returns:
|
|
101
|
+
Tuple of (reader, writer) for the gateway connection.
|
|
102
|
+
"""
|
|
103
|
+
# Create SSL context
|
|
104
|
+
ssl_ctx = ssl.create_default_context()
|
|
105
|
+
if not self.verify_ssl:
|
|
106
|
+
ssl_ctx.check_hostname = False
|
|
107
|
+
ssl_ctx.verify_mode = ssl.CERT_NONE
|
|
108
|
+
|
|
109
|
+
# Connect with TLS, using SNI for routing
|
|
110
|
+
reader, writer = await asyncio.wait_for(
|
|
111
|
+
asyncio.open_connection(
|
|
112
|
+
self.gateway_host,
|
|
113
|
+
self.gateway_port,
|
|
114
|
+
ssl=ssl_ctx,
|
|
115
|
+
server_hostname=self.sni, # SNI determines which VM/port to route to
|
|
116
|
+
),
|
|
117
|
+
timeout=timeout,
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
# Enable TCP keepalive
|
|
121
|
+
sock = writer.get_extra_info("socket")
|
|
122
|
+
if isinstance(sock, socket.socket):
|
|
123
|
+
try:
|
|
124
|
+
sock.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1)
|
|
125
|
+
# macOS/BSD keepalive idle time
|
|
126
|
+
TCP_KEEPALIVE = getattr(socket, "TCP_KEEPALIVE", 0x10)
|
|
127
|
+
sock.setsockopt(socket.IPPROTO_TCP, TCP_KEEPALIVE, 30)
|
|
128
|
+
except OSError:
|
|
129
|
+
pass # Best effort
|
|
130
|
+
|
|
131
|
+
return reader, writer
|
|
132
|
+
|
|
133
|
+
async def _pipe(
|
|
134
|
+
self,
|
|
135
|
+
src: asyncio.StreamReader,
|
|
136
|
+
dst: asyncio.StreamWriter,
|
|
137
|
+
) -> None:
|
|
138
|
+
"""Forward data from src to dst until EOF."""
|
|
139
|
+
try:
|
|
140
|
+
while True:
|
|
141
|
+
data = await src.read(65536)
|
|
142
|
+
if not data:
|
|
143
|
+
break
|
|
144
|
+
dst.write(data)
|
|
145
|
+
await dst.drain()
|
|
146
|
+
except (ConnectionResetError, BrokenPipeError, OSError):
|
|
147
|
+
pass
|
|
148
|
+
finally:
|
|
149
|
+
try:
|
|
150
|
+
dst.close()
|
|
151
|
+
await dst.wait_closed()
|
|
152
|
+
except Exception:
|
|
153
|
+
pass
|
|
154
|
+
|
|
155
|
+
async def _handle_client(
|
|
156
|
+
self,
|
|
157
|
+
client_reader: asyncio.StreamReader,
|
|
158
|
+
client_writer: asyncio.StreamWriter,
|
|
159
|
+
) -> None:
|
|
160
|
+
"""Handle a single client connection by forwarding through gateway."""
|
|
161
|
+
task = asyncio.current_task()
|
|
162
|
+
if task:
|
|
163
|
+
self._client_tasks.add(task)
|
|
164
|
+
|
|
165
|
+
try:
|
|
166
|
+
# Connect to gateway via TLS with SNI
|
|
167
|
+
gateway_reader, gateway_writer = await self._open_gateway_connection()
|
|
168
|
+
|
|
169
|
+
# Bidirectional forwarding
|
|
170
|
+
await asyncio.gather(
|
|
171
|
+
self._pipe(client_reader, gateway_writer),
|
|
172
|
+
self._pipe(gateway_reader, client_writer),
|
|
173
|
+
)
|
|
174
|
+
except Exception as e:
|
|
175
|
+
logger.warning(f"Gateway tunnel error: {e}")
|
|
176
|
+
try:
|
|
177
|
+
client_writer.close()
|
|
178
|
+
await client_writer.wait_closed()
|
|
179
|
+
except Exception:
|
|
180
|
+
pass
|
|
181
|
+
finally:
|
|
182
|
+
if task:
|
|
183
|
+
self._client_tasks.discard(task)
|
|
184
|
+
|
|
185
|
+
async def start(self) -> None:
|
|
186
|
+
"""Start the gateway tunnel server."""
|
|
187
|
+
logger.info(
|
|
188
|
+
f"Starting gateway tunnel: localhost:{self.local_port} -> "
|
|
189
|
+
f"{self.job_id}:{self.remote_port} via {self.gateway_host}"
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
self._server = await asyncio.start_server(
|
|
193
|
+
self._handle_client,
|
|
194
|
+
host="127.0.0.1",
|
|
195
|
+
port=self.local_port,
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
# Small delay to ensure binding is settled
|
|
199
|
+
await asyncio.sleep(0.1)
|
|
200
|
+
|
|
201
|
+
if not self._server.sockets:
|
|
202
|
+
raise RuntimeError("Gateway tunnel failed to start: no listening sockets")
|
|
203
|
+
|
|
204
|
+
logger.info(f"Gateway tunnel established on port {self.local_port}")
|
|
205
|
+
|
|
206
|
+
async def stop(self) -> None:
|
|
207
|
+
"""Stop the gateway tunnel server."""
|
|
208
|
+
if self._server is not None:
|
|
209
|
+
logger.info("Stopping gateway tunnel")
|
|
210
|
+
|
|
211
|
+
# Stop accepting new connections
|
|
212
|
+
self._server.close()
|
|
213
|
+
await self._server.wait_closed()
|
|
214
|
+
self._server = None
|
|
215
|
+
|
|
216
|
+
# Cancel active client tasks
|
|
217
|
+
for t in list(self._client_tasks):
|
|
218
|
+
t.cancel()
|
|
219
|
+
self._client_tasks.clear()
|
|
220
|
+
|
|
221
|
+
logger.info("Gateway tunnel stopped")
|
|
@@ -494,7 +494,8 @@ plato/v2/sync/environment.py,sha256=WnDzbyEHpwCSEP8XnfNSjIYS7rt7lYR4HGJjzprZmTQ,
|
|
|
494
494
|
plato/v2/sync/flow_executor.py,sha256=N41-WCWIJVcCR2UmPUEiK7roNacYoeONkRXpR7lUgT8,13941
|
|
495
495
|
plato/v2/sync/session.py,sha256=okXqF-CjMmA82WRy2zPXaGidbovgjAENSqiuvE4_jKE,30420
|
|
496
496
|
plato/v2/utils/__init__.py,sha256=XLeFFsjXkm9g2raMmo7Wt4QN4hhCrNZDJKnpffJ4LtM,38
|
|
497
|
-
plato/v2/utils/db_cleanup.py,sha256=
|
|
497
|
+
plato/v2/utils/db_cleanup.py,sha256=JMzAAJz0ZnoUXtd8F4jpQmBpJpos2__RkgN_cuEearg,8692
|
|
498
|
+
plato/v2/utils/gateway_tunnel.py,sha256=eWgwf4VV8-jx6iCuHFgCISsAOVmNOOjCB56EuZLsnOA,7171
|
|
498
499
|
plato/v2/utils/models.py,sha256=PwehSSnIRG-tM3tWL1PzZEH77ZHhIAZ9R0UPs6YknbM,1441
|
|
499
500
|
plato/v2/utils/proxy_tunnel.py,sha256=8ZTd0jCGSfIHMvSv1fgEyacuISWnGPHLPbDglWroTzY,10463
|
|
500
501
|
plato/worlds/README.md,sha256=XFOkEA3cNNcrWkk-Cxnsl-zn-y0kvUENKQRSqFKpdqw,5479
|
|
@@ -503,7 +504,7 @@ plato/worlds/base.py,sha256=-RR71bSxEFI5yydtrtq-AAbuw98CIjvmrbztqzB9oIc,31041
|
|
|
503
504
|
plato/worlds/build_hook.py,sha256=KSoW0kqa5b7NyZ7MYOw2qsZ_2FkWuz0M3Ru7AKOP7Qw,3486
|
|
504
505
|
plato/worlds/config.py,sha256=O1lUXzxp-Z_M7izslT8naXgE6XujjzwYFFrDDzUOueI,12736
|
|
505
506
|
plato/worlds/runner.py,sha256=r9B2BxBae8_dM7y5cJf9xhThp_I1Qvf_tlPq2rs8qC8,4013
|
|
506
|
-
plato_sdk_v2-2.7.
|
|
507
|
-
plato_sdk_v2-2.7.
|
|
508
|
-
plato_sdk_v2-2.7.
|
|
509
|
-
plato_sdk_v2-2.7.
|
|
507
|
+
plato_sdk_v2-2.7.2.dist-info/METADATA,sha256=fv3apoCuAT5uplSgW9dbuMmduW2z7NqhVSOVErRIb5k,8652
|
|
508
|
+
plato_sdk_v2-2.7.2.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
509
|
+
plato_sdk_v2-2.7.2.dist-info/entry_points.txt,sha256=upGMbJCx6YWUTKrPoYvYUYfFCqYr75nHDwhA-45m6p8,136
|
|
510
|
+
plato_sdk_v2-2.7.2.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|