remoteRF-server-testing 0.0.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.
Files changed (44) hide show
  1. remoteRF_server/__init__.py +0 -0
  2. remoteRF_server/common/__init__.py +0 -0
  3. remoteRF_server/common/grpc/__init__.py +1 -0
  4. remoteRF_server/common/grpc/grpc_host_pb2.py +63 -0
  5. remoteRF_server/common/grpc/grpc_host_pb2_grpc.py +97 -0
  6. remoteRF_server/common/grpc/grpc_pb2.py +59 -0
  7. remoteRF_server/common/grpc/grpc_pb2_grpc.py +97 -0
  8. remoteRF_server/common/idl/__init__.py +1 -0
  9. remoteRF_server/common/idl/device_schema.py +39 -0
  10. remoteRF_server/common/idl/pluto_schema.py +174 -0
  11. remoteRF_server/common/idl/schema.py +358 -0
  12. remoteRF_server/common/utils/__init__.py +6 -0
  13. remoteRF_server/common/utils/ansi_codes.py +120 -0
  14. remoteRF_server/common/utils/api_token.py +21 -0
  15. remoteRF_server/common/utils/db_connection.py +35 -0
  16. remoteRF_server/common/utils/db_location.py +24 -0
  17. remoteRF_server/common/utils/list_string.py +5 -0
  18. remoteRF_server/common/utils/process_arg.py +80 -0
  19. remoteRF_server/drivers/__init__.py +0 -0
  20. remoteRF_server/drivers/adalm_pluto/__init__.py +0 -0
  21. remoteRF_server/drivers/adalm_pluto/pluto_remote_server.py +105 -0
  22. remoteRF_server/host/__init__.py +0 -0
  23. remoteRF_server/host/host_auth_token.py +292 -0
  24. remoteRF_server/host/host_directory_store.py +142 -0
  25. remoteRF_server/host/host_tunnel_server.py +1388 -0
  26. remoteRF_server/server/__init__.py +0 -0
  27. remoteRF_server/server/acc_perms.py +317 -0
  28. remoteRF_server/server/cert_provider.py +184 -0
  29. remoteRF_server/server/device_manager.py +688 -0
  30. remoteRF_server/server/grpc_server.py +1023 -0
  31. remoteRF_server/server/reservation.py +811 -0
  32. remoteRF_server/server/rpc_manager.py +104 -0
  33. remoteRF_server/server/user_group_cli.py +723 -0
  34. remoteRF_server/server/user_group_handler.py +1120 -0
  35. remoteRF_server/serverrf_cli.py +1377 -0
  36. remoteRF_server/tools/__init__.py +191 -0
  37. remoteRF_server/tools/gen_certs.py +274 -0
  38. remoteRF_server/tools/gist_status.py +139 -0
  39. remoteRF_server/tools/gist_status_testing.py +67 -0
  40. remoterf_server_testing-0.0.0.dist-info/METADATA +612 -0
  41. remoterf_server_testing-0.0.0.dist-info/RECORD +44 -0
  42. remoterf_server_testing-0.0.0.dist-info/WHEEL +5 -0
  43. remoterf_server_testing-0.0.0.dist-info/entry_points.txt +2 -0
  44. remoterf_server_testing-0.0.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,1023 @@
