vssh 3.3.1__tar.gz → 3.3.6__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.
- {vssh-3.3.1/vssh.egg-info → vssh-3.3.6}/PKG-INFO +3 -3
- {vssh-3.3.1 → vssh-3.3.6}/README.md +2 -2
- {vssh-3.3.1 → vssh-3.3.6}/pyproject.toml +5 -2
- {vssh-3.3.1 → vssh-3.3.6/vssh.egg-info}/PKG-INFO +3 -3
- {vssh-3.3.1 → vssh-3.3.6}/vssh.egg-info/SOURCES.txt +0 -2
- {vssh-3.3.1 → vssh-3.3.6}/vssh.egg-info/entry_points.txt +3 -0
- {vssh-3.3.1 → vssh-3.3.6}/vssh.egg-info/top_level.txt +0 -2
- {vssh-3.3.1 → vssh-3.3.6}/vssh.py +187 -17
- {vssh-3.3.1 → vssh-3.3.6}/vssh_mcp_server.py +140 -35
- {vssh-3.3.1 → vssh-3.3.6}/vssh_p2p.py +2 -2
- vssh-3.3.1/vssh_p2p_fast.py +0 -500
- vssh-3.3.1/vssh_quic.py +0 -436
- {vssh-3.3.1 → vssh-3.3.6}/LICENSE +0 -0
- {vssh-3.3.1 → vssh-3.3.6}/setup.cfg +0 -0
- {vssh-3.3.1 → vssh-3.3.6}/vssh.egg-info/dependency_links.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: vssh
|
|
3
|
-
Version: 3.3.
|
|
3
|
+
Version: 3.3.6
|
|
4
4
|
Summary: Secure SSH/SCP tool with Tailscale failover, P2P transport, and MCP server
|
|
5
5
|
Author-email: MeshPOP <mpop@mpop.dev>
|
|
6
6
|
License: MIT
|
|
@@ -41,10 +41,10 @@ pip install vssh
|
|
|
41
41
|
|
|
42
42
|
```bash
|
|
43
43
|
# Execute remote command
|
|
44
|
-
vssh exec
|
|
44
|
+
vssh exec relay1 "uptime"
|
|
45
45
|
|
|
46
46
|
# Transfer file at 50+ MB/s
|
|
47
|
-
vssh put
|
|
47
|
+
vssh put relay1 ./deploy.tar.gz /opt/deploy.tar.gz
|
|
48
48
|
|
|
49
49
|
# Check connection status
|
|
50
50
|
vssh status
|
|
@@ -18,10 +18,10 @@ pip install vssh
|
|
|
18
18
|
|
|
19
19
|
```bash
|
|
20
20
|
# Execute remote command
|
|
21
|
-
vssh exec
|
|
21
|
+
vssh exec relay1 "uptime"
|
|
22
22
|
|
|
23
23
|
# Transfer file at 50+ MB/s
|
|
24
|
-
vssh put
|
|
24
|
+
vssh put relay1 ./deploy.tar.gz /opt/deploy.tar.gz
|
|
25
25
|
|
|
26
26
|
# Check connection status
|
|
27
27
|
vssh status
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "vssh"
|
|
7
|
-
version = "3.3.
|
|
7
|
+
version = "3.3.6"
|
|
8
8
|
description = "Secure SSH/SCP tool with Tailscale failover, P2P transport, and MCP server"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = {text = "MIT"}
|
|
@@ -34,5 +34,8 @@ vssh-mcp = "vssh_mcp_server:main"
|
|
|
34
34
|
Homepage = "https://github.com/meshpop/vssh"
|
|
35
35
|
Repository = "https://github.com/meshpop/vssh"
|
|
36
36
|
|
|
37
|
+
[project.entry-points."meshpop.mcp"]
|
|
38
|
+
vssh = "vssh_mcp_server"
|
|
39
|
+
|
|
37
40
|
[tool.setuptools]
|
|
38
|
-
py-modules = ["vssh", "vssh_p2p", "
|
|
41
|
+
py-modules = ["vssh", "vssh_p2p", "vssh_mcp_server"]
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: vssh
|
|
3
|
-
Version: 3.3.
|
|
3
|
+
Version: 3.3.6
|
|
4
4
|
Summary: Secure SSH/SCP tool with Tailscale failover, P2P transport, and MCP server
|
|
5
5
|
Author-email: MeshPOP <mpop@mpop.dev>
|
|
6
6
|
License: MIT
|
|
@@ -41,10 +41,10 @@ pip install vssh
|
|
|
41
41
|
|
|
42
42
|
```bash
|
|
43
43
|
# Execute remote command
|
|
44
|
-
vssh exec
|
|
44
|
+
vssh exec relay1 "uptime"
|
|
45
45
|
|
|
46
46
|
# Transfer file at 50+ MB/s
|
|
47
|
-
vssh put
|
|
47
|
+
vssh put relay1 ./deploy.tar.gz /opt/deploy.tar.gz
|
|
48
48
|
|
|
49
49
|
# Check connection status
|
|
50
50
|
vssh status
|
|
@@ -35,7 +35,14 @@ def _load_vssh_secret():
|
|
|
35
35
|
s = sf.read_text().strip()
|
|
36
36
|
if s: return s
|
|
37
37
|
conf = _load_vssh_config()
|
|
38
|
-
|
|
38
|
+
s = conf.get('SECRET', '')
|
|
39
|
+
if s: return s
|
|
40
|
+
# Auto-generate on first install
|
|
41
|
+
import secrets as _sec
|
|
42
|
+
s = _sec.token_hex(16)
|
|
43
|
+
VSSH_DIR_CONF.mkdir(parents=True, exist_ok=True)
|
|
44
|
+
sf.write_text(s)
|
|
45
|
+
return s
|
|
39
46
|
|
|
40
47
|
VSSH_CONFIG = _load_vssh_config()
|
|
41
48
|
SECRET = _load_vssh_secret()
|
|
@@ -68,7 +75,7 @@ def _load_tailscale_map() -> dict:
|
|
|
68
75
|
if len(parts) >= 2:
|
|
69
76
|
ts_map[parts[0]] = parts[1] # wire_ip tailscale_ip
|
|
70
77
|
except (OSError, ValueError) as e:
|
|
71
|
-
pass #
|
|
78
|
+
pass # e silenced
|
|
72
79
|
|
|
73
80
|
return ts_map
|
|
74
81
|
|
|
@@ -103,7 +110,7 @@ def vssh_connect(host: str, timeout: float = 5.0) -> socket.socket:
|
|
|
103
110
|
sock.connect((host, PORT))
|
|
104
111
|
return sock
|
|
105
112
|
except (ConnectionRefusedError, ConnectionError, OSError, socket.timeout) as wire_err:
|
|
106
|
-
pass #
|
|
113
|
+
pass # wire_err silenced
|
|
107
114
|
|
|
108
115
|
# Fallback: try Tailscale IP
|
|
109
116
|
ts_ip = TAILSCALE_MAP.get(host)
|
|
@@ -380,7 +387,7 @@ def get_public_ip() -> str:
|
|
|
380
387
|
except (urllib.error.URLError, OSError):
|
|
381
388
|
continue
|
|
382
389
|
except Exception as e:
|
|
383
|
-
pass #
|
|
390
|
+
pass # e silenced
|
|
384
391
|
return ''
|
|
385
392
|
|
|
386
393
|
def get_network_info_local() -> dict:
|
|
@@ -435,7 +442,7 @@ def is_safe_network(host: str) -> Tuple[bool, str]:
|
|
|
435
442
|
if 16 <= second <= 31:
|
|
436
443
|
return True, "lan"
|
|
437
444
|
except (ValueError, IndexError) as e:
|
|
438
|
-
pass #
|
|
445
|
+
pass # e silenced
|
|
439
446
|
|
|
440
447
|
# Everything else is public IP - UNSAFE!
|
|
441
448
|
return False, "public_ip_unencrypted"
|
|
@@ -499,7 +506,7 @@ def resolve_name(host: str) -> str:
|
|
|
499
506
|
if host in _name_cache:
|
|
500
507
|
return _name_cache[host]
|
|
501
508
|
break # got peers, just no match
|
|
502
|
-
except (OSError,
|
|
509
|
+
except (OSError, ValueError, TimeoutError):
|
|
503
510
|
continue
|
|
504
511
|
return host
|
|
505
512
|
|
|
@@ -748,7 +755,7 @@ def _rpc_get_logs(payload: dict) -> dict:
|
|
|
748
755
|
)
|
|
749
756
|
return {'service': service, 'lines': r.stdout.strip().split('\n')}
|
|
750
757
|
except (subprocess.SubprocessError, OSError) as e:
|
|
751
|
-
pass #
|
|
758
|
+
pass # e silenced
|
|
752
759
|
|
|
753
760
|
path = log_paths.get(service)
|
|
754
761
|
if path and os.path.exists(path):
|
|
@@ -2421,11 +2428,11 @@ def pty_session(host: str, name: str = ''):
|
|
|
2421
2428
|
sys.stdout.buffer.write(data)
|
|
2422
2429
|
sys.stdout.buffer.flush()
|
|
2423
2430
|
except (BlockingIOError) as e:
|
|
2424
|
-
pass #
|
|
2431
|
+
pass # e silenced
|
|
2425
2432
|
except (EOFError, ConnectionError):
|
|
2426
2433
|
raise EOFError
|
|
2427
2434
|
except (EOFError, KeyboardInterrupt, ConnectionError, BrokenPipeError) as e:
|
|
2428
|
-
pass #
|
|
2435
|
+
pass # e silenced
|
|
2429
2436
|
finally:
|
|
2430
2437
|
# Restore terminal
|
|
2431
2438
|
if old_settings:
|
|
@@ -2588,7 +2595,7 @@ class VsshSession:
|
|
|
2588
2595
|
self.sock.sendall(f"CLOSE:{self.session_id}\n".encode())
|
|
2589
2596
|
self.sock.recv(32) # CLOSE_OK
|
|
2590
2597
|
except OSError as e:
|
|
2591
|
-
pass #
|
|
2598
|
+
pass # e silenced
|
|
2592
2599
|
finally:
|
|
2593
2600
|
self.sock.close()
|
|
2594
2601
|
self.sock = None
|
|
@@ -2961,6 +2968,57 @@ def server():
|
|
|
2961
2968
|
result = subprocess.run(cmdline, shell=True, capture_output=True, timeout=60)
|
|
2962
2969
|
conn.sendall(result.stdout + result.stderr + b'__END__')
|
|
2963
2970
|
|
|
2971
|
+
elif cmd == 'PTY_SESSION':
|
|
2972
|
+
# PTY_SESSION:auth:rows:cols → full bash PTY relay
|
|
2973
|
+
try:
|
|
2974
|
+
rows = int(parts[2]) if len(parts) > 2 else 24
|
|
2975
|
+
cols = int(parts[3]) if len(parts) > 3 else 80
|
|
2976
|
+
except (IndexError, ValueError):
|
|
2977
|
+
rows, cols = 24, 80
|
|
2978
|
+
conn.sendall(b'PTY_OK\n')
|
|
2979
|
+
print(f'[PTY] {addr[0]} {rows}x{cols}')
|
|
2980
|
+
import pty as _pty, select as _sel, fcntl as _fcntl
|
|
2981
|
+
import termios as _ter, struct as _stru
|
|
2982
|
+
mfd, sfd = _pty.openpty()
|
|
2983
|
+
_fcntl.ioctl(sfd, _ter.TIOCSWINSZ,
|
|
2984
|
+
_stru.pack('HHHH', rows, cols, 0, 0))
|
|
2985
|
+
proc = subprocess.Popen(
|
|
2986
|
+
['/bin/bash', '-l'],
|
|
2987
|
+
stdin=sfd, stdout=sfd, stderr=sfd,
|
|
2988
|
+
preexec_fn=os.setsid, close_fds=True
|
|
2989
|
+
)
|
|
2990
|
+
os.close(sfd)
|
|
2991
|
+
cfd = conn.fileno()
|
|
2992
|
+
try:
|
|
2993
|
+
while proc.poll() is None:
|
|
2994
|
+
r, _, _ = _sel.select([cfd, mfd], [], [], 0.05)
|
|
2995
|
+
if cfd in r:
|
|
2996
|
+
data = conn.recv(4096)
|
|
2997
|
+
if not data:
|
|
2998
|
+
break
|
|
2999
|
+
try:
|
|
3000
|
+
os.write(mfd, data)
|
|
3001
|
+
except OSError:
|
|
3002
|
+
break
|
|
3003
|
+
if mfd in r:
|
|
3004
|
+
try:
|
|
3005
|
+
data = os.read(mfd, 4096)
|
|
3006
|
+
if data:
|
|
3007
|
+
conn.sendall(data)
|
|
3008
|
+
except OSError:
|
|
3009
|
+
break
|
|
3010
|
+
finally:
|
|
3011
|
+
try:
|
|
3012
|
+
proc.terminate()
|
|
3013
|
+
proc.wait(timeout=2)
|
|
3014
|
+
except Exception:
|
|
3015
|
+
pass
|
|
3016
|
+
try:
|
|
3017
|
+
os.close(mfd)
|
|
3018
|
+
except Exception:
|
|
3019
|
+
pass
|
|
3020
|
+
print(f'[PTY] {addr[0]} session ended')
|
|
3021
|
+
|
|
2964
3022
|
elif cmd == 'INFO':
|
|
2965
3023
|
conn.sendall(b'OK\n')
|
|
2966
3024
|
|
|
@@ -3213,7 +3271,7 @@ def server():
|
|
|
3213
3271
|
try:
|
|
3214
3272
|
payload = json.loads(payload_data.decode())
|
|
3215
3273
|
except (json.JSONDecodeError, ValueError, UnicodeDecodeError) as e:
|
|
3216
|
-
pass #
|
|
3274
|
+
pass # e silenced
|
|
3217
3275
|
|
|
3218
3276
|
result = handle_rpc(method, payload, sender_ip=addr[0])
|
|
3219
3277
|
conn.sendall(f'RPC_RESULT:{len(result)}\n'.encode())
|
|
@@ -3279,8 +3337,118 @@ def server():
|
|
|
3279
3337
|
except KeyboardInterrupt:
|
|
3280
3338
|
print('\nStopped')
|
|
3281
3339
|
|
|
3340
|
+
|
|
3341
|
+
def _cmd_install(args):
|
|
3342
|
+
"""Install vssh as a system daemon (systemd on Linux, LaunchAgent on macOS)."""
|
|
3343
|
+
import sys as _sys, os as _os, platform as _plat, subprocess as _sp, stat as _stat
|
|
3344
|
+
|
|
3345
|
+
python_exe = _sys.executable
|
|
3346
|
+
vssh_py = _os.path.abspath(__file__)
|
|
3347
|
+
secret = _load_vssh_secret()
|
|
3348
|
+
|
|
3349
|
+
print(f"[vssh install]")
|
|
3350
|
+
print(f" python : {python_exe}")
|
|
3351
|
+
print(f" vssh.py : {vssh_py}")
|
|
3352
|
+
print(f" secret : {secret[:6]}...{secret[-4:]} ({'auto-generated' if not _os.environ.get('VSSH_SECRET') else 'from env'})")
|
|
3353
|
+
|
|
3354
|
+
if _plat.system() == 'Linux':
|
|
3355
|
+
service = f"""[Unit]
|
|
3356
|
+
Description=vssh daemon
|
|
3357
|
+
After=network.target
|
|
3358
|
+
|
|
3359
|
+
[Service]
|
|
3360
|
+
Type=simple
|
|
3361
|
+
ExecStart={python_exe} {vssh_py} server
|
|
3362
|
+
Environment="VSSH_SECRET={secret}"
|
|
3363
|
+
Restart=always
|
|
3364
|
+
RestartSec=5
|
|
3365
|
+
StandardOutput=journal
|
|
3366
|
+
StandardError=journal
|
|
3367
|
+
|
|
3368
|
+
[Install]
|
|
3369
|
+
WantedBy=multi-user.target
|
|
3370
|
+
"""
|
|
3371
|
+
svc_path = '/etc/systemd/system/vssh.service'
|
|
3372
|
+
try:
|
|
3373
|
+
with open(svc_path, 'w') as f:
|
|
3374
|
+
f.write(service)
|
|
3375
|
+
print(f" Created : {svc_path}")
|
|
3376
|
+
_sp.run(['systemctl', 'daemon-reload'], check=True, capture_output=True)
|
|
3377
|
+
_sp.run(['systemctl', 'enable', 'vssh'], check=True, capture_output=True)
|
|
3378
|
+
_sp.run(['systemctl', 'restart', 'vssh'], check=True, capture_output=True)
|
|
3379
|
+
import time; time.sleep(1)
|
|
3380
|
+
r = _sp.run(['systemctl', 'is-active', 'vssh'], capture_output=True, text=True)
|
|
3381
|
+
status = r.stdout.strip()
|
|
3382
|
+
print(f" Service : {status}")
|
|
3383
|
+
if status == 'active':
|
|
3384
|
+
print("[vssh install] ✓ Done. vssh daemon running.")
|
|
3385
|
+
else:
|
|
3386
|
+
print("[vssh install] ✗ Service not active — check: journalctl -u vssh -n 20")
|
|
3387
|
+
except PermissionError:
|
|
3388
|
+
print("[vssh install] ✗ Need root. Run: sudo vssh install")
|
|
3389
|
+
except Exception as e:
|
|
3390
|
+
print(f"[vssh install] ✗ Error: {e}")
|
|
3391
|
+
|
|
3392
|
+
elif _plat.system() == 'Darwin':
|
|
3393
|
+
home = _os.path.expanduser('~')
|
|
3394
|
+
plist_dir = _os.path.join(home, 'Library', 'LaunchAgents')
|
|
3395
|
+
_os.makedirs(plist_dir, exist_ok=True)
|
|
3396
|
+
plist_path = _os.path.join(plist_dir, 'com.vssh.server.plist')
|
|
3397
|
+
plist = f"""<?xml version="1.0" encoding="UTF-8"?>
|
|
3398
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
3399
|
+
<plist version="1.0">
|
|
3400
|
+
<dict>
|
|
3401
|
+
<key>Label</key>
|
|
3402
|
+
<string>com.vssh.server</string>
|
|
3403
|
+
<key>ProgramArguments</key>
|
|
3404
|
+
<array>
|
|
3405
|
+
<string>{python_exe}</string>
|
|
3406
|
+
<string>{vssh_py}</string>
|
|
3407
|
+
<string>server</string>
|
|
3408
|
+
</array>
|
|
3409
|
+
<key>EnvironmentVariables</key>
|
|
3410
|
+
<dict>
|
|
3411
|
+
<key>VSSH_SECRET</key>
|
|
3412
|
+
<string>{secret}</string>
|
|
3413
|
+
</dict>
|
|
3414
|
+
<key>RunAtLoad</key>
|
|
3415
|
+
<true/>
|
|
3416
|
+
<key>KeepAlive</key>
|
|
3417
|
+
<true/>
|
|
3418
|
+
<key>StandardOutPath</key>
|
|
3419
|
+
<string>/tmp/vssh.log</string>
|
|
3420
|
+
<key>StandardErrorPath</key>
|
|
3421
|
+
<string>/tmp/vssh.err</string>
|
|
3422
|
+
</dict>
|
|
3423
|
+
</plist>"""
|
|
3424
|
+
# Unload existing if any
|
|
3425
|
+
_sp.run(['launchctl', 'unload', plist_path], capture_output=True)
|
|
3426
|
+
with open(plist_path, 'w') as f:
|
|
3427
|
+
f.write(plist)
|
|
3428
|
+
print(f" Created : {plist_path}")
|
|
3429
|
+
r = _sp.run(['launchctl', 'load', plist_path], capture_output=True, text=True)
|
|
3430
|
+
if r.returncode == 0:
|
|
3431
|
+
import time; time.sleep(1)
|
|
3432
|
+
r2 = _sp.run(['pgrep', '-f', 'vssh.py server'], capture_output=True, text=True)
|
|
3433
|
+
if r2.stdout.strip():
|
|
3434
|
+
print("[vssh install] ✓ Done. vssh daemon running.")
|
|
3435
|
+
else:
|
|
3436
|
+
print("[vssh install] ✗ Process not found — check: cat /tmp/vssh.err")
|
|
3437
|
+
else:
|
|
3438
|
+
print(f"[vssh install] ✗ launchctl error: {r.stderr.strip()}")
|
|
3439
|
+
else:
|
|
3440
|
+
print(f"[vssh install] ✗ Unsupported OS: {_plat.system()}")
|
|
3441
|
+
|
|
3282
3442
|
def main():
|
|
3283
3443
|
args = sys.argv[1:]
|
|
3444
|
+
# Handle --version and --help flags
|
|
3445
|
+
if args and args[0] in ("--version", "-v", "-V"):
|
|
3446
|
+
try:
|
|
3447
|
+
from importlib.metadata import version as _v
|
|
3448
|
+
print(f"vssh v{_v('vssh')}")
|
|
3449
|
+
except Exception:
|
|
3450
|
+
print("vssh v3.3.2")
|
|
3451
|
+
return
|
|
3284
3452
|
if not args:
|
|
3285
3453
|
print('''vssh v3 - Distributed command & file transport daemon for server meshes
|
|
3286
3454
|
|
|
@@ -3469,7 +3637,7 @@ Env: VSSH_SECRET
|
|
|
3469
3637
|
try:
|
|
3470
3638
|
days = int(args[1])
|
|
3471
3639
|
except (ValueError, TypeError) as e:
|
|
3472
|
-
pass #
|
|
3640
|
+
pass # e silenced
|
|
3473
3641
|
stats = get_transfer_stats(days)
|
|
3474
3642
|
print(f'Transfer Statistics (last {days} days)')
|
|
3475
3643
|
print(f' Total transfers: {stats["total"]}')
|
|
@@ -3586,6 +3754,8 @@ Env: VSSH_SECRET
|
|
|
3586
3754
|
sys.exit(0 if result['fail'] == 0 else 1)
|
|
3587
3755
|
elif cmd == 'server':
|
|
3588
3756
|
server()
|
|
3757
|
+
elif cmd == 'install':
|
|
3758
|
+
_cmd_install(args)
|
|
3589
3759
|
elif cmd == 'help' or cmd == '-h' or cmd == '--help':
|
|
3590
3760
|
main_args_bak = sys.argv[1:]
|
|
3591
3761
|
sys.argv = sys.argv[:1]
|
|
@@ -3607,7 +3777,7 @@ Env: VSSH_SECRET
|
|
|
3607
3777
|
result = {'name': name, 'ip': ip, 'online': False, 'latency': 0}
|
|
3608
3778
|
try:
|
|
3609
3779
|
s = _sock.socket(_sock.AF_INET, _sock.SOCK_STREAM)
|
|
3610
|
-
s.settimeout(
|
|
3780
|
+
s.settimeout(1)
|
|
3611
3781
|
t0 = time.time()
|
|
3612
3782
|
s.connect((ip, 48291))
|
|
3613
3783
|
result['latency'] = (time.time() - t0) * 1000
|
|
@@ -3634,16 +3804,16 @@ Env: VSSH_SECRET
|
|
|
3634
3804
|
try:
|
|
3635
3805
|
result['info'] = json.loads(buf.decode())
|
|
3636
3806
|
except (json.JSONDecodeError, ValueError, UnicodeDecodeError) as e:
|
|
3637
|
-
pass #
|
|
3807
|
+
pass # e silenced
|
|
3638
3808
|
s.close()
|
|
3639
3809
|
except OSError:
|
|
3640
3810
|
pass # safe to ignore
|
|
3641
3811
|
return result
|
|
3642
3812
|
|
|
3643
3813
|
if full_mode:
|
|
3644
|
-
print("vssh
|
|
3814
|
+
print(f"vssh v{_get_vssh_version()} - Cluster Status (full)")
|
|
3645
3815
|
else:
|
|
3646
|
-
print("vssh
|
|
3816
|
+
print(f"vssh v{_get_vssh_version()} - Connection Status")
|
|
3647
3817
|
print("=" * 70)
|
|
3648
3818
|
|
|
3649
3819
|
online = 0
|
|
@@ -3723,7 +3893,7 @@ Env: VSSH_SECRET
|
|
|
3723
3893
|
print(f" {r['name']:6s} {r['ip']:18s} ○ offline")
|
|
3724
3894
|
print(f"\nTotal: {online}/{total} online")
|
|
3725
3895
|
else:
|
|
3726
|
-
# Unknown command -> try as PTY session shortcut (vssh
|
|
3896
|
+
# Unknown command -> try as PTY session shortcut (vssh node3 = vssh session node3)
|
|
3727
3897
|
host = resolve_name(cmd)
|
|
3728
3898
|
rc = pty_session(host, cmd)
|
|
3729
3899
|
if rc != 0:
|