server-management-lib 0.2.0__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.
- server_management_lib/__init__.py +124 -0
- server_management_lib/audit.py +80 -0
- server_management_lib/base.py +55 -0
- server_management_lib/config.py +385 -0
- server_management_lib/executor.py +53 -0
- server_management_lib/http_clients.py +636 -0
- server_management_lib/local_executor.py +200 -0
- server_management_lib/operations.py +33 -0
- server_management_lib/output.py +61 -0
- server_management_lib/py.typed +0 -0
- server_management_lib/rate_limit.py +118 -0
- server_management_lib/redaction.py +191 -0
- server_management_lib/responses.py +126 -0
- server_management_lib/security.py +1552 -0
- server_management_lib/ssh_manager.py +447 -0
- server_management_lib/transport.py +128 -0
- server_management_lib/tunnel.py +380 -0
- server_management_lib-0.2.0.dist-info/METADATA +46 -0
- server_management_lib-0.2.0.dist-info/RECORD +21 -0
- server_management_lib-0.2.0.dist-info/WHEEL +5 -0
- server_management_lib-0.2.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,380 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Optional SSH local port forwarding for loopback-only metric endpoints.
|
|
3
|
+
|
|
4
|
+
A metric service (InfluxDB, Prometheus) may listen only on the target host's
|
|
5
|
+
loopback interface. In that case the MCP cannot reach it directly: the normal
|
|
6
|
+
mode is an HTTPS reverse proxy with a token. This module lets the MCP create a
|
|
7
|
+
local tunnel through the configured SSH connection instead of requiring the
|
|
8
|
+
operator to keep ``ssh -N -L`` running by hand.
|
|
9
|
+
|
|
10
|
+
The tunnel is deliberately narrow:
|
|
11
|
+
|
|
12
|
+
- it listens on a loopback address only (never ``0.0.0.0``);
|
|
13
|
+
- it forwards to the host/port configured in the database section;
|
|
14
|
+
- it uses the same SSH settings (host key verification, key, agent) as the
|
|
15
|
+
executor.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
import asyncio
|
|
19
|
+
import logging
|
|
20
|
+
from dataclasses import dataclass
|
|
21
|
+
from typing import Any
|
|
22
|
+
|
|
23
|
+
import asyncssh
|
|
24
|
+
|
|
25
|
+
from .security import SecurityValidator
|
|
26
|
+
from .ssh_manager import SSHManager
|
|
27
|
+
|
|
28
|
+
logger = logging.getLogger(__name__)
|
|
29
|
+
|
|
30
|
+
LOOPBACK_HOSTS = frozenset({"127.0.0.1", "::1", "localhost"})
|
|
31
|
+
MAX_PORT = 65535
|
|
32
|
+
|
|
33
|
+
# Section name -> default remote port. The database port is the remote port
|
|
34
|
+
# when ``ssh_tunnel.remote_port`` is not set.
|
|
35
|
+
METRIC_SECTIONS: tuple[tuple[str, int], ...] = (
|
|
36
|
+
("influxdb", 8181),
|
|
37
|
+
("prometheus", 9090),
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class SSHTunnelError(RuntimeError):
|
|
42
|
+
"""Raised when an SSH tunnel cannot be configured or opened."""
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@dataclass(frozen=True)
|
|
46
|
+
class TunnelEndpoint:
|
|
47
|
+
"""Local address a tunnel is listening on."""
|
|
48
|
+
|
|
49
|
+
host: str
|
|
50
|
+
port: int
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@dataclass(frozen=True)
|
|
54
|
+
class TunnelConfig:
|
|
55
|
+
"""Validated ``ssh_tunnel`` settings for one metric section."""
|
|
56
|
+
|
|
57
|
+
remote_host: str
|
|
58
|
+
remote_port: int
|
|
59
|
+
local_host: str = "127.0.0.1"
|
|
60
|
+
local_port: int = 0
|
|
61
|
+
|
|
62
|
+
@classmethod
|
|
63
|
+
def from_config(
|
|
64
|
+
cls,
|
|
65
|
+
value: Any,
|
|
66
|
+
*,
|
|
67
|
+
default_remote_port: int,
|
|
68
|
+
) -> "TunnelConfig":
|
|
69
|
+
"""Validate a raw ``ssh_tunnel`` mapping.
|
|
70
|
+
|
|
71
|
+
``default_remote_port`` is the database section's ``port`` (or its
|
|
72
|
+
built-in default), so a minimal configuration only needs
|
|
73
|
+
``enabled: true``.
|
|
74
|
+
"""
|
|
75
|
+
if not isinstance(value, dict):
|
|
76
|
+
raise SSHTunnelError("ssh_tunnel must be a mapping")
|
|
77
|
+
|
|
78
|
+
remote_host = _validate_host(
|
|
79
|
+
value.get("remote_host", "127.0.0.1"),
|
|
80
|
+
"ssh_tunnel.remote_host",
|
|
81
|
+
)
|
|
82
|
+
raw_remote_port = value.get("remote_port")
|
|
83
|
+
if raw_remote_port is None:
|
|
84
|
+
raw_remote_port = default_remote_port
|
|
85
|
+
remote_port = _validate_port(
|
|
86
|
+
raw_remote_port,
|
|
87
|
+
"ssh_tunnel.remote_port",
|
|
88
|
+
allow_zero=False,
|
|
89
|
+
)
|
|
90
|
+
local_host = _validate_host(
|
|
91
|
+
value.get("local_host", "127.0.0.1"),
|
|
92
|
+
"ssh_tunnel.local_host",
|
|
93
|
+
)
|
|
94
|
+
if local_host not in LOOPBACK_HOSTS:
|
|
95
|
+
raise SSHTunnelError(
|
|
96
|
+
"ssh_tunnel.local_host must be a loopback address "
|
|
97
|
+
f"({', '.join(sorted(LOOPBACK_HOSTS))})"
|
|
98
|
+
)
|
|
99
|
+
local_port = _validate_port(
|
|
100
|
+
value.get("local_port", 0),
|
|
101
|
+
"ssh_tunnel.local_port",
|
|
102
|
+
allow_zero=True,
|
|
103
|
+
)
|
|
104
|
+
return cls(
|
|
105
|
+
remote_host=remote_host,
|
|
106
|
+
remote_port=remote_port,
|
|
107
|
+
local_host=local_host,
|
|
108
|
+
local_port=local_port,
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _validate_host(value: Any, name: str) -> str:
|
|
113
|
+
if not isinstance(value, str):
|
|
114
|
+
raise SSHTunnelError(f"{name} must be a hostname or IP address")
|
|
115
|
+
host = value.strip()
|
|
116
|
+
if not host:
|
|
117
|
+
raise SSHTunnelError(f"{name} must not be empty")
|
|
118
|
+
if len(host) > 253 or any(char.isspace() or ord(char) < 32 for char in host):
|
|
119
|
+
raise SSHTunnelError(f"{name} contains an invalid character")
|
|
120
|
+
return host
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _validate_port(value: Any, name: str, *, allow_zero: bool) -> int:
|
|
124
|
+
if isinstance(value, bool):
|
|
125
|
+
raise SSHTunnelError(f"{name} must be an integer between 0 and {MAX_PORT}")
|
|
126
|
+
try:
|
|
127
|
+
port = int(value)
|
|
128
|
+
except (TypeError, ValueError) as e:
|
|
129
|
+
raise SSHTunnelError(
|
|
130
|
+
f"{name} must be an integer between 0 and {MAX_PORT}"
|
|
131
|
+
) from e
|
|
132
|
+
minimum = 0 if allow_zero else 1
|
|
133
|
+
if not minimum <= port <= MAX_PORT:
|
|
134
|
+
raise SSHTunnelError(f"{name} must be between {minimum} and {MAX_PORT}")
|
|
135
|
+
return port
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
class SSHTunnel:
|
|
139
|
+
"""One local port forward over a dedicated SSH connection.
|
|
140
|
+
|
|
141
|
+
A dropped SSH connection closes the forwarding listener. The tunnel keeps a
|
|
142
|
+
monitor task that detects the close and re-opens both the connection and
|
|
143
|
+
the listener on the same local port, so clients that already point at the
|
|
144
|
+
endpoint keep working. Reconnection is retried until :meth:`close`; while
|
|
145
|
+
it is failing, ``down`` is true and ``last_error`` describes the failure.
|
|
146
|
+
"""
|
|
147
|
+
|
|
148
|
+
def __init__(
|
|
149
|
+
self,
|
|
150
|
+
ssh_config: dict[str, Any],
|
|
151
|
+
tunnel_config: dict[str, Any],
|
|
152
|
+
*,
|
|
153
|
+
default_remote_port: int,
|
|
154
|
+
):
|
|
155
|
+
self.ssh_config = dict(ssh_config or {})
|
|
156
|
+
self.config = TunnelConfig.from_config(
|
|
157
|
+
tunnel_config,
|
|
158
|
+
default_remote_port=default_remote_port,
|
|
159
|
+
)
|
|
160
|
+
self._manager: SSHManager | None = None
|
|
161
|
+
self._listener: asyncssh.SSHListener | None = None
|
|
162
|
+
self._endpoint: TunnelEndpoint | None = None
|
|
163
|
+
self._monitor_task: asyncio.Task | None = None
|
|
164
|
+
self._closed = False
|
|
165
|
+
self.down = False
|
|
166
|
+
self.last_error: str | None = None
|
|
167
|
+
|
|
168
|
+
async def _open(self, local_port: int) -> TunnelEndpoint:
|
|
169
|
+
"""Open one SSH connection and local forward."""
|
|
170
|
+
manager = SSHManager({"ssh": self.ssh_config}, SecurityValidator({}))
|
|
171
|
+
try:
|
|
172
|
+
await manager.connect()
|
|
173
|
+
connection = manager.connection
|
|
174
|
+
if connection is None:
|
|
175
|
+
raise SSHTunnelError("SSH connection failed")
|
|
176
|
+
listener = await connection.forward_local_port(
|
|
177
|
+
self.config.local_host,
|
|
178
|
+
local_port,
|
|
179
|
+
self.config.remote_host,
|
|
180
|
+
self.config.remote_port,
|
|
181
|
+
)
|
|
182
|
+
except SSHTunnelError:
|
|
183
|
+
await manager.disconnect()
|
|
184
|
+
raise
|
|
185
|
+
except Exception as e:
|
|
186
|
+
await manager.disconnect()
|
|
187
|
+
raise SSHTunnelError(f"Could not open SSH tunnel: {e}") from e
|
|
188
|
+
|
|
189
|
+
self._manager = manager
|
|
190
|
+
self._listener = listener
|
|
191
|
+
port = listener.get_port() or local_port
|
|
192
|
+
self._endpoint = TunnelEndpoint(self.config.local_host, port)
|
|
193
|
+
logger.info(
|
|
194
|
+
"SSH tunnel listening on %s:%s for %s:%s",
|
|
195
|
+
self.config.local_host,
|
|
196
|
+
port,
|
|
197
|
+
self.config.remote_host,
|
|
198
|
+
self.config.remote_port,
|
|
199
|
+
)
|
|
200
|
+
return self._endpoint
|
|
201
|
+
|
|
202
|
+
async def start(self) -> TunnelEndpoint:
|
|
203
|
+
"""Open the SSH connection and the local listener."""
|
|
204
|
+
self._closed = False
|
|
205
|
+
endpoint = await self._open(self.config.local_port)
|
|
206
|
+
self._start_monitor()
|
|
207
|
+
return endpoint
|
|
208
|
+
|
|
209
|
+
def _start_monitor(self) -> None:
|
|
210
|
+
"""Watch the current connection and reconnect when it closes."""
|
|
211
|
+
manager = self._manager
|
|
212
|
+
connection = manager.connection if manager is not None else None
|
|
213
|
+
wait_closed = getattr(connection, "wait_closed", None)
|
|
214
|
+
if connection is None or wait_closed is None:
|
|
215
|
+
# Test doubles and older asyncssh releases may not expose
|
|
216
|
+
# wait_closed; reconnection is best effort in that case.
|
|
217
|
+
return
|
|
218
|
+
self._monitor_task = asyncio.create_task(
|
|
219
|
+
self._monitor_connection(connection, wait_closed)
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
async def _monitor_connection(self, connection, wait_closed) -> None:
|
|
223
|
+
try:
|
|
224
|
+
await wait_closed()
|
|
225
|
+
except asyncio.CancelledError:
|
|
226
|
+
raise
|
|
227
|
+
except Exception as e: # pragma: no cover - defensive
|
|
228
|
+
logger.debug("SSH tunnel connection close reported an error: %s", e)
|
|
229
|
+
|
|
230
|
+
if self._closed:
|
|
231
|
+
return
|
|
232
|
+
logger.warning(
|
|
233
|
+
"SSH tunnel connection dropped; reconnecting to %s:%s",
|
|
234
|
+
self.config.remote_host,
|
|
235
|
+
self.config.remote_port,
|
|
236
|
+
)
|
|
237
|
+
await self._reconnect()
|
|
238
|
+
|
|
239
|
+
async def _reconnect(self) -> None:
|
|
240
|
+
"""Re-open the forward on the previously bound local port."""
|
|
241
|
+
local_port = self._endpoint.port if self._endpoint else self.config.local_port
|
|
242
|
+
delay = 1.0
|
|
243
|
+
|
|
244
|
+
old_listener = self._listener
|
|
245
|
+
self._listener = None
|
|
246
|
+
if old_listener is not None:
|
|
247
|
+
old_listener.close()
|
|
248
|
+
try:
|
|
249
|
+
await old_listener.wait_closed()
|
|
250
|
+
except Exception as e: # pragma: no cover - best effort
|
|
251
|
+
logger.debug("Error while closing the dropped tunnel listener: %s", e)
|
|
252
|
+
|
|
253
|
+
old_manager = self._manager
|
|
254
|
+
self._manager = None
|
|
255
|
+
if old_manager is not None:
|
|
256
|
+
try:
|
|
257
|
+
await old_manager.disconnect()
|
|
258
|
+
except Exception as e: # pragma: no cover - best effort
|
|
259
|
+
logger.debug("Error while discarding the dropped SSH tunnel: %s", e)
|
|
260
|
+
|
|
261
|
+
while not self._closed:
|
|
262
|
+
try:
|
|
263
|
+
await self._open(local_port)
|
|
264
|
+
except Exception as e:
|
|
265
|
+
self.down = True
|
|
266
|
+
self.last_error = str(e)
|
|
267
|
+
logger.error("SSH tunnel is down (%s); retrying in %.0fs", e, delay)
|
|
268
|
+
try:
|
|
269
|
+
await asyncio.sleep(delay)
|
|
270
|
+
except asyncio.CancelledError:
|
|
271
|
+
raise
|
|
272
|
+
delay = min(delay * 2, 30.0)
|
|
273
|
+
continue
|
|
274
|
+
|
|
275
|
+
self.down = False
|
|
276
|
+
self.last_error = None
|
|
277
|
+
self._start_monitor()
|
|
278
|
+
return
|
|
279
|
+
|
|
280
|
+
async def close(self) -> None:
|
|
281
|
+
"""Close the listener and the SSH connection (idempotent)."""
|
|
282
|
+
self._closed = True
|
|
283
|
+
monitor = self._monitor_task
|
|
284
|
+
self._monitor_task = None
|
|
285
|
+
if monitor is not None:
|
|
286
|
+
monitor.cancel()
|
|
287
|
+
try:
|
|
288
|
+
await monitor
|
|
289
|
+
except asyncio.CancelledError:
|
|
290
|
+
pass
|
|
291
|
+
except Exception as e: # pragma: no cover - best effort
|
|
292
|
+
logger.debug("Error while stopping the SSH tunnel monitor: %s", e)
|
|
293
|
+
|
|
294
|
+
listener = self._listener
|
|
295
|
+
self._listener = None
|
|
296
|
+
if listener is not None:
|
|
297
|
+
listener.close()
|
|
298
|
+
try:
|
|
299
|
+
await listener.wait_closed()
|
|
300
|
+
except Exception as e: # pragma: no cover - best effort
|
|
301
|
+
logger.debug("Error while closing SSH tunnel listener: %s", e)
|
|
302
|
+
|
|
303
|
+
manager = self._manager
|
|
304
|
+
self._manager = None
|
|
305
|
+
if manager is not None:
|
|
306
|
+
await manager.disconnect()
|
|
307
|
+
|
|
308
|
+
async def __aenter__(self) -> TunnelEndpoint:
|
|
309
|
+
return await self.start()
|
|
310
|
+
|
|
311
|
+
async def __aexit__(self, exc_type, exc_val, exc_tb) -> None:
|
|
312
|
+
await self.close()
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
@dataclass
|
|
316
|
+
class ActiveTunnel:
|
|
317
|
+
"""A tunnel opened from configuration and the endpoint it replaced."""
|
|
318
|
+
|
|
319
|
+
section: str
|
|
320
|
+
tunnel: SSHTunnel
|
|
321
|
+
endpoint: TunnelEndpoint
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
async def open_metric_tunnels(config: dict[str, Any]) -> list[ActiveTunnel]:
|
|
325
|
+
"""Open tunnels for enabled ``influxdb``/``prometheus`` sections.
|
|
326
|
+
|
|
327
|
+
The section's ``host``/``port`` are rewritten to the local tunnel endpoint
|
|
328
|
+
so the existing HTTP clients can use them unchanged. When any tunnel
|
|
329
|
+
fails, all previously opened tunnels are closed before the error is
|
|
330
|
+
re-raised.
|
|
331
|
+
"""
|
|
332
|
+
ssh_config = config.get("ssh", {}) or {}
|
|
333
|
+
pending: list[tuple[str, dict[str, Any], dict[str, Any], int]] = []
|
|
334
|
+
|
|
335
|
+
# Validate every configured tunnel before opening any of them so a typo
|
|
336
|
+
# cannot leave an earlier tunnel running.
|
|
337
|
+
for section, default_port in METRIC_SECTIONS:
|
|
338
|
+
section_config = config.get(section)
|
|
339
|
+
if not isinstance(section_config, dict):
|
|
340
|
+
continue
|
|
341
|
+
if not section_config.get("enabled", False):
|
|
342
|
+
continue
|
|
343
|
+
tunnel_config = section_config.get("ssh_tunnel")
|
|
344
|
+
if tunnel_config is None or tunnel_config is False:
|
|
345
|
+
continue
|
|
346
|
+
if not isinstance(tunnel_config, dict):
|
|
347
|
+
raise SSHTunnelError(
|
|
348
|
+
f"{section}.ssh_tunnel must be a mapping with enabled: true"
|
|
349
|
+
)
|
|
350
|
+
if not tunnel_config.get("enabled"):
|
|
351
|
+
continue
|
|
352
|
+
pending.append((section, section_config, tunnel_config, default_port))
|
|
353
|
+
|
|
354
|
+
active: list[ActiveTunnel] = []
|
|
355
|
+
try:
|
|
356
|
+
for section, section_config, tunnel_config, default_port in pending:
|
|
357
|
+
tunnel = SSHTunnel(
|
|
358
|
+
ssh_config,
|
|
359
|
+
tunnel_config,
|
|
360
|
+
default_remote_port=section_config.get("port") or default_port,
|
|
361
|
+
)
|
|
362
|
+
endpoint = await tunnel.start()
|
|
363
|
+
section_config["host"] = endpoint.host
|
|
364
|
+
section_config["port"] = endpoint.port
|
|
365
|
+
active.append(ActiveTunnel(section, tunnel, endpoint))
|
|
366
|
+
except Exception:
|
|
367
|
+
await close_metric_tunnels(active)
|
|
368
|
+
raise
|
|
369
|
+
|
|
370
|
+
return active
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
async def close_metric_tunnels(tunnels: list[ActiveTunnel]) -> None:
|
|
374
|
+
"""Close every tunnel opened by :func:`open_metric_tunnels`."""
|
|
375
|
+
while tunnels:
|
|
376
|
+
active = tunnels.pop()
|
|
377
|
+
try:
|
|
378
|
+
await active.tunnel.close()
|
|
379
|
+
except Exception as e: # pragma: no cover - best effort
|
|
380
|
+
logger.debug("Error while closing %s tunnel: %s", active.section, e)
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: server-management-lib
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Shared security, SSH/local execution, configuration and HTTP clients for the Server MCP suite.
|
|
5
|
+
Author-email: Alessio Elmi <alessio.elmi@gmail.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/elmuz/server-mcp
|
|
8
|
+
Project-URL: Repository, https://github.com/elmuz/server-mcp
|
|
9
|
+
Project-URL: Documentation, https://github.com/elmuz/server-mcp/tree/main/docs
|
|
10
|
+
Project-URL: Issues, https://github.com/elmuz/server-mcp/issues
|
|
11
|
+
Project-URL: Changelog, https://github.com/elmuz/server-mcp/blob/main/CHANGELOG.md
|
|
12
|
+
Keywords: mcp,model-context-protocol,linux,server-management,docker,homelab,self-hosting
|
|
13
|
+
Classifier: Development Status :: 4 - Beta
|
|
14
|
+
Classifier: Environment :: Console
|
|
15
|
+
Classifier: Intended Audience :: System Administrators
|
|
16
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
22
|
+
Classifier: Topic :: System :: Systems Administration
|
|
23
|
+
Classifier: Typing :: Typed
|
|
24
|
+
Requires-Python: >=3.11
|
|
25
|
+
Description-Content-Type: text/markdown
|
|
26
|
+
Requires-Dist: asyncssh<3,>=2.22
|
|
27
|
+
Requires-Dist: pyyaml>=6.0
|
|
28
|
+
Requires-Dist: aiohttp>=3.9.0
|
|
29
|
+
Requires-Dist: python-dotenv>=1.0.0
|
|
30
|
+
Requires-Dist: sqlparse>=0.5
|
|
31
|
+
Requires-Dist: pydantic>=2.12
|
|
32
|
+
|
|
33
|
+
# server-management-lib
|
|
34
|
+
|
|
35
|
+
Shared core for the Server MCP suite: security validation, SSH and local executors,
|
|
36
|
+
configuration resolution, output limits, audit logging, and read-only InfluxDB/Prometheus
|
|
37
|
+
clients.
|
|
38
|
+
|
|
39
|
+
Part of the [Server MCP suite](../../README.md).
|
|
40
|
+
|
|
41
|
+
- [Architecture](../../docs/architecture.md)
|
|
42
|
+
- [Security model](../../docs/security-model.md)
|
|
43
|
+
- [Configuration](../../docs/configuration.md)
|
|
44
|
+
|
|
45
|
+
The two MCP servers depend on this package through the uv workspace. It has no MCP
|
|
46
|
+
dependency and can be used by other server-management tools.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
server_management_lib/__init__.py,sha256=CC39UORohKcFBrIAf9RVMrMmE9_xx46V5kZF3ByUcAM,2883
|
|
2
|
+
server_management_lib/audit.py,sha256=hcMGLW04aHdeqhC_fX5obbxAUd2FIaCsJgF6dvkJzj4,2791
|
|
3
|
+
server_management_lib/base.py,sha256=moZdJKvaVtlLlNxguDjwq_XCv2DWS7r2djCfnFyQNak,1510
|
|
4
|
+
server_management_lib/config.py,sha256=rykN_Hx5VtX4ssrJHd2DI-7H0CYbfv5G4Tq1Xyy_2CE,13185
|
|
5
|
+
server_management_lib/executor.py,sha256=Npebfva17WK9lvNgGNfpIsPh0npellQH-USOb5Xl0sc,1539
|
|
6
|
+
server_management_lib/http_clients.py,sha256=nXmTgKGWYVdpcUg45oafwFZdHc_3PN9ll4GQln4vq40,24102
|
|
7
|
+
server_management_lib/local_executor.py,sha256=WzEgYQBnZQ5FB8TVb0hwWsjy_7uUYG6VbliNX0GWBdM,7037
|
|
8
|
+
server_management_lib/operations.py,sha256=9u51YxawFeEkWMd47WC8NAePkCNx4EzrMWpl06gr30E,1017
|
|
9
|
+
server_management_lib/output.py,sha256=Z94OzAg5bkeCX70Sm7Ijvie5rXqej-moH7C7aB9iW4s,1980
|
|
10
|
+
server_management_lib/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
11
|
+
server_management_lib/rate_limit.py,sha256=K2UJt1L1_kfd92b1UjnIhoR9SrNK2tr2zUilm1kxsTI,3826
|
|
12
|
+
server_management_lib/redaction.py,sha256=sEsT_Nw537JotKU-UsG8Jre1n0YERh5ZIm9mHcofFU8,6390
|
|
13
|
+
server_management_lib/responses.py,sha256=XnFyTRSQ_pNQx8dx8jIDVUqt9Qh9LOldKZuQFzG3gpE,4481
|
|
14
|
+
server_management_lib/security.py,sha256=F969RKY2oif3bp5JQLdaPDMS2bBS2-5a-h_lMR04iQ8,56146
|
|
15
|
+
server_management_lib/ssh_manager.py,sha256=6-2nNGtzDr18lJa0dw8SCW9cDq8Jzua810rBPNyG18w,16339
|
|
16
|
+
server_management_lib/transport.py,sha256=vmfRqdSZc3EuCJEnGm-D16HjGXUr1XPCaN_haVFCGlA,4187
|
|
17
|
+
server_management_lib/tunnel.py,sha256=tmhTrgYWjKfXi--SXc6pJp4Y47EyFEef1ZfFWSSuvHs,13290
|
|
18
|
+
server_management_lib-0.2.0.dist-info/METADATA,sha256=JWOOz6TpSjnRej5lGwG7BMkzo15j2pmhTTIFDWD9Ifk,1989
|
|
19
|
+
server_management_lib-0.2.0.dist-info/WHEEL,sha256=mOdXR9C3wMVov7cSY8PY-XCzo3SmmWCHvGQr5csJoao,91
|
|
20
|
+
server_management_lib-0.2.0.dist-info/top_level.txt,sha256=03qQ0Fx0rrnN6WBTUgRUmoZkiv1oMn6oStik9gl12tk,22
|
|
21
|
+
server_management_lib-0.2.0.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
server_management_lib
|