1
+
2
+ from __future__ import annotations
3
+ import threading
4
+ import socket
5
+ import os
6
+ import io
7
+ import sys
8
+ import time
9
+
10
+ from typing import Tuple
11
+
12
+ # os.system('clear')
13
+
14
+ from concurrent import futures
15
+ from datetime import datetime
16
+
17
+ # Remove warnings
18
+ # import os
19
+ # os.environ.setdefault("GRPC_VERBOSITY", "ERROR")
20
+ # os.environ.setdefault("GRPC_TRACE", "")
21
+
22
+ from pathlib import Path
23
+ import os
24
+ import re
25
+ import time
26
+ from typing import Any
27
+
28
+ import grpc
29
+
30
+ import traceback
31
+ import contextlib
32
+
33
+ from ..common.grpc import *
34
+ from .reservation import reservation_handler
35
+ from .rpc_manager import rpc_manager
36
+ from .acc_perms import perms
37
+ from ..common.utils import *
38
+ from .device_manager import get_all_devices, get_all_devices_str
39
+ from .user_group_cli import make_user_group_local, edit_user_group_local, list_user_groups_local, remove_user_group_local, make_enrollment_codes_local, remove_enrollment_code_local, list_enrollment_codes_local, export_user_groups_csv_local, export_enrollment_codes_csv_local
40
+ from .user_group_handler import user_group_handler, start_usergroup_cleanup_loop
41
+
42
+ from ..host import host_tunnel_server as hts
43
+ from ..host.host_tunnel_server import HostTunnelRegistry, HostTunnelServicer
44
+ from ..common.grpc import grpc_host_pb2_grpc as host_tunnel_pb2_grpc
45
+ from ..common.grpc import grpc_host_pb2 as host_tunnel_pb2
46
+
47
+ from pathlib import Path
48
+ from prompt_toolkit import PromptSession
49
+
50
+ from ..tools.gist_status import start_status_publisher
51
+
52
+ from pathlib import Path
53
+
54
+ def _xdg_config_home() -> Path:
55
+ return Path(os.getenv("XDG_CONFIG_HOME", Path.home() / ".config"))
56
+
57
+ def _cfg_dir() -> Path:
58
+ # same convention as your serverrf CLI
59
+ return Path(os.getenv("REMOTERF_CONFIG_DIR", _xdg_config_home() / "remoterf"))
60
+
61
+ def _server_env_path() -> Path:
62
+ return _cfg_dir() / "server.env"
63
+
64
+ def _read_env_kv(path: Path) -> dict[str, str]:
65
+ out: dict[str, str] = {}
66
+ if not path.exists():
67
+ return out
68
+ for raw in path.read_text(encoding="utf-8").splitlines():
69
+ line = raw.strip()
70
+ if not line or line.startswith("#") or "=" not in line:
71
+ continue
72
+ k, v = line.split("=", 1)
73
+ out[k.strip()] = v.strip().strip('"').strip("'")
74
+ return out
75
+
76
+ def _get_port(name: str, *, default: int | None = None) -> int:
77
+ # precedence: real env var > server.env > default
78
+ v = os.getenv(name)
79
+ if v is None:
80
+ kv = _read_env_kv(_server_env_path())
81
+ v = kv.get(name)
82
+
83
+ if v is None or str(v).strip() == "":
84
+ if default is None:
85
+ raise SystemExit(f"Missing {name}. Set it via serverrf --config or environment.")
86
+ return int(default)
87
+
88
+ try:
89
+ n = int(str(v).strip())
90
+ except Exception:
91
+ raise SystemExit(f"Invalid {name} (expected int): {v!r}")
92
+
93
+ if n <= 0 or n > 65535:
94
+ raise SystemExit(f"{name} out of range (1..65535): {n}")
95
+ return n
96
+
97
+ session = PromptSession()
98
+
99
+ from dotenv import load_dotenv, find_dotenv
100
+ dotenv_path = find_dotenv('.env.server')
101
+ load_dotenv(dotenv_path)
102
+
103
+ s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
104
+ s.connect(("8.8.8.8", 80))
105
+
106
+ local_ip = s.getsockname()[0] # grab the local ip
107
+ # local_port = os.getenv('GRPC_PORT')
108
+ local_port = str(_get_port("GRPC_PORT"))
109
+ cert_port = str(_get_port("CERT_PORT"))
110
+
111
+ # keep dotenv for *other* settings if you want, but ports come from server.env
112
+ dotenv_path = find_dotenv('.env.server')
113
+ load_dotenv(dotenv_path)
114
+
115
+ # print(f"Local IP: {local_ip}")
116
+ # print(f"Local Port: {local_port}")
117
+
118
+ # Implement the GenericRPC service
119
+ class GenericRPCServicer(grpc_pb2_grpc.GenericRPCServicer):
120
+ def Call(self, request, context):
121
+
122
+ # Remote Admin
123
+ # function_name_args = request.function_name.split(':')
124
+ # if function_name_args[0] == "RemoteAdmin":
125
+ # ok, err, caller = verify_remote_admin(request.args)
126
+ # if not ok:
127
+ # # You can also: context.abort(grpc.StatusCode.PERMISSION_DENIED, err)
128
+ # return grpc_pb2.GenericRPCResponse(results={
129
+ # "Ok": map_arg(False),
130
+ # "Error": map_arg(err),
131
+ # })
132
+
133
+ # return grpc_pb2.GenericRPCResponse(
134
+ # results=remote_admin_call(function_name=function_name_args[1], args=request.args)
135
+ # )
136
+
137
+ # Normal RPC
138
+ response = rpc_manager.run_rpc(function_name=request.function_name, args=request.args)
139
+ return grpc_pb2.GenericRPCResponse(results=response)
140
+
141
+
142
+ # Function to start the server
143
+ def serve():
144
+
145
+ options = [
146
+ ('grpc.max_send_message_length', 100 * 1024 * 1024),
147
+ ('grpc.max_receive_message_length', 100 * 1024 * 1024)
148
+ ]
149
+
150
+ server = grpc.server(futures.ThreadPoolExecutor(max_workers=100), options=options)
151
+ grpc_pb2_grpc.add_GenericRPCServicer_to_server(GenericRPCServicer(), server)
152
+
153
+ # For generating server credentials for secure connections
154
+
155
+ # Rasp
156
+ #* Server credentials generation: Add server ip to this line
157
+ #* openssl req -newkey rsa:2048 -nodes -keyout server.key -x509 -days 365 -out server.crt -subj "/CN=192.168.1.169" -addext "subjectAltName=IP:192.168.1.169"
158
+
159
+ #ubuntu VM
160
+ # openssl req -newkey rsa:2048 -nodes -keyout server.key -x509 -days 365 -out server.crt -subj "/CN=192.168.64.5" -addext "subjectAltName=IP:192.168.64.5"
161
+
162
+ #router
163
+ # openssl req -newkey rsa:2048 -nodes -keyout server.key -x509 -days 365 -out server.crt -subj "/CN=128.97.88.21" -addext "subjectAltName=IP:128.97.88.21"
164
+
165
+ #magikarp
166
+ # openssl req -newkey rsa:2048 -nodes -keyout server.key -x509 -days 365 -out server.crt -subj "/CN=192.168.1.109" -addext "subjectAltName=IP:192.168.1.109"
167
+
168
+ #wlab
169
+ # openssl req -newkey rsa:2048 -nodes -keyout server.key -x509 -days 365 -out server.crt -subj "/CN=164.67.195.207" -addext "subjectAltName=IP:164.67.195.207"
170
+
171
+ # Read key and certificate
172
+
173
+ # cert_key = Path(__file__).resolve().parent.parent/'certs'/'server.key'
174
+ # cert_crt = Path(__file__).resolve().parent.parent/'certs'/'server.crt'
175
+
176
+ # with cert_key.open('rb') as f:
177
+ # private_key = f.read()
178
+ # with cert_crt.open('rb') as f:
179
+ # certificate_chain = f.read()
180
+
181
+ # --- TLS certs live in user config (~/.config/remoterf/certs by default) ---
182
+ xdg_config = Path(os.getenv("XDG_CONFIG_HOME", Path.home() / ".config"))
183
+ cert_dir = Path(os.getenv("REMOTERF_CERT_DIR", xdg_config / "remoterf" / "certs"))
184
+
185
+ cert_key = cert_dir / "server.key"
186
+ cert_crt = cert_dir / "server.crt"
187
+
188
+ try:
189
+ if not cert_key.exists() or not cert_crt.exists():
190
+ missing = []
191
+ if not cert_key.exists(): missing.append(str(cert_key))
192
+ if not cert_crt.exists(): missing.append(str(cert_crt))
193
+ raise FileNotFoundError("Missing TLS file(s):\n " + "\n ".join(missing))
194
+
195
+ with cert_key.open("rb") as f:
196
+ private_key = f.read()
197
+ with cert_crt.open("rb") as f:
198
+ certificate_chain = f.read()
199
+
200
+ except Exception as e:
201
+ printf("TLS certs/keys not found or unreadable.", Sty.RED)
202
+ printf("Expected in: ", Sty.DEFAULT, f"{cert_dir}", Sty.MAGENTA)
203
+ printf("Fix: run ", Sty.DEFAULT, "RRRFcerts --ip ", Sty.GREEN, f"{local_ip}", Sty.MAGENTA, " --force", Sty.DEFAULT)
204
+ printf("Or set REMOTERF_CERT_DIR to your cert folder.", Sty.GRAY)
205
+ print(f"\nDetails: {e}\n", file=sys.stderr)
206
+ raise SystemExit(2)
207
+
208
+ # Create server credentials
209
+ server_credentials = grpc.ssl_server_credentials(
210
+ ((private_key, certificate_chain,),))
211
+
212
+ reg = hts.get_tunnel_registry()
213
+ assert reg is not None
214
+
215
+ # publish registry where device_manager expects it
216
+ hts.TUNNEL_REGISTRY = reg
217
+
218
+ host_tunnel_pb2_grpc.add_HostTunnelServicer_to_server(
219
+ HostTunnelServicer(reg),
220
+ server
221
+ )
222
+
223
+ # Inject registry into rpc_manager so it can forward
224
+ # rpc_manager.set_tunnel_registry(registry)
225
+
226
+ # Cert fetcher
227
+ from .cert_provider import start_cert_provider
228
+ cert_server, cert_thread = start_cert_provider(host=local_ip, port=int(cert_port))
229
+
230
+ start_status_publisher()
231
+
232
+ # Add secure port
233
+ server.add_secure_port(f'{local_ip}:{local_port}', server_credentials)
234
+ server.start()
235
+ server.wait_for_termination()
236
+
237
+ def update_devices():
238
+ while True:
239
+ reservation_handler.update_devices()
240
+ time.sleep(60)
241
+
242
+ threading.Thread(target=serve, daemon=True).start()
243
+ threading.Thread(target=update_devices, daemon=True).start()
244
+ start_usergroup_cleanup_loop(interval_sec=60)
245
+
246
+ start_time = datetime.now()
247
+
248
+ def welcome():
249
+ printf(f"Server running nominally.", Sty.BOLD)
250
+ printf(f"Local IP: ", Sty.DEFAULT, f"{local_ip}", Sty.ITALIC, f" | Port: ", Sty.DEFAULT, f"{local_port}", Sty.ITALIC)
251
+ # printf(f" -> Started at ", Sty.DEFAULT, f"{start_time}", Sty.DEFAULT)
252
+ printf("Input ", Sty.DEFAULT, "'help'", Sty.BOLD," for help menu.", Sty.DEFAULT)
253
+
254
+ def clear():
255
+ os.system('clear')
256
+ welcome()
257
+
258
+ def set_acc():
259
+ while True:
260
+ username = session.prompt("Enter username to manage (c to cancel): ")
261
+ if username == 'exit' or username == 'c' or username == 'e':
262
+ return
263
+ perm = perms.get_perms(username)
264
+ if perm == []:
265
+ printf(f"User: {username} does not exist.", Sty.RED)
266
+ else:
267
+ break
268
+
269
+ print(f"User: {username} : <{perm[0][0]}>")
270
+ printf(f"'s'", Sty.MAGENTA, " - Set perm details", Sty.DEFAULT)
271
+ printf(f"'D'", Sty.MAGENTA, " - Delete user", Sty.DEFAULT)
272
+ printf(f"'r'", Sty.MAGENTA, " - Manage reservations", Sty.DEFAULT)
273
+ printf(f"'c'", Sty.MAGENTA, " - Cancel", Sty.DEFAULT)
274
+
275
+ inpu = session.prompt(stylize("Enter command: ", Sty.DEFAULT))
276
+ if inpu == 's':
277
+ inpu2 = session.prompt(stylize("Setting perms: ", Sty.DEFAULT, " (U{User}/P{PowerUser}/A{Admin}) ", Sty.GRAY, ": ", Sty.DEFAULT))
278
+ if inpu2 == 'U':
279
+ perms.set_user(username)
280
+ printf("Set to User. ", Sty.GREEN)
281
+
282
+ elif inpu2 == 'P':
283
+ printf("Setting PowerUser perms. ", Sty.BOLD, "Input 'c' to cancel anytime.", Sty.DEFAULT)
284
+
285
+ while True: # Allowed Devices
286
+ device_ids = session.prompt(stylize("Enter device ids allowed ", Sty.DEFAULT, 'int,int,int,...', Sty.GRAY, ": ", Sty.DEFAULT))
287
+ if device_ids == 'c':
288
+ return
289
+ try:
290
+ device_list = [int(x) for x in device_ids.split(',')]
291
+ if session.prompt(stylize(f"Confirm that this is the correct list of devices: {device_list} ", Sty.DEFAULT, "(y/n)", Sty.GRAY, ": ", Sty.DEFAULT)) == 'y':
292
+ break
293
+ except ValueError:
294
+ printf("Invalid input. Try Again.", Sty.RED)
295
+
296
+ while True: # Max Reservations
297
+ max_res = session.prompt(stylize("Enter max reservations allowed ", Sty.DEFAULT, 'int', Sty.GRAY, ": ", Sty.DEFAULT))
298
+ if max_res == 'c':
299
+ return
300
+ try:
301
+ max_res = int(max_res)
302
+ if session.prompt(stylize(f"Confirm that this is the correct number of max reservations: {max_res} ", Sty.DEFAULT, "(y/n)", Sty.GRAY, ": ", Sty.DEFAULT)) == 'y':
303
+ break
304
+ except ValueError:
305
+ printf("Invalid input. Try Again.", Sty.RED)
306
+
307
+ while True: # Max Reservation Time
308
+ max_res_time = session.prompt(stylize("Enter max length of a single reservation ", Sty.DEFAULT, 'int [seconds]', Sty.GRAY, ": ", Sty.DEFAULT))
309
+ if max_res_time == 'c':
310
+ return
311
+ try:
312
+ max_res_time = int(max_res_time)
313
+ if session.prompt(stylize(f"Confirm that this is the correct max reservation time in SECONDS: {max_res_time} ", Sty.DEFAULT, "(y/n)", Sty.GRAY, ": ", Sty.DEFAULT)) == 'y':
314
+ break
315
+ except ValueError:
316
+ printf("Invalid input. Try Again.", Sty.RED)
317
+
318
+ perms.set_poweruser(username, devices_allowed=device_list, max_reservations=max_res, max_reservation_time_sec=max_res_time)
319
+ elif inpu2 == 'A':
320
+ perms.set_admin(username)
321
+ else:
322
+ printf("Invalid input. Skipping.", Sty.RED)
323
+
324
+ elif inpu == 'D':
325
+ if input(f"Are you sure you want to delete user: {username}? (Y/N): ") == 'Y':
326
+ printf(f"Deleting user: {username}", Sty.MAGENTA)
327
+ reservation_handler.remove_user(username=username)
328
+ else:
329
+ printf("Delete canceled", Sty.GRAY)
330
+ elif inpu == 'r':
331
+ print("Managing reservations:")
332
+ while True:
333
+ inpu = session.prompt(stylize("Enter command: ", Sty.DEFAULT, "'a' to add, 'c' to cancel, 'e' to exit (anytime), 'v' to view. : ", Sty.GRAY))
334
+ if inpu == 'e':
335
+ break
336
+ elif inpu == 'a':
337
+ while True:
338
+ device_id = session.prompt(stylize("Enter device id: ", Sty.DEFAULT))
339
+ if device_id == 'c' or device_id == 'e':
340
+ break
341
+ try:
342
+ device_id = int(device_id)
343
+ if device_id not in get_all_devices():
344
+ printf("Invalid device id.", Sty.GRAY)
345
+ continue
346
+ start_time = session.prompt(stylize("Enter start time: ", Sty.DEFAULT, "YYYY-MM-DD HH:MM:SS -> ", Sty.GRAY))
347
+ end_time = session.prompt(stylize("Enter end time: ", Sty.DEFAULT, "YYYY-MM-DD HH:MM:SS -> ", Sty.GRAY))
348
+ token = reservation_handler.reserve_device(username=username, device_id=device_id, start_time=datetime.strptime(start_time, '%Y-%m-%d %H:%M:%S'), end_time=datetime.strptime(end_time, '%Y-%m-%d %H:%M:%S'), is_server=True)
349
+ api = unmap_arg(token['Token'])
350
+ printf("Reservation added. TOKEN -> ", Sty.GREEN, f"{api}", Sty.MAGENTA)
351
+ break
352
+ except ValueError:
353
+ printf("Invalid input. Try Again.", Sty.GRAY)
354
+ elif inpu == 'v':
355
+ reservations = reservation_handler.grab_all_reservations_server()
356
+ for key, reservation in reservations.items():
357
+ if reservation[0] == username:
358
+ print(f"ID: {key}, User: {reservation[0]}, Device: {reservation[1]}, Start: {reservation[2]}, End: {reservation[3]}")
359
+ if reservations == {}:
360
+ printf("No reservations found.", Sty.GRAY)
361
+ elif inpu == 'c':
362
+ while True:
363
+ try:
364
+ inpu2 = session.prompt(stylize("Enter reservation id to cancel: ", Sty.DEFAULT))
365
+ if inpu2 == 'c' or inpu2 == 'e':
366
+ break
367
+ reservation_handler.remove_reservation(res_id=int(inpu2))
368
+ printf(f"Reservation {inpu2} removed.", Sty.GREEN)
369
+ break
370
+ except Exception as e:
371
+ printf(f"Error: {e}", Sty.GRAY)
372
+
373
+ elif inpu == 'c':
374
+ print("Canceled. Exiting ...")
375
+
376
+ def get_devices():
377
+ data = get_all_devices_str()
378
+
379
+ printf("Devices:", Sty.BOLD)
380
+ for key, value in data.items():
381
+ printf(f"Device ID:", Sty.DEFAULT, f' {key}', Sty.MAGENTA, f" Device Name: ", Sty.DEFAULT, f"{(value)}", Sty.GRAY)
382
+
383
+ def _fmt_ms_ago(ms: int) -> str:
384
+ if not ms:
385
+ return "-"
386
+ now = int(time.time() * 1000)
387
+ dt = max(0, now - int(ms))
388
+ if dt < 1000:
389
+ return f"{dt}ms"
390
+ if dt < 60_000:
391
+ return f"{dt/1000:.1f}s"
392
+ return f"{dt/60_000:.1f}m"
393
+
394
+ def _cmd_hosts_status() -> None:
395
+ reg = hts.get_tunnel_registry()
396
+ assert reg is not None
397
+
398
+ # print("\n[hosts status] registry debug")
399
+ # print(f" hts module: {hts.__name__}")
400
+ # reg = hts.get_tunnel_registry(create=False)
401
+ # print(f" registry id: {id(reg) if reg else None}\n")
402
+
403
+ hosts = reg.list_hosts() # host_id -> HostStatus
404
+ devices = reg.list_devices() # device_id -> DeviceStatus
405
+
406
+ # Build host -> device list
407
+ by_host: dict[str, list[str]] = {}
408
+ for did, ds in devices.items():
409
+ by_host.setdefault(ds.host_id or "-", []).append(did)
410
+
411
+ printf("Hosts:", Sty.BOLD)
412
+ if not hosts:
413
+ printf(" (none)", Sty.GRAY)
414
+ return
415
+
416
+ for hid, hs in sorted(hosts.items(), key=lambda kv: kv[0]):
417
+ dlist = sorted(by_host.get(hid, []))
418
+ printf(
419
+ " ", Sty.DEFAULT,
420
+ f"{hid}", Sty.MAGENTA,
421
+ " online=", Sty.DEFAULT, f"{hs.online}", Sty.GRAY if hs.online else Sty.GRAY,
422
+ " last_seen=", Sty.DEFAULT, f"{_fmt_ms_ago(hs.last_seen_ms)} ago", Sty.GRAY,
423
+ " devices=", Sty.DEFAULT, f"{len(dlist)}", Sty.GRAY
424
+ )
425
+ for did in dlist:
426
+ ds = devices.get(did)
427
+ if ds is None:
428
+ continue
429
+ printf(
430
+ " - ", Sty.DEFAULT,
431
+ f"{did}", Sty.MAGENTA,
432
+ " local_id=", Sty.DEFAULT, f"{ds.host_local_id}", Sty.GRAY,
433
+ " online=", Sty.DEFAULT, f"{ds.online}", Sty.GRAY if ds.online else Sty.GRAY,
434
+ " last_seen=", Sty.DEFAULT, f"{_fmt_ms_ago(ds.last_seen_ms)} ago", Sty.GRAY
435
+ )
436
+
437
+ def _cmd_devices_status() -> None:
438
+ reg = hts.get_tunnel_registry()
439
+ assert reg is not None
440
+
441
+ devices = reg.list_devices()
442
+ printf("Devices (live):", Sty.BOLD)
443
+ if not devices:
444
+ printf(" (none)", Sty.GRAY)
445
+ return
446
+
447
+ for did, ds in sorted(devices.items(), key=lambda kv: kv[0]):
448
+ printf(
449
+ " ", Sty.DEFAULT,
450
+ f"{did}", Sty.MAGENTA,
451
+ " host=", Sty.DEFAULT, f"{ds.host_id or '-'}", Sty.GRAY,
452
+ " local_id=", Sty.DEFAULT, f"{ds.host_local_id}", Sty.GRAY,
453
+ " online=", Sty.DEFAULT, f"{ds.online}", Sty.GREEN if ds.online else Sty.RED,
454
+ " last_seen=", Sty.DEFAULT, f"{_fmt_ms_ago(ds.last_seen_ms)} ago", Sty.BLUE
455
+ )
456
+
457
+ from typing import Iterable
458
+ import contextlib
459
+
460
+ def _repo_root_guess() -> Path:
461
+ """
462
+ Best-effort: find <repo>/src/... and return <repo>.
463
+ Fallback: walk up a few levels from this file.
464
+ """
465
+ p = Path(__file__).resolve()
466
+ for parent in p.parents:
467
+ if parent.name == "src":
468
+ return parent.parent
469
+ # fallback
470
+ return p.parents[3] if len(p.parents) >= 4 else p.parent
471
+
472
+ # Optional: if you later add "kick" support (see section 2)
473
+ HOST_TUNNEL_EPOCH = 0
474
+
475
+ def _read_env_kv_file(path: Path) -> dict[str, str]:
476
+ out: dict[str, str] = {}
477
+ if not path.exists():
478
+ return out
479
+ for raw in path.read_text(encoding="utf-8").splitlines():
480
+ line = raw.strip()
481
+ if not line or line.startswith("#") or "=" not in line:
482
+ continue
483
+ k, v = line.split("=", 1)
484
+ out[k.strip()] = v.strip().strip('"').strip("'")
485
+ return out
486
+
487
+ def _env_values_as_paths(env_path: Path) -> list[Path]:
488
+ """
489
+ Parse an env file and treat values that look like filesystem paths as candidate dirs.
490
+ Relative paths are resolved relative to the env file's directory.
491
+ """
492
+ kv = _read_env_kv_file(env_path)
493
+ base = env_path.parent
494
+ out: list[Path] = []
495
+
496
+ for _, v in kv.items():
497
+ if not v:
498
+ continue
499
+ # heuristics: looks like a path
500
+ if ("/" not in v) and ("\\" not in v) and (not v.startswith(".")) and (not v.startswith("~")):
501
+ continue
502
+
503
+ p = Path(v).expanduser()
504
+ if not p.is_absolute():
505
+ p = (base / p).resolve()
506
+ out.append(p)
507
+
508
+ return out
509
+
510
+ def _host_state_dirs() -> list[Path]:
511
+ """
512
+ Aggressive candidate list:
513
+ - user config (~/.config/remoterf or REMOTERF_CONFIG_DIR)
514
+ - repo/.config
515
+ - repo/config (+ common subdirs)
516
+ - src/remoteRF_server/config
517
+ - plus env overrides + paths referenced by .env.host_directory (if present)
518
+ """
519
+ repo = _repo_root_guess()
520
+
521
+ cands: list[Path] = [
522
+ _cfg_dir(),
523
+ repo / ".config",
524
+ repo / "config",
525
+ repo / "config" / "remoteRF_server",
526
+ repo / "config" / "remoteRF_host",
527
+ repo / "src" / "remoteRF_server" / "config",
528
+ (Path(__file__).resolve().parent.parent / "config"),
529
+ ]
530
+
531
+ # Environment overrides (add any you might plausibly use)
532
+ for k in (
533
+ "REMOTERF_HOSTDIR_DIR",
534
+ "REMOTERF_HOST_DIRECTORY_DIR",
535
+ "REMOTERF_HOST_DIR",
536
+ "HOST_DIRECTORY_DIR",
537
+ "HOST_DIRECTORY_PATH",
538
+ "REMOTERF_STATE_DIR",
539
+ ):
540
+ v = os.getenv(k)
541
+ if v:
542
+ cands.append(Path(v).expanduser())
543
+
544
+ # If you have a .env.host_directory file, harvest any path-like values from it
545
+ env_host_dir = repo / "src" / "remoteRF_server" / "config" / ".env.host_directory"
546
+ if env_host_dir.exists():
547
+ cands.extend(_env_values_as_paths(env_host_dir))
548
+
549
+ # de-dupe + keep only dirs (or dirs that could exist)
550
+ out: list[Path] = []
551
+ seen: set[str] = set()
552
+ for d in cands:
553
+ try:
554
+ rp = str(d.expanduser().resolve())
555
+ except Exception:
556
+ rp = str(d)
557
+ if rp not in seen:
558
+ seen.add(rp)
559
+ out.append(d)
560
+ return out
561
+
562
+ def _host_state_files() -> list[Path]:
563
+ """
564
+ Broader scan than just host_directory.env:
565
+ We only match filenames that clearly look like host/tunnel/registry state.
566
+ """
567
+ patterns = [
568
+ "host_directory*.env",
569
+ "host_directory*.json",
570
+ "host_directory*.db",
571
+ "host_directory*.sqlite*",
572
+ "host_directory*.pkl",
573
+ "host_tunnel*.env",
574
+ "host_tunnel*.json",
575
+ "tunnel_registry*.env",
576
+ "tunnel_registry*.json",
577
+ "*host*registry*.json",
578
+ "*host*registry*.db",
579
+ "*host*registry*.sqlite*",
580
+ "*host*state*.json",
581
+ "*host*state*.db",
582
+ "*host*state*.sqlite*",
583
+ ]
584
+
585
+ files: list[Path] = []
586
+ for d in _host_state_dirs():
587
+ try:
588
+ if not d.exists() or not d.is_dir():
589
+ continue
590
+ except Exception:
591
+ continue
592
+
593
+ for pat in patterns:
594
+ try:
595
+ files.extend(sorted(d.glob(pat)))
596
+ except Exception:
597
+ pass
598
+
599
+ # de-dupe
600
+ out: list[Path] = []
601
+ seen: set[str] = set()
602
+ for p in files:
603
+ try:
604
+ rp = str(p.resolve())
605
+ except Exception:
606
+ rp = str(p)
607
+ if rp not in seen:
608
+ seen.add(rp)
609
+ out.append(p)
610
+ return out
611
+
612
+ def _deep_clear_state(obj: Any) -> list[str]:
613
+ """
614
+ Best-effort: clear dict/list/set fields on an object, recursively,
615
+ but only for fields that look like state (host/device/route/session/cache/store/etc).
616
+ """
617
+ cleared: list[str] = []
618
+ seen: set[int] = set()
619
+
620
+ state_name = re.compile(r"(host|device|route|session|dir|store|cache|meta|seen|status)", re.I)
621
+
622
+ def walk(x: Any, prefix: str) -> None:
623
+ oid = id(x)
624
+ if oid in seen:
625
+ return
626
+ seen.add(oid)
627
+
628
+ # containers
629
+ if isinstance(x, dict):
630
+ x.clear()
631
+ cleared.append(prefix)
632
+ return
633
+ if isinstance(x, list):
634
+ x.clear()
635
+ cleared.append(prefix)
636
+ return
637
+ if isinstance(x, set):
638
+ x.clear()
639
+ cleared.append(prefix)
640
+ return
641
+
642
+ # objects with __dict__
643
+ d = getattr(x, "__dict__", None)
644
+ if not isinstance(d, dict):
645
+ return
646
+
647
+ for k, v in list(d.items()):
648
+ # never touch locks/threads/etc
649
+ if k in ("_lock", "lock", "_thread", "thread", "_executor", "executor"):
650
+ continue
651
+ if not state_name.search(k):
652
+ continue
653
+
654
+ if isinstance(v, (dict, list, set)):
655
+ try:
656
+ v.clear()
657
+ cleared.append(f"{prefix}.{k}")
658
+ except Exception:
659
+ pass
660
+ else:
661
+ # recurse into nested state objects (EnvStore, etc.)
662
+ walk(v, f"{prefix}.{k}")
663
+
664
+ walk(obj, obj.__class__.__name__)
665
+ return cleared
666
+
667
+ def _try_clear_registry_in_memory(reg: object) -> list[str]:
668
+ """
669
+ Stronger in-memory clear:
670
+ - calls obvious reset/clear methods if present
671
+ - clears common containers
672
+ - recursively clears state-like fields (even if names differ)
673
+ """
674
+ cleared: list[str] = []
675
+
676
+ lock = getattr(reg, "_lock", None)
677
+ acquired = False
678
+ try:
679
+ if lock is not None and hasattr(lock, "acquire") and hasattr(lock, "release"):
680
+ lock.acquire()
681
+ acquired = True
682
+
683
+ # Prefer explicit APIs if you have them
684
+ for meth in (
685
+ "clear",
686
+ "reset",
687
+ "wipe",
688
+ "wipe_all",
689
+ "wipe_state",
690
+ "clear_state",
691
+ "reset_state",
692
+ "prune",
693
+ "gc",
694
+ ):
695
+ fn = getattr(reg, meth, None)
696
+ if callable(fn):
697
+ try:
698
+ fn()
699
+ cleared.append(f"{meth}()")
700
+ except Exception:
701
+ pass
702
+
703
+ # Clear obvious containers by direct attribute scan
704
+ for attr, v in list(getattr(reg, "__dict__", {}).items()):
705
+ if isinstance(v, (dict, list, set)):
706
+ try:
707
+ v.clear()
708
+ cleared.append(attr)
709
+ except Exception:
710
+ pass
711
+
712
+ # Recursive targeted clear (for EnvStore etc.)
713
+ cleared.extend(_deep_clear_state(reg))
714
+
715
+ finally:
716
+ if acquired:
717
+ try:
718
+ lock.release()
719
+ except Exception:
720
+ pass
721
+
722
+ # de-dupe
723
+ out: list[str] = []
724
+ seen: set[str] = set()
725
+ for x in cleared:
726
+ if x not in seen:
727
+ seen.add(x)
728
+ out.append(x)
729
+ return out
730
+
731
+ def _cmd_hosts_wipe(*, yes: bool = False) -> None:
732
+ if not yes:
733
+ try:
734
+ resp = session.prompt(stylize("Type 'wipe-hosts' to confirm wiping host state: ", Sty.RED))
735
+ except KeyboardInterrupt:
736
+ printf("Cancelled.", Sty.GRAY)
737
+ return
738
+ if (resp or "").strip().lower() != "wipe-hosts":
739
+ printf("Wipe aborted.", Sty.GRAY)
740
+ return
741
+
742
+ # 1) delete persisted files (if any)
743
+ files = [p for p in _host_state_files() if p.exists()]
744
+ if not files:
745
+ printf("No persisted host directory files found (searched common dirs/patterns).", Sty.YELLOW)
746
+ else:
747
+ for p in files:
748
+ try:
749
+ p.unlink()
750
+ printf("Deleted: ", Sty.DEFAULT, f"{p}", Sty.MAGENTA)
751
+ except Exception as e:
752
+ printf("ERROR deleting: ", Sty.RED, f"{p} -> {e}", Sty.DEFAULT)
753
+ return
754
+
755
+ # 2) clear in-memory registry (THIS is what drives `hosts status`)
756
+ reg = hts.get_tunnel_registry()
757
+ if reg is None:
758
+ printf("Host tunnel registry not initialized; only persisted wipe attempted.", Sty.GRAY)
759
+ else:
760
+ cleared = _try_clear_registry_in_memory(reg)
761
+ if cleared:
762
+ printf("Cleared in-memory registry state:", Sty.GREEN)
763
+ # keep output short-ish
764
+ for item in cleared[:20]:
765
+ printf(" - ", Sty.DEFAULT, item, Sty.GRAY)
766
+ if len(cleared) > 20:
767
+ printf(" ...", Sty.GRAY)
768
+ else:
769
+ printf("WARNING: in-memory wipe could not confirm any cleared fields.", Sty.YELLOW)
770
+ printf("If `hosts status` still shows data, it is almost certainly being repopulated by live connections.", Sty.YELLOW)
771
+
772
+ printf("Host wipe complete.", Sty.GREEN)
773
+
774
+ # def help():
775
+ # printf("/------- HELP -------/", Sty.BOLD)
776
+ # printf("'exit' ", Sty.MAGENTA, " - Stop server.", Sty.DEFAULT)
777
+ # printf("'help' ", Sty.MAGENTA, " - Help.", Sty.DEFAULT)
778
+ # printf("'clear' ", Sty.MAGENTA, " - Clear terminal screen.", Sty.DEFAULT)
779
+ # printf("'show a' ", Sty.MAGENTA, " - Print all accounts", Sty.DEFAULT)
780
+ # printf("'show d' ", Sty.MAGENTA, " - Print all devices", Sty.DEFAULT)
781
+ # # printf("'view p' ", Sty.MAGENTA, " - Print all perms", Sty.DEFAULT) # Deprecated
782
+ # printf("'show r' ", Sty.MAGENTA, " - Print all reservations", Sty.DEFAULT)
783
+ # printf("'rm aa' ", Sty.MAGENTA, " - Remove all accounts", Sty.DEFAULT)
784
+ # # printf("'rm a' ", Sty.MAGENTA, " - Remove one account", Sty.DEFAULT)
785
+ # printf("'rm ar' ", Sty.MAGENTA, " - Remove all reservations", Sty.DEFAULT)
786
+ # printf("'rm db' ", Sty.MAGENTA, " - Remove all database entries", Sty.DEFAULT)
787
+ # printf("'setacc' ", Sty.MAGENTA, " - Set account details (and deletion)", Sty.DEFAULT)
788
+
789
+ # printf("'mk group' ", Sty.MAGENTA, " - Make a user group", Sty.DEFAULT)
790
+ # printf("'rm group' ", Sty.MAGENTA, " - Remove a user group", Sty.DEFAULT)
791
+ # printf("'edit group' ", Sty.MAGENTA, " - Edit a user group", Sty.DEFAULT)
792
+ # printf("'show g' ", Sty.MAGENTA, " - View all user groups", Sty.DEFAULT)
793
+
794
+ # printf("'mk codes' ", Sty.MAGENTA, " - Make enrollment codes", Sty.DEFAULT)
795
+ # printf("'show c' ", Sty.MAGENTA, " - View all enrollment codes", Sty.DEFAULT)
796
+ # printf("'rm codes' ", Sty.MAGENTA, " - Remove enrollment code", Sty.DEFAULT)
797
+
798
+ # def server_input(inpu:str) -> bool:
799
+ # if inpu == "h" or inpu == "help":
800
+ # help()
801
+ # elif inpu == 'show r':
802
+ # reservation_handler.print_all_reservations()
803
+ # elif inpu == 'show a':
804
+ # reservation_handler.print_all_accounts()
805
+ # elif inpu == 'show p':
806
+ # perms.print_perms()
807
+ # elif inpu == 'show d':
808
+ # get_devices()
809
+ # elif inpu == 'clear':
810
+ # clear()
811
+ # elif inpu == 'rm aa':
812
+ # reservation_handler.remove_all_users()
813
+ # elif inpu == 'rm ar':
814
+ # reservation_handler.remove_all_reservations()
815
+ # elif inpu == 'rm db':
816
+ # reservation_handler.rm_db()
817
+ # elif inpu == 'setacc':
818
+ # set_acc()
819
+ # elif inpu in ('mk group', 'mk g'):
820
+ # make_user_group_local(session)
821
+ # elif inpu in ('rm group', 'rm g'):
822
+ # remove_user_group_local(session)
823
+ # elif inpu in ('edit group', 'edit g'):
824
+ # edit_user_group_local(session)
825
+ # elif inpu in ('show g', 'show groups'):
826
+ # list_user_groups_local(session)
827
+ # elif inpu in ('mk codes', 'mk code', 'mk c'):
828
+ # make_enrollment_codes_local(session)
829
+ # elif inpu == 'show c':
830
+ # list_enrollment_codes_local(session)
831
+ # elif inpu in ('rm code', 'rm c', 'rm codes'):
832
+ # remove_enrollment_code_local(session)
833
+ # elif inpu in ('exit', 'quit'):
834
+ # return False
835
+ # else:
836
+ # print(f"Invalid command: {inpu}")
837
+ # return True
838
+
839
+ from typing import Callable
840
+
841
+ def _norm_cmd(s: str) -> str:
842
+ return " ".join((s or "").strip().split()).lower()
843
+
844
+ def _cmd_status() -> None:
845
+ try:
846
+ uptime = datetime.now() - start_time
847
+ printf("Status:", Sty.BOLD)
848
+ printf(" Started: ", Sty.DEFAULT, f"{start_time}", Sty.BLUE)
849
+ printf(" Uptime : ", Sty.DEFAULT, f"{str(uptime).split('.')[0]}", Sty.BLUE)
850
+ printf(" Bind : ", Sty.DEFAULT, f"{local_ip}:{local_port}", Sty.MAGENTA)
851
+ printf(" Cert : ", Sty.DEFAULT, f"{local_ip}:{cert_port}", Sty.MAGENTA)
852
+ except Exception:
853
+ printf("Status unavailable.", Sty.RED)
854
+
855
+ def help() -> None:
856
+ printf("RemoteRF Server Shell", Sty.BOLD)
857
+ printf("Type ", Sty.DEFAULT, "help", Sty.MAGENTA, " to see commands. Type ", Sty.DEFAULT, "quit", Sty.MAGENTA, " to exit.", Sty.DEFAULT)
858
+ print()
859
+
860
+ printf("Server:", Sty.BOLD)
861
+ printf(" help, h ", Sty.MAGENTA, "- Show this help", Sty.DEFAULT)
862
+ printf(" clear ", Sty.MAGENTA, "- Clear the screen", Sty.DEFAULT)
863
+ printf(" status ", Sty.MAGENTA, "- Show server status (uptime/bind)", Sty.DEFAULT)
864
+ printf(" quit / exit ", Sty.MAGENTA, "- Exit the shell", Sty.DEFAULT)
865
+ print()
866
+
867
+ printf("Users:", Sty.BOLD)
868
+ printf(" users list ", Sty.MAGENTA, "- List accounts", Sty.DEFAULT)
869
+ printf(" users manage ", Sty.MAGENTA, "- Manage a user (perms / delete / reservations)", Sty.DEFAULT)
870
+ printf(" users purge ", Sty.MAGENTA, "- Remove ALL users", Sty.DEFAULT)
871
+ print()
872
+
873
+ printf("Devices:", Sty.BOLD)
874
+ printf(" devices list ", Sty.MAGENTA, "- List devices", Sty.DEFAULT)
875
+ printf(" devices status ", Sty.MAGENTA, "- Live device online/last_seen/route", Sty.DEFAULT)
876
+ print()
877
+
878
+ printf("Reservations:", Sty.BOLD)
879
+ printf(" reservations list ", Sty.MAGENTA, "- List reservations", Sty.DEFAULT)
880
+ printf(" reservations purge ", Sty.MAGENTA, "- Remove ALL reservations", Sty.DEFAULT)
881
+ print()
882
+
883
+ printf("Groups:", Sty.BOLD)
884
+ printf(" groups list ", Sty.MAGENTA, "- List user groups", Sty.DEFAULT)
885
+ printf(" groups create ", Sty.MAGENTA, "- Create a user group (interactive)", Sty.DEFAULT)
886
+ printf(" groups edit ", Sty.MAGENTA, "- Edit a user group (interactive)", Sty.DEFAULT)
887
+ printf(" groups delete ", Sty.MAGENTA, "- Delete a user group (interactive)", Sty.DEFAULT)
888
+ printf(" groups csv ", Sty.MAGENTA, "- Export ALL user groups as CSV to Downloads", Sty.DEFAULT)
889
+ print()
890
+
891
+ printf("Enrollment Codes:", Sty.BOLD)
892
+ printf(" codes list ", Sty.MAGENTA, "- List enrollment codes", Sty.DEFAULT)
893
+ printf(" codes create ", Sty.MAGENTA, "- Create enrollment codes (interactive)", Sty.DEFAULT)
894
+ printf(" codes delete ", Sty.MAGENTA, "- Delete an enrollment code (interactive)", Sty.DEFAULT)
895
+ printf(" codes csv ", Sty.MAGENTA, "- Export ALL enrollment codes as CSV to Downloads", Sty.DEFAULT)
896
+ print()
897
+
898
+ printf("Host Tunnel (live):", Sty.BOLD)
899
+ printf(" hosts status ", Sty.MAGENTA, "- Live host online/last_seen/devices", Sty.DEFAULT)
900
+ printf(" hosts wipe ", Sty.MAGENTA, "- Wipe persisted host directory state (and clear in-memory)", Sty.DEFAULT)
901
+
902
+ printf("Database:", Sty.BOLD)
903
+ printf(" db purge ", Sty.MAGENTA, "- Remove all database entries", Sty.DEFAULT)
904
+ print()
905
+
906
+ def server_input(inpu: str) -> bool:
907
+ cmd = _norm_cmd(inpu)
908
+ if not cmd:
909
+ return True
910
+
911
+ def _stay(fn: Callable[[], None]) -> bool:
912
+ fn()
913
+ return True
914
+
915
+ DISPATCH: dict[str, Callable[[], bool]] = {
916
+ # ---- server ----
917
+ "help": lambda: _stay(help),
918
+ "h": lambda: _stay(help),
919
+ "?": lambda: _stay(help),
920
+
921
+ "clear": lambda: _stay(clear),
922
+ "cls": lambda: _stay(clear),
923
+
924
+ "status": lambda: _stay(_cmd_status),
925
+ "server status": lambda: _stay(_cmd_status),
926
+
927
+ "quit": lambda: False,
928
+ "exit": lambda: False,
929
+ "q": lambda: False,
930
+
931
+ # ---- users ----
932
+ "users list": lambda: _stay(reservation_handler.print_all_accounts),
933
+ "u list": lambda: _stay(reservation_handler.print_all_accounts),
934
+ "accounts list": lambda: _stay(reservation_handler.print_all_accounts),
935
+
936
+ "users manage": lambda: _stay(set_acc),
937
+ "users manage": lambda: _stay(set_acc),
938
+ "setacc": lambda: _stay(set_acc), # old
939
+
940
+ "users purge": lambda: _stay(reservation_handler.remove_all_users),
941
+ "rm aa": lambda: _stay(reservation_handler.remove_all_users), # old
942
+
943
+ # perms (kept; optional to advertise)
944
+ "users perms": lambda: _stay(perms.print_perms),
945
+ "show p": lambda: _stay(perms.print_perms), # old
946
+
947
+ # ---- devices ----
948
+ "devices list": lambda: _stay(get_devices),
949
+ "show d": lambda: _stay(get_devices), # old
950
+
951
+ # ---- reservations ----
952
+ "reservations list": lambda: _stay(reservation_handler.print_all_reservations),
953
+ "show r": lambda: _stay(reservation_handler.print_all_reservations), # old
954
+
955
+ "reservations purge": lambda: _stay(reservation_handler.remove_all_reservations),
956
+ "rm ar": lambda: _stay(reservation_handler.remove_all_reservations), # old
957
+
958
+ # ---- db ----
959
+ "db purge": lambda: _stay(reservation_handler.rm_db),
960
+ "rm db": lambda: _stay(reservation_handler.rm_db), # old
961
+
962
+ # ---- groups ----
963
+ "groups create": lambda: _stay(lambda: make_user_group_local(session)),
964
+ "mk group": lambda: _stay(lambda: make_user_group_local(session)), # old
965
+ "mk g": lambda: _stay(lambda: make_user_group_local(session)), # old
966
+
967
+ "groups delete": lambda: _stay(lambda: remove_user_group_local(session)),
968
+ "rm group": lambda: _stay(lambda: remove_user_group_local(session)), # old
969
+ "rm g": lambda: _stay(lambda: remove_user_group_local(session)), # old
970
+
971
+ "groups edit": lambda: _stay(lambda: edit_user_group_local(session)),
972
+ "edit group": lambda: _stay(lambda: edit_user_group_local(session)), # old
973
+ "edit g": lambda: _stay(lambda: edit_user_group_local(session)), # old
974
+
975
+ "groups list": lambda: _stay(lambda: list_user_groups_local(session)),
976
+ "show g": lambda: _stay(lambda: list_user_groups_local(session)), # old
977
+ "show groups": lambda: _stay(lambda: list_user_groups_local(session)), # old
978
+ "groups csv": lambda: _stay(export_user_groups_csv_local),
979
+
980
+ # ---- enrollment codes ----
981
+ "codes create": lambda: _stay(lambda: make_enrollment_codes_local(session)),
982
+ "mk codes": lambda: _stay(lambda: make_enrollment_codes_local(session)), # old
983
+ "mk code": lambda: _stay(lambda: make_enrollment_codes_local(session)), # old
984
+ "mk c": lambda: _stay(lambda: make_enrollment_codes_local(session)), # old
985
+
986
+ "codes list": lambda: _stay(lambda: list_enrollment_codes_local(session)),
987
+ "show c": lambda: _stay(lambda: list_enrollment_codes_local(session)), # old
988
+
989
+ "codes delete": lambda: _stay(lambda: remove_enrollment_code_local(session)),
990
+ "rm codes": lambda: _stay(lambda: remove_enrollment_code_local(session)), # old
991
+ "rm code": lambda: _stay(lambda: remove_enrollment_code_local(session)), # old
992
+ "rm c": lambda: _stay(lambda: remove_enrollment_code_local(session)), # old
993
+
994
+ # ---- host tunnel ----
995
+ "hosts status": lambda: _stay(_cmd_hosts_status),
996
+ "host status": lambda: _stay(_cmd_hosts_status),
997
+
998
+ "devices status": lambda: _stay(_cmd_devices_status),
999
+ "tunnel devices": lambda: _stay(_cmd_devices_status),
1000
+
1001
+ "codes csv": lambda: _stay(export_enrollment_codes_csv_local),
1002
+ }
1003
+
1004
+ fn = DISPATCH.get(cmd)
1005
+ if fn is None:
1006
+ # allow: "hosts wipe" and "hosts wipe -y"
1007
+ if cmd.startswith("hosts wipe"):
1008
+ yes = ("-y" in cmd.split()) or ("--yes" in cmd.split())
1009
+ _cmd_hosts_wipe(yes=yes)
1010
+ return True
1011
+
1012
+ printf("Unknown command: ", Sty.RED, f"{inpu}", Sty.DEFAULT)
1013
+ printf("Type ", Sty.DEFAULT, "help", Sty.MAGENTA, " to see commands.", Sty.DEFAULT)
1014
+ return True
1015
+
1016
+ return fn()
1017
+
1018
+ # os.system('clear')
1019
+ welcome()
1020
+
1021
+ def main():
1022
+ while server_input(session.prompt(stylize('server@remote_rf: ', Sty.BOLD))):
1023
+ pass