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,1377 @@
1
+ from __future__ import annotations
2
+
3
+ import sys
4
+ import os
5
+ import subprocess
6
+ import shutil
7
+ import re
8
+ from pathlib import Path
9
+ from typing import Optional, Dict, List, Tuple
10
+ from datetime import datetime, timezone
11
+ import base64
12
+ import secrets
13
+ import hashlib
14
+ import getpass
15
+
16
+ from .host.host_auth_token import create_host_token, delete_host_token, wipe_all_host_tokens, show_host_tokens
17
+
18
+ def _x509_notafter_utc(cert_path: Path) -> Optional[datetime]:
19
+ # openssl output: notAfter=Feb 16 23:12:02 2027 GMT
20
+ rc, out = _run_capture(["openssl", "x509", "-in", str(cert_path), "-noout", "-enddate"])
21
+ if rc != 0 or not out:
22
+ return None
23
+
24
+ line = out.strip()
25
+ if not line.startswith("notAfter="):
26
+ return None
27
+
28
+ s = line.split("=", 1)[1].strip()
29
+ if s.endswith(" GMT"):
30
+ s = s[:-4].strip()
31
+
32
+ try:
33
+ dt = datetime.strptime(s, "%b %d %H:%M:%S %Y").replace(tzinfo=timezone.utc)
34
+ return dt
35
+ except Exception:
36
+ return None
37
+
38
+
39
+ def _fmt_remaining(na_utc: datetime) -> str:
40
+ now = datetime.now(timezone.utc)
41
+ delta = na_utc - now
42
+ secs = int(delta.total_seconds())
43
+
44
+ if secs <= 0:
45
+ return "EXPIRED"
46
+
47
+ days = secs // 86400
48
+ secs %= 86400
49
+ hours = secs // 3600
50
+ secs %= 3600
51
+ mins = secs // 60
52
+
53
+ return f"{days}d {hours}h {mins}m"
54
+
55
+
56
+ # -----------------------------
57
+ # Repo-local server config locations
58
+ # -----------------------------
59
+
60
+ def _repo_root() -> Path:
61
+ # <repo>/src/remoteRF_server/serverrf_cli.py -> parents[2] == <repo>
62
+ return Path(__file__).resolve().parents[2]
63
+
64
+
65
+ def _xdg_config_home() -> Path:
66
+ return Path(os.getenv("XDG_CONFIG_HOME", Path.home() / ".config"))
67
+
68
+
69
+ def _cfg_dir() -> Path:
70
+ # default: ~/.config/remoterf
71
+ return Path(os.getenv("REMOTERF_CONFIG_DIR", _xdg_config_home() / "remoterf"))
72
+
73
+
74
+ def _certs_dir() -> Path:
75
+ # allow override via REMOTERF_CERT_DIR (matches your server runtime)
76
+ return Path(os.getenv("REMOTERF_CERT_DIR", _cfg_dir() / "certs"))
77
+
78
+
79
+ def _server_env_path() -> Path:
80
+ return _cfg_dir() / "server.env"
81
+
82
+
83
+ def _devices_env_path() -> Path:
84
+ # server-side device config storage (same schema as hostrf)
85
+ return _cfg_dir() / "devices.env"
86
+
87
+
88
+ def _gen_certs_path() -> Path:
89
+ """
90
+ Prefer repo-local tools next to this module, but allow repo-root /tools fallback.
91
+ - <repo>/src/remoteRF_server/tools/gen_certs.py
92
+ - <repo>/tools/gen_certs.py
93
+ """
94
+ here = Path(__file__).resolve().parent
95
+ p1 = (here / "tools" / "gen_certs.py").resolve()
96
+ if p1.exists():
97
+ return p1
98
+ return (_repo_root() / "tools" / "gen_certs.py").resolve()
99
+
100
+ def _gist_env_path() -> Path:
101
+ # Stored alongside server.env/devices.env under ~/.config/remoterf by default
102
+ return _cfg_dir() / "gist.env"
103
+
104
+ # -----------------------------
105
+ # Help (exact commands only)
106
+ # -----------------------------
107
+
108
+ def print_help() -> None:
109
+ print(
110
+ "RemoteRF Server CLI Help\n"
111
+ "\n"
112
+ "Usage:\n"
113
+ " serverrf -h | --help\n"
114
+ "\n"
115
+ "Serve:\n"
116
+ " serverrf -s | --serve Run server (NOTE: only when -s/--serve is the FIRST arg)\n"
117
+ "\n"
118
+ "Certs:\n"
119
+ " serverrf --gen-certs <static_ip> [options]\n"
120
+ " serverrf --show-certs [-v]\n"
121
+ " serverrf --wipe-certs [-y]\n"
122
+ "\n"
123
+ "Ports:\n"
124
+ " serverrf -c | --config [options]\n"
125
+ "\n"
126
+ "Port options:\n"
127
+ " --main-port <int> Set GRPC_PORT\n"
128
+ " --cert-port <int> Set CERT_PORT\n"
129
+ " --show | -s Show current ports\n"
130
+ " -w | --wipe [-y] Wipe ONLY port config (server.env)\n"
131
+ "\n"
132
+ "Hosts:\n"
133
+ " serverrf --host --token-create <host_id> [--length <int>] Create/rotate host token\n"
134
+ " serverrf --host --show Show all host token records\n"
135
+ " serverrf --host --delete <host_id> Delete host token record\n"
136
+ " serverrf --host --wipe [-y] Wipe ALL host token records\n"
137
+ " serverrf --host --list --secrets (optional) include salt/hash\n"
138
+ "\n"
139
+ "Devices:\n"
140
+ " serverrf -d | --device [options]\n"
141
+ "\n"
142
+ "Device options:\n"
143
+ " --add --pluto <id:name:iio_serial> Add device (fails if gid OR serial already used)\n"
144
+ " --remove <id> Remove device\n"
145
+ " --edit-name <id> <name> Override device NAME for existing device\n"
146
+ " --show | -s Show all devices\n"
147
+ " -w | --wipe [-y] Wipe ONLY device config (devices.env)\n"
148
+
149
+ "\n"
150
+ "Cert options (for --gen-certs):\n"
151
+ " --days <int> Server cert validity in days (default: 365)\n"
152
+ " --ca-days <int> CA cert validity in days (default: 3650)\n"
153
+ " --bits <int> RSA key size (default: 2048)\n"
154
+ " --dns <name> DNS SAN entry (repeatable)\n"
155
+ " --cn <name> Common Name (defaults to first SAN entry)\n"
156
+ " --force Overwrite existing certs/keys\n"
157
+ " --no-detect-ip Do not auto-detect IP when none provided\n"
158
+ "\n"
159
+ "Global:\n"
160
+ " -y, --yes Skip wipe confirmation prompts\n"
161
+ " -v, --verbose With --show-certs, also print x509 details (requires openssl)\n"
162
+ "\n"
163
+ "Gist Status:\n"
164
+ " serverrf --gist --set --id <gist_id> --file <filename> [--token <tok> | --token-stdin]\n"
165
+ " serverrf --gist --show [--secrets]\n"
166
+ " serverrf --gist --wipe [-y]\n"
167
+ "\n"
168
+ "Examples:\n"
169
+ " serverrf --gist --set --id 2a35e0... --file ucla-wlab-remoterf-status.json\n"
170
+ " serverrf --gist --set --id 2a35e0... --file status.json --token-stdin\n"
171
+ " serverrf --gist --show\n"
172
+ " serverrf --gist --wipe -y\n"
173
+ "\n"
174
+ # " serverrf --gen-certs 192.168.1.24 --dns rrf2 --dns rrf2.local --force\n"
175
+ " serverrf --gen-certs 192.168.1.50 --days 3650 --force\n"
176
+ " serverrf --show-certs -v\n"
177
+ " serverrf --wipe-certs -y\n"
178
+ "\n"
179
+ " serverrf --config --main-port 61005\n"
180
+ " serverrf --config --cert-port 61006\n"
181
+ " serverrf --config --show\n"
182
+ " serverrf -c -s\n"
183
+ " serverrf --config --wipe -y\n"
184
+ "\n"
185
+ " serverrf --host --token-create lab-host-01 --length 8 --force\n"
186
+ " serverrf --host --show\n"
187
+ " serverrf --host --delete lab-host-01\n"
188
+ " serverrf --host --wipe -y\n"
189
+ "\n"
190
+ " serverrf --device --add --pluto 0:pluto_aaa:123123\n"
191
+ " serverrf --device --edit-name 0 \"New Pluto Name\"\n"
192
+ " serverrf --device --remove 0\n"
193
+ " serverrf --device --show\n"
194
+ " serverrf --device --wipe -y\n"
195
+ "\n"
196
+ " serverrf -s\n"
197
+ " serverrf --serve\n"
198
+ "\n"
199
+ )
200
+
201
+
202
+ # -----------------------------
203
+ # Serve
204
+ # -----------------------------
205
+
206
+ def _serve() -> int:
207
+ """
208
+ Alias for the RRRFserver entrypoint.
209
+ Equivalent to running the RRRFserver console_script.
210
+ """
211
+ from remoteRF_server.server.grpc_server import main as grpc_main
212
+ grpc_main()
213
+ return 0
214
+
215
+
216
+ # -----------------------------
217
+ # Tiny env utils (same style as hostrf)
218
+ # -----------------------------
219
+
220
+ def _write_env_kv(path: Path, kv: Dict[str, str]) -> None:
221
+ path.parent.mkdir(parents=True, exist_ok=True)
222
+ lines: List[str] = []
223
+ for k, v in kv.items():
224
+ v = str(v)
225
+ if any(c.isspace() for c in v) or any(c in v for c in ['"', "'"]):
226
+ v = v.replace('"', '\\"')
227
+ lines.append(f'{k}="{v}"')
228
+ else:
229
+ lines.append(f"{k}={v}")
230
+ path.write_text("\n".join(lines) + "\n", encoding="utf-8")
231
+
232
+
233
+ def _read_env_kv(path: Path) -> Dict[str, str]:
234
+ out: Dict[str, str] = {}
235
+ if not path.exists():
236
+ return out
237
+ for raw in path.read_text(encoding="utf-8").splitlines():
238
+ line = raw.strip()
239
+ if not line or line.startswith("#") or "=" not in line:
240
+ continue
241
+ k, v = line.split("=", 1)
242
+ out[k.strip()] = v.strip().strip('"').strip("'")
243
+ return out
244
+
245
+
246
+ def _validate_port(n: int, *, name: str) -> Optional[str]:
247
+ if n <= 0 or n > 65535:
248
+ return f"{name} out of range (1..65535)"
249
+ return None
250
+
251
+ def _redact(s: str) -> str:
252
+ s = (s or "").strip()
253
+ if len(s) <= 8:
254
+ return "****"
255
+ return f"{s[:4]}...{s[-4:]}"
256
+
257
+
258
+ def _gist_show(*, show_secrets: bool = False) -> int:
259
+ p = _gist_env_path()
260
+ if not p.exists():
261
+ print(f"No gist config found (missing {p}).")
262
+ return 0
263
+
264
+ kv = _read_env_kv(p)
265
+ print(f"Gist config: {p}")
266
+ gid = kv.get("STATUS_GIST_ID", "")
267
+ fn = kv.get("STATUS_GIST_FILENAME", "")
268
+ tok = kv.get("GITHUB_TOKEN", "")
269
+
270
+ print(f" STATUS_GIST_ID={gid}")
271
+ print(f" STATUS_GIST_FILENAME={fn}")
272
+ if show_secrets:
273
+ print(f" GITHUB_TOKEN={tok}")
274
+ else:
275
+ print(f" GITHUB_TOKEN={_redact(tok)} (use --secrets to show)")
276
+ return 0
277
+
278
+
279
+ def _gist_set(*, gist_id: str, filename: str, token: str) -> int:
280
+ gist_id = (gist_id or "").strip()
281
+ filename = (filename or "").strip()
282
+ token = (token or "").strip()
283
+
284
+ if not gist_id:
285
+ print("ERROR: missing --id <gist_id>", file=sys.stderr)
286
+ return 2
287
+ if not filename:
288
+ print("ERROR: missing --file <filename>", file=sys.stderr)
289
+ return 2
290
+ if not token:
291
+ print("ERROR: missing token (use prompt or --token-stdin or --token)", file=sys.stderr)
292
+ return 2
293
+
294
+ _cfg_dir().mkdir(parents=True, exist_ok=True)
295
+ p = _gist_env_path()
296
+
297
+ kv = _read_env_kv(p)
298
+ kv["STATUS_GIST_ID"] = gist_id
299
+ kv["STATUS_GIST_FILENAME"] = filename
300
+ kv["GITHUB_TOKEN"] = token
301
+
302
+ _write_env_kv(p, kv)
303
+
304
+ try:
305
+ os.chmod(p, 0o600)
306
+ except Exception:
307
+ pass
308
+
309
+ print("Configured gist status env:")
310
+ print(f" {p}")
311
+ print(f" STATUS_GIST_ID={gist_id}")
312
+ print(f" STATUS_GIST_FILENAME={filename}")
313
+ print(f" GITHUB_TOKEN={_redact(token)}")
314
+ return 0
315
+
316
+
317
+ def _gist_wipe(*, yes: bool) -> int:
318
+ p = _gist_env_path()
319
+ if not p.exists():
320
+ print(f"No gist config found at: {p}")
321
+ return 0
322
+
323
+ if not yes:
324
+ try:
325
+ if input("This will delete gist.env (gist status config). Type 'wipe' to confirm: ").strip().lower() != "wipe":
326
+ print("Wipe aborted.")
327
+ return 1
328
+ except KeyboardInterrupt:
329
+ print("\nCancelled.")
330
+ return 1
331
+
332
+ p.unlink()
333
+ print("Wiped gist config (gist.env).")
334
+ return 0
335
+
336
+ # -----------------------------
337
+ # Subprocess utils
338
+ # -----------------------------
339
+
340
+ def _run(cmd: List[str]) -> int:
341
+ try:
342
+ subprocess.run(cmd, check=True)
343
+ return 0
344
+ except subprocess.CalledProcessError as e:
345
+ return int(e.returncode)
346
+
347
+
348
+ def _run_capture(cmd: List[str]) -> Tuple[int, str]:
349
+ try:
350
+ p = subprocess.run(cmd, check=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True)
351
+ return 0, p.stdout
352
+ except subprocess.CalledProcessError as e:
353
+ out = ""
354
+ if getattr(e, "stdout", None):
355
+ out = e.stdout if isinstance(e.stdout, str) else e.stdout.decode(errors="replace")
356
+ return int(e.returncode), out
357
+
358
+
359
+ # -----------------------------
360
+ # Cert actions
361
+ # -----------------------------
362
+
363
+ def _looks_like_pem_cert(p: Path) -> bool:
364
+ try:
365
+ data = p.read_bytes()
366
+ except Exception:
367
+ return False
368
+ return b"BEGIN CERTIFICATE" in data and b"END CERTIFICATE" in data
369
+
370
+
371
+ def _show_certs(*, verbose: bool) -> int:
372
+ d = _certs_dir()
373
+ print(f"Certs dir: {d}")
374
+ if not d.exists():
375
+ print(" (missing)")
376
+ return 1
377
+
378
+ files = sorted([p for p in d.iterdir() if p.is_file()])
379
+ if not files:
380
+ print(" (empty)")
381
+ return 1
382
+
383
+ for p in files:
384
+ try:
385
+ size = p.stat().st_size
386
+ except Exception:
387
+ size = -1
388
+
389
+ tag = ""
390
+ if p.suffix in (".crt", ".pem") and _looks_like_pem_cert(p):
391
+ tag = " [cert]"
392
+ elif p.suffix == ".key":
393
+ tag = " [key]"
394
+ print(f" - {p.name} ({size} bytes){tag}")
395
+
396
+ rc, _ = _run_capture(["openssl", "version"])
397
+ if rc != 0:
398
+ print("\n(openssl not available; install it to view cert expiry)")
399
+ else:
400
+ candidates: List[Path] = []
401
+ ca = d / "ca.crt"
402
+ srv = d / "server.crt"
403
+ if ca.exists():
404
+ candidates.append(ca)
405
+ if srv.exists():
406
+ candidates.append(srv)
407
+
408
+ for p in files:
409
+ if p in candidates:
410
+ continue
411
+ if p.suffix in (".crt", ".pem") and _looks_like_pem_cert(p):
412
+ candidates.append(p)
413
+
414
+ if candidates:
415
+ print("\nExpiry:")
416
+ for p in candidates:
417
+ na = _x509_notafter_utc(p)
418
+ if not na:
419
+ print(f" {p.name}: (could not read notAfter)")
420
+ continue
421
+ remaining = _fmt_remaining(na)
422
+ local = na.astimezone().strftime("%Y-%m-%d %H:%M:%S %Z")
423
+ print(f" {p.name}: expires {local} ({remaining})")
424
+ else:
425
+ print("\nExpiry / Remaining:\n (no cert files found)")
426
+
427
+ if not verbose:
428
+ return 0
429
+
430
+ if rc != 0:
431
+ print("\n(openssl not available; install it to view cert details)")
432
+ return 0
433
+
434
+ print("\nCertificate details (openssl):")
435
+ for p in files:
436
+ if p.suffix not in (".crt", ".pem"):
437
+ continue
438
+ if not _looks_like_pem_cert(p):
439
+ continue
440
+
441
+ print(f"\n== {p.name} ==")
442
+ rc2, out2 = _run_capture(["openssl", "x509", "-in", str(p), "-noout", "-subject", "-issuer", "-dates"])
443
+ if rc2 == 0 and out2.strip():
444
+ print(out2.rstrip())
445
+ else:
446
+ print(" (failed to parse with openssl)")
447
+
448
+ rc2, out2 = _run_capture(["openssl", "x509", "-in", str(p), "-noout", "-ext", "subjectAltName"])
449
+ if rc2 == 0 and out2.strip():
450
+ print(out2.rstrip())
451
+
452
+ return 0
453
+
454
+
455
+ def _wipe_certs(*, yes: bool) -> int:
456
+ d = _certs_dir()
457
+ if not d.exists():
458
+ print(f"No cert output found at: {d}")
459
+ return 0
460
+
461
+ if not yes:
462
+ try:
463
+ if input(f"This will delete ALL cert outputs at:\n {d}\nType 'wipe' to confirm: ").strip().lower() != "wipe":
464
+ print("Wipe aborted.")
465
+ return 1
466
+ except KeyboardInterrupt:
467
+ print("\nCancelled.")
468
+ return 1
469
+
470
+ shutil.rmtree(d)
471
+ print(f"Wiped cert output: {d}")
472
+ return 0
473
+
474
+
475
+ def _gen_certs(
476
+ *,
477
+ static_ip: str,
478
+ days: int,
479
+ ca_days: int,
480
+ bits: int,
481
+ dns: List[str],
482
+ cn: Optional[str],
483
+ force: bool,
484
+ no_detect_ip: bool,
485
+ ) -> int:
486
+ gen_path = _gen_certs_path()
487
+ if not gen_path.exists():
488
+ print(f"ERROR: cert generator not found: {gen_path}", file=sys.stderr)
489
+ return 2
490
+
491
+ _cfg_dir().mkdir(parents=True, exist_ok=True)
492
+
493
+ cmd: List[str] = [
494
+ sys.executable,
495
+ str(gen_path),
496
+ "--days", str(days),
497
+ "--ca-days", str(ca_days),
498
+ "--bits", str(bits),
499
+ "--config-dir", str(_cfg_dir()),
500
+ "--out-subdir", "certs",
501
+ "--ip", static_ip,
502
+ ]
503
+
504
+ if force:
505
+ cmd.append("--force")
506
+ if no_detect_ip:
507
+ cmd.append("--no-detect-ip")
508
+ for d in dns:
509
+ cmd += ["--dns", d]
510
+ if cn:
511
+ cmd += ["--cn", cn]
512
+
513
+ rc = _run(cmd)
514
+ if rc == 0:
515
+ print("Generated certs:")
516
+ print(f" {_certs_dir()}")
517
+
518
+ print("\nUsers/Hosts must reconfigure each time new certs are generated.")
519
+ return rc
520
+
521
+
522
+ # -----------------------------
523
+ # Port config actions
524
+ # -----------------------------
525
+
526
+ def _config_show_ports() -> int:
527
+ p = _server_env_path()
528
+ if not p.exists():
529
+ print(f"No port config found (missing {p}).")
530
+ return 0
531
+
532
+ kv = _read_env_kv(p)
533
+ if not kv:
534
+ print("Port config is empty.")
535
+ return 0
536
+
537
+ print(f"Port config: {p}")
538
+ if "GRPC_PORT" in kv:
539
+ print(f" GRPC_PORT={kv['GRPC_PORT']}")
540
+ if "CERT_PORT" in kv:
541
+ print(f" CERT_PORT={kv['CERT_PORT']}")
542
+ return 0
543
+
544
+
545
+ def _config_set_ports(*, main_port: Optional[int], cert_port: Optional[int]) -> int:
546
+ if main_port is None and cert_port is None:
547
+ print("ERROR: expected --main-port <int> and/or --cert-port <int>", file=sys.stderr)
548
+ return 2
549
+
550
+ if main_port is not None:
551
+ err = _validate_port(main_port, name="main-port")
552
+ if err:
553
+ print(f"ERROR: {err}", file=sys.stderr)
554
+ return 2
555
+
556
+ if cert_port is not None:
557
+ err = _validate_port(cert_port, name="cert-port")
558
+ if err:
559
+ print(f"ERROR: {err}", file=sys.stderr)
560
+ return 2
561
+
562
+ _cfg_dir().mkdir(parents=True, exist_ok=True)
563
+ p = _server_env_path()
564
+ kv = _read_env_kv(p)
565
+
566
+ if main_port is not None:
567
+ kv["GRPC_PORT"] = str(main_port)
568
+ if cert_port is not None:
569
+ kv["CERT_PORT"] = str(cert_port)
570
+
571
+ _write_env_kv(p, kv)
572
+ print("Configured ports:")
573
+ print(f" {p}")
574
+ if main_port is not None:
575
+ print(f" GRPC_PORT={main_port}")
576
+ if cert_port is not None:
577
+ print(f" CERT_PORT={cert_port}")
578
+ return 0
579
+
580
+
581
+ def _config_wipe_ports(*, yes: bool) -> int:
582
+ p = _server_env_path()
583
+ if not p.exists():
584
+ print(f"No port config found at: {p}")
585
+ return 0
586
+
587
+ if not yes:
588
+ try:
589
+ if input("This will delete server.env (ports). Type 'wipe' to confirm: ").strip().lower() != "wipe":
590
+ print("Wipe aborted.")
591
+ return 1
592
+ except KeyboardInterrupt:
593
+ print("\nCancelled.")
594
+ return 1
595
+
596
+ p.unlink()
597
+ print("Wiped port config (server.env).")
598
+ return 0
599
+
600
+
601
+ # -----------------------------
602
+ # Device storage/actions
603
+ # -----------------------------
604
+
605
+ _DEVICE_KEY_RE = re.compile(r"^DEVICE_(\d+)_(.+)$", re.IGNORECASE)
606
+
607
+ def _parse_pluto_add_spec(tok: str) -> Tuple[int, str, str]:
608
+ # <gid:name:iio_serial>
609
+ parts = [p.strip() for p in (tok or "").split(":")]
610
+ if len(parts) != 3:
611
+ raise ValueError("Expected <gid:name:iio_serial>")
612
+ gid_s, name, iio_serial = parts
613
+ gid = int(gid_s)
614
+ if gid < 0:
615
+ raise ValueError("gid must be >= 0")
616
+ if not name:
617
+ raise ValueError("name is empty")
618
+ if not iio_serial:
619
+ raise ValueError("iio_serial is empty")
620
+ return gid, name, iio_serial
621
+
622
+ def _device_edit_name(global_id_str: str, new_name: str) -> int:
623
+ try:
624
+ gid = int((global_id_str or "").strip())
625
+ except Exception:
626
+ print("ERROR: --edit-name expects <gid:int> <new_name>", file=sys.stderr)
627
+ return 2
628
+
629
+ name = (new_name or "").strip()
630
+ if not name:
631
+ print("ERROR: new_name is empty", file=sys.stderr)
632
+ return 2
633
+
634
+ recs = _read_device_records()
635
+ if gid not in recs:
636
+ print(f"ERROR: global_id {gid} not present; cannot edit name.", file=sys.stderr)
637
+ return 2
638
+
639
+ recs[gid]["NAME"] = name
640
+ _write_device_records(recs)
641
+ print(f"Updated device name: {gid} name={name}")
642
+ return 0
643
+
644
+ def _read_device_records() -> Dict[int, Dict[str, str]]:
645
+ kv = _read_env_kv(_devices_env_path())
646
+ recs: Dict[int, Dict[str, str]] = {}
647
+ for k, v in kv.items():
648
+ m = _DEVICE_KEY_RE.match(k.strip())
649
+ if not m:
650
+ continue
651
+ gid = int(m.group(1))
652
+ field = m.group(2).upper().strip()
653
+ recs.setdefault(gid, {})[field] = str(v).strip()
654
+ return recs
655
+
656
+
657
+ def _write_device_records(recs: Dict[int, Dict[str, str]]) -> None:
658
+ out: Dict[str, str] = {}
659
+ for gid in sorted(recs.keys()):
660
+ fields = recs[gid]
661
+ order = ["TYPE", "NAME", "IDENT_KIND", "IDENT"]
662
+ for f in order:
663
+ if f in fields and fields[f] != "":
664
+ out[f"DEVICE_{gid}_{f}"] = fields[f]
665
+ for f in sorted(fields.keys()):
666
+ if f in order:
667
+ continue
668
+ if fields[f] != "":
669
+ out[f"DEVICE_{gid}_{f}"] = fields[f]
670
+ _write_env_kv(_devices_env_path(), out)
671
+
672
+
673
+ def _device_add_pluto(spec: str) -> int:
674
+ try:
675
+ gid, name, iio_serial = _parse_pluto_add_spec(spec)
676
+ except Exception as e:
677
+ print(f"ERROR: invalid pluto spec: {e}", file=sys.stderr)
678
+ return 2
679
+
680
+ _cfg_dir().mkdir(parents=True, exist_ok=True)
681
+ recs = _read_device_records()
682
+
683
+ if gid in recs:
684
+ print(f"ERROR: global_id {gid} already exists", file=sys.stderr)
685
+ return 2
686
+
687
+ for other_gid, r in recs.items():
688
+ if r.get("IDENT_KIND", "") == "iio_serial" and r.get("IDENT", "") == iio_serial:
689
+ print(f"ERROR: iio_serial already used by global_id={other_gid}", file=sys.stderr)
690
+ return 2
691
+
692
+ recs[gid] = {
693
+ "TYPE": "pluto",
694
+ "NAME": name,
695
+ "IDENT_KIND": "iio_serial",
696
+ "IDENT": iio_serial,
697
+ }
698
+ _write_device_records(recs)
699
+
700
+ print(f"Added device: {gid} type=pluto name={name} iio_serial={iio_serial}")
701
+ return 0
702
+
703
+
704
+ def _device_remove(global_id_str: str) -> int:
705
+ try:
706
+ gid = int((global_id_str or "").strip())
707
+ except Exception:
708
+ print("ERROR: --remove expects <gid:int>", file=sys.stderr)
709
+ return 2
710
+
711
+ recs = _read_device_records()
712
+ if gid not in recs:
713
+ print(f"WARNING: global_id {gid} not present; nothing to remove.")
714
+ return 0
715
+
716
+ recs.pop(gid, None)
717
+ _write_device_records(recs)
718
+ print(f"Removed device: {gid}")
719
+ return 0
720
+
721
+
722
+ def _device_show() -> int:
723
+ p = _devices_env_path()
724
+ if not p.exists():
725
+ print(f"No devices configured (missing {p}).")
726
+ return 0
727
+
728
+ recs = _read_device_records()
729
+ print(f"Devices: {p}")
730
+ if not recs:
731
+ print(" (none)")
732
+ return 0
733
+
734
+ for gid in sorted(recs.keys()):
735
+ r = recs[gid]
736
+ dtype = r.get("TYPE", "")
737
+ name = r.get("NAME", "")
738
+ ik = r.get("IDENT_KIND", "")
739
+ ident = r.get("IDENT", "")
740
+ print(f" {gid}: type={dtype} name={name} {ik}={ident}")
741
+ return 0
742
+
743
+
744
+ def _wipe_devices_only(*, yes: bool) -> int:
745
+ p = _devices_env_path()
746
+ if not p.exists():
747
+ print(f"No device config found at: {p}")
748
+ return 0
749
+
750
+ if not yes:
751
+ try:
752
+ if input("This will delete devices.env. Type 'wipe' to confirm: ").strip().lower() != "wipe":
753
+ print("Wipe aborted.")
754
+ return 1
755
+ except KeyboardInterrupt:
756
+ print("\nCancelled.")
757
+ return 1
758
+
759
+ p.unlink()
760
+ print("Wiped device config (devices.env).")
761
+ return 0
762
+
763
+ # -----------------------------
764
+ # CLI parser (ONLY commands in help)
765
+ # -----------------------------
766
+ def main() -> int:
767
+ argv = list(sys.argv[1:])
768
+
769
+ # help
770
+ if len(argv) == 0 or argv[0] in ("--help", "-h", "-help", "--h"):
771
+ print_help()
772
+ return 0
773
+
774
+ # IMPORTANT: ONLY "serverrf -s/--serve" (first arg) starts the server.
775
+ if argv[0] in ("--serve", "-s"):
776
+ if len(argv) != 1:
777
+ print("ERROR: -s/--serve cannot be combined with other commands. Run it alone.", file=sys.stderr)
778
+ return 2
779
+ return _serve()
780
+
781
+ yes = False
782
+ verbose = False
783
+
784
+ # cert commands
785
+ gen_ip: Optional[str] = None
786
+ show_certs = False
787
+ wipe_certs = False
788
+
789
+ # gen-certs options
790
+ days = 365
791
+ ca_days = 3650
792
+ bits = 2048
793
+ dns: List[str] = []
794
+ cn: Optional[str] = None
795
+ force = False
796
+ no_detect_ip = False
797
+
798
+ # config commands
799
+ config_mode = False
800
+ config_show = False
801
+ config_wipe = False
802
+ main_port: Optional[int] = None
803
+ cert_port: Optional[int] = None
804
+
805
+ # device commands
806
+ device_mode = False
807
+ device_show = False
808
+ device_wipe = False
809
+ device_add_type: Optional[str] = None
810
+ device_add_spec: Optional[str] = None
811
+ device_remove_gid: Optional[str] = None
812
+ device_edit_gid: Optional[str] = None
813
+ device_edit_new_name: Optional[str] = None
814
+
815
+ # host commands
816
+ host_mode = False
817
+ host_token_create_id: Optional[str] = None
818
+ host_delete_id: Optional[str] = None
819
+ host_list = False
820
+ host_wipe = False
821
+ host_show_secrets = False # optional
822
+ host_token_length: Optional[int] = None
823
+ host_force = False # host-only overwrite/rotate
824
+
825
+ # gist commands
826
+ gist_mode = False
827
+ gist_show = False
828
+ gist_wipe = False
829
+ gist_set = False
830
+ gist_show_secrets = False
831
+
832
+ gist_id: Optional[str] = None
833
+ gist_filename: Optional[str] = None
834
+ gist_token: Optional[str] = None
835
+ gist_token_stdin = False
836
+
837
+ i = 0
838
+ while i < len(argv):
839
+ tok = argv[i]
840
+
841
+ if tok in ("-y", "--yes", "-yes"):
842
+ yes = True
843
+ i += 1
844
+ continue
845
+
846
+ if tok in ("-v", "--verbose", "-verbose"):
847
+ verbose = True
848
+ i += 1
849
+ continue
850
+
851
+ # enter config mode
852
+ if tok in ("--config", "-c", "-config"):
853
+ config_mode = True
854
+ i += 1
855
+ continue
856
+
857
+ # enter device mode
858
+ if tok in ("--device", "-d", "-device"):
859
+ device_mode = True
860
+ i += 1
861
+ continue
862
+
863
+ # cert verbs
864
+ if tok == "--show-certs":
865
+ show_certs = True
866
+ i += 1
867
+ continue
868
+
869
+ if tok == "--wipe-certs":
870
+ wipe_certs = True
871
+ i += 1
872
+ continue
873
+
874
+ if tok == "--gen-certs":
875
+ if i + 1 >= len(argv):
876
+ print("ERROR: missing value after --gen-certs <static_ip>", file=sys.stderr)
877
+ return 2
878
+ gen_ip = argv[i + 1].strip()
879
+ i += 2
880
+ continue
881
+
882
+ # config verbs/options
883
+ if config_mode and tok in ("--show", "-show", "-s"):
884
+ config_show = True
885
+ i += 1
886
+ continue
887
+
888
+ if config_mode and tok in ("-w", "--wipe", "-wipe"):
889
+ config_wipe = True
890
+ i += 1
891
+ continue
892
+
893
+ if config_mode and tok == "--main-port":
894
+ if i + 1 >= len(argv):
895
+ print("ERROR: missing value after --main-port <int>", file=sys.stderr)
896
+ return 2
897
+ try:
898
+ main_port = int(argv[i + 1].strip())
899
+ except Exception:
900
+ print("ERROR: --main-port expects an int", file=sys.stderr)
901
+ return 2
902
+ i += 2
903
+ continue
904
+
905
+ if config_mode and tok == "--cert-port":
906
+ if i + 1 >= len(argv):
907
+ print("ERROR: missing value after --cert-port <int>", file=sys.stderr)
908
+ return 2
909
+ try:
910
+ cert_port = int(argv[i + 1].strip())
911
+ except Exception:
912
+ print("ERROR: --cert-port expects an int", file=sys.stderr)
913
+ return 2
914
+ i += 2
915
+ continue
916
+
917
+ # enter host mode
918
+ if tok in ("--host", "-host"):
919
+ host_mode = True
920
+ i += 1
921
+ continue
922
+
923
+ # host verbs
924
+ if host_mode and tok in ("--token-create", "-token-create"):
925
+ if i + 1 >= len(argv):
926
+ print("ERROR: --token-create expects <host_id>", file=sys.stderr)
927
+ return 2
928
+ host_token_create_id = argv[i + 1].strip()
929
+ i += 2
930
+ continue
931
+
932
+ if host_mode and tok in ("--length", "-length", "--token-length"):
933
+ if i + 1 >= len(argv):
934
+ print("ERROR: --length expects <int>", file=sys.stderr)
935
+ return 2
936
+ try:
937
+ host_token_length = int(argv[i + 1].strip())
938
+ except Exception:
939
+ print("ERROR: --length expects an int", file=sys.stderr)
940
+ return 2
941
+ if host_token_length < 4 or host_token_length > 64:
942
+ print("ERROR: --length out of range (4..64)", file=sys.stderr)
943
+ return 2
944
+ i += 2
945
+ continue
946
+
947
+ if host_mode and tok in ("--delete", "-delete", "--token-delete", "-token-delete"):
948
+ if i + 1 >= len(argv):
949
+ print("ERROR: --delete expects <host_id>", file=sys.stderr)
950
+ return 2
951
+ host_delete_id = argv[i + 1].strip()
952
+ i += 2
953
+ continue
954
+
955
+ if host_mode and tok in ("--list", "-list", "--show", "-show"):
956
+ host_list = True
957
+ i += 1
958
+ continue
959
+
960
+ if host_mode and tok in ("--wipe", "-wipe", "-w"):
961
+ host_wipe = True
962
+ i += 1
963
+ continue
964
+
965
+ if host_mode and tok in ("--secrets", "-secrets"):
966
+ host_show_secrets = True
967
+ i += 1
968
+ continue
969
+
970
+ if host_mode and tok in ("--force", "-f", "--rotate"):
971
+ host_force = True
972
+ i += 1
973
+ continue
974
+
975
+ # enter gist mode
976
+ if tok in ("--gist", "-gist"):
977
+ gist_mode = True
978
+ i += 1
979
+ continue
980
+
981
+ # gist verbs/options
982
+ if gist_mode and tok in ("--show", "-show", "-s"):
983
+ gist_show = True
984
+ i += 1
985
+ continue
986
+
987
+ if gist_mode and tok in ("--wipe", "-wipe", "-w"):
988
+ gist_wipe = True
989
+ i += 1
990
+ continue
991
+
992
+ if gist_mode and tok in ("--set", "-set", "--config", "--configure"):
993
+ gist_set = True
994
+ i += 1
995
+ continue
996
+
997
+ if gist_mode and tok in ("--secrets", "-secrets"):
998
+ gist_show_secrets = True
999
+ i += 1
1000
+ continue
1001
+
1002
+ if gist_mode and tok == "--id":
1003
+ if i + 1 >= len(argv):
1004
+ print("ERROR: --id expects <gist_id>", file=sys.stderr)
1005
+ return 2
1006
+ gist_id = argv[i + 1].strip()
1007
+ i += 2
1008
+ continue
1009
+
1010
+ if gist_mode and tok in ("--file", "--filename"):
1011
+ if i + 1 >= len(argv):
1012
+ print("ERROR: --file expects <filename>", file=sys.stderr)
1013
+ return 2
1014
+ gist_filename = argv[i + 1].strip()
1015
+ i += 2
1016
+ continue
1017
+
1018
+ # Not recommended (shell history), but provided anyway
1019
+ if gist_mode and tok == "--token":
1020
+ if i + 1 >= len(argv):
1021
+ print("ERROR: --token expects <token>", file=sys.stderr)
1022
+ return 2
1023
+ gist_token = argv[i + 1].strip()
1024
+ i += 2
1025
+ continue
1026
+
1027
+ if gist_mode and tok == "--token-stdin":
1028
+ gist_token_stdin = True
1029
+ i += 1
1030
+ continue
1031
+
1032
+ # device verbs/options
1033
+ if device_mode and tok in ("--show", "-show", "-s"):
1034
+ device_show = True
1035
+ i += 1
1036
+ continue
1037
+
1038
+ if device_mode and tok in ("-w", "--wipe", "-wipe"):
1039
+ device_wipe = True
1040
+ i += 1
1041
+ continue
1042
+
1043
+ if device_mode and tok in ("--add", "-add", "-a"):
1044
+ if i + 1 >= len(argv):
1045
+ print("ERROR: missing device type after --device --add (expected --pluto)", file=sys.stderr)
1046
+ return 2
1047
+ dt = argv[i + 1].strip()
1048
+ if dt != "--pluto":
1049
+ print("ERROR: only '--device --add --pluto <gid:name:iio_serial>' is supported", file=sys.stderr)
1050
+ return 2
1051
+ if i + 2 >= len(argv):
1052
+ print("ERROR: missing spec after --device --add --pluto", file=sys.stderr)
1053
+ return 2
1054
+ device_add_type = "pluto"
1055
+ device_add_spec = argv[i + 2].strip()
1056
+ i += 3
1057
+ continue
1058
+
1059
+ if device_mode and tok in ("--remove", "-remove", "-r"):
1060
+ if i + 1 >= len(argv):
1061
+ print("ERROR: missing value after --device --remove <gid>", file=sys.stderr)
1062
+ return 2
1063
+ device_remove_gid = argv[i + 1].strip()
1064
+ i += 2
1065
+ continue
1066
+
1067
+ if device_mode and tok in ("--edit-name", "-edit-name"):
1068
+ if i + 2 >= len(argv):
1069
+ print("ERROR: --edit-name expects <gid> <new_name>", file=sys.stderr)
1070
+ return 2
1071
+ device_edit_gid = argv[i + 1].strip()
1072
+ device_edit_new_name = argv[i + 2].strip()
1073
+ i += 3
1074
+ continue
1075
+
1076
+ # gen-certs options
1077
+ if tok == "--days":
1078
+ if i + 1 >= len(argv):
1079
+ print("ERROR: missing value after --days <int>", file=sys.stderr)
1080
+ return 2
1081
+ try:
1082
+ days = int(argv[i + 1].strip())
1083
+ except Exception:
1084
+ print("ERROR: --days expects an int", file=sys.stderr)
1085
+ return 2
1086
+ i += 2
1087
+ continue
1088
+
1089
+ if tok == "--ca-days":
1090
+ if i + 1 >= len(argv):
1091
+ print("ERROR: missing value after --ca-days <int>", file=sys.stderr)
1092
+ return 2
1093
+ try:
1094
+ ca_days = int(argv[i + 1].strip())
1095
+ except Exception:
1096
+ print("ERROR: --ca-days expects an int", file=sys.stderr)
1097
+ return 2
1098
+ i += 2
1099
+ continue
1100
+
1101
+ if tok == "--bits":
1102
+ if i + 1 >= len(argv):
1103
+ print("ERROR: missing value after --bits <int>", file=sys.stderr)
1104
+ return 2
1105
+ try:
1106
+ bits = int(argv[i + 1].strip())
1107
+ except Exception:
1108
+ print("ERROR: --bits expects an int", file=sys.stderr)
1109
+ return 2
1110
+ i += 2
1111
+ continue
1112
+
1113
+ if tok == "--dns":
1114
+ if i + 1 >= len(argv):
1115
+ print("ERROR: missing value after --dns <name>", file=sys.stderr)
1116
+ return 2
1117
+ dns.append(argv[i + 1].strip())
1118
+ i += 2
1119
+ continue
1120
+
1121
+ if tok == "--cn":
1122
+ if i + 1 >= len(argv):
1123
+ print("ERROR: missing value after --cn <name>", file=sys.stderr)
1124
+ return 2
1125
+ cn = argv[i + 1].strip()
1126
+ i += 2
1127
+ continue
1128
+
1129
+ if tok == "--force":
1130
+ force = True
1131
+ i += 1
1132
+ continue
1133
+
1134
+ if tok == "--no-detect-ip":
1135
+ no_detect_ip = True
1136
+ i += 1
1137
+ continue
1138
+
1139
+ print(f"ERROR: unknown option: {tok!r}", file=sys.stderr)
1140
+ return 2
1141
+
1142
+ # ----------------
1143
+ # Enforce single-mode semantics (device vs config vs cert)
1144
+ # ----------------
1145
+ if device_mode and (config_mode or show_certs or wipe_certs or (gen_ip is not None)):
1146
+ print("ERROR: --device cannot be combined with config/cert commands. Run them separately.", file=sys.stderr)
1147
+ return 2
1148
+
1149
+ if host_mode and (device_mode or config_mode or show_certs or wipe_certs or (gen_ip is not None)):
1150
+ print("ERROR: --host cannot be combined with device/config/cert commands. Run them separately.", file=sys.stderr)
1151
+ return 2
1152
+
1153
+ if host_force and host_token_create_id is None:
1154
+ print("ERROR: --force is only valid with --host --token-create <host_id>", file=sys.stderr)
1155
+ return 2
1156
+
1157
+ if host_mode:
1158
+ actions = int(host_token_create_id is not None) + int(host_delete_id is not None) + int(host_list) + int(host_wipe)
1159
+ if actions == 0:
1160
+ print_help()
1161
+ return 2
1162
+ if actions > 1:
1163
+ print("ERROR: host commands are mutually exclusive. Use exactly one of: --token-create, --delete, --list, --wipe", file=sys.stderr)
1164
+ return 2
1165
+
1166
+ if host_wipe and not yes:
1167
+ # mimic your other wipe confirmations
1168
+ try:
1169
+ if input("This will delete ALL host tokens. Type 'wipe' to confirm: ").strip().lower() != "wipe":
1170
+ print("Wipe aborted.")
1171
+ return 1
1172
+ except KeyboardInterrupt:
1173
+ print("\nCancelled.")
1174
+ return 1
1175
+
1176
+ if gist_mode and (device_mode or config_mode or host_mode or show_certs or wipe_certs or (gen_ip is not None)):
1177
+ print("ERROR: --gist cannot be combined with device/config/host/cert commands. Run them separately.", file=sys.stderr)
1178
+ return 2
1179
+
1180
+ # ----------------
1181
+ # Gist mode execution
1182
+ # ----------------
1183
+ if gist_mode:
1184
+ actions = int(gist_show) + int(gist_wipe) + int(gist_set)
1185
+ if actions == 0:
1186
+ print_help()
1187
+ return 2
1188
+ if actions > 1:
1189
+ print("ERROR: gist commands are mutually exclusive. Use exactly one of: --set, --show, --wipe", file=sys.stderr)
1190
+ return 2
1191
+
1192
+ if gist_show:
1193
+ return int(_gist_show(show_secrets=gist_show_secrets))
1194
+
1195
+ if gist_wipe:
1196
+ return int(_gist_wipe(yes=yes))
1197
+
1198
+ # gist_set
1199
+ if gist_id is None or gist_filename is None:
1200
+ print("ERROR: --gist --set requires --id <gist_id> and --file <filename>", file=sys.stderr)
1201
+ return 2
1202
+
1203
+ tok = gist_token or ""
1204
+ if gist_token_stdin:
1205
+ tok = sys.stdin.read().strip()
1206
+ if not tok:
1207
+ # secure prompt (no echo)
1208
+ tok = getpass.getpass("GitHub token (won't echo): ").strip()
1209
+
1210
+ return int(_gist_set(gist_id=gist_id, filename=gist_filename, token=tok))
1211
+
1212
+ # ----------------
1213
+ # Host mode execution
1214
+ # ----------------
1215
+ if host_mode:
1216
+ # 1) list
1217
+ if host_list:
1218
+ show_host_tokens(include_secrets=host_show_secrets, file=sys.stdout)
1219
+ return 0
1220
+
1221
+ # 2) delete
1222
+ if host_delete_id is not None:
1223
+ try:
1224
+ ok = delete_host_token(host_delete_id)
1225
+ except Exception as e:
1226
+ print(f"ERROR: {e}", file=sys.stderr)
1227
+ return 2
1228
+
1229
+ if ok:
1230
+ print(f"Deleted host token: {host_delete_id}")
1231
+ return 0
1232
+ else:
1233
+ print(f"No host token found for: {host_delete_id}")
1234
+ return 1
1235
+
1236
+ # 3) wipe all
1237
+ if host_wipe:
1238
+ try:
1239
+ n = wipe_all_host_tokens()
1240
+ except Exception as e:
1241
+ print(f"ERROR: {e}", file=sys.stderr)
1242
+ return 2
1243
+ print(f"Wiped {n} host token record(s).")
1244
+ return 0
1245
+
1246
+ # 4) token create
1247
+ if host_token_create_id is None:
1248
+ print_help()
1249
+ return 2
1250
+
1251
+ try:
1252
+ token_len = host_token_length if host_token_length is not None else 8
1253
+ token = create_host_token(host_token_create_id, length=token_len, overwrite=host_force)
1254
+ except Exception as e:
1255
+ print(f"ERROR: {e}", file=sys.stderr)
1256
+ return 2
1257
+
1258
+ print("Host token created and stored at ~/.config/remoterf/db/hosts_auth.env")
1259
+ print(f'To config host:\n hostrf --config --host {host_token_create_id} "{token}"')
1260
+ return 0
1261
+
1262
+ # ----------------
1263
+ # Device mode execution
1264
+ # ----------------
1265
+ if device_mode:
1266
+ if device_wipe and (
1267
+ device_show
1268
+ or device_add_spec is not None
1269
+ or device_remove_gid is not None
1270
+ or device_edit_gid is not None
1271
+ ):
1272
+ print("ERROR: cannot combine device add/remove/show/edit with device wipe. Use '-d -w' only.", file=sys.stderr)
1273
+ return 2
1274
+
1275
+ if device_wipe:
1276
+ return int(_wipe_devices_only(yes=yes))
1277
+
1278
+ did_any = False
1279
+
1280
+ if device_show:
1281
+ rc = _device_show()
1282
+ if rc != 0:
1283
+ return rc
1284
+ did_any = True
1285
+
1286
+ if device_add_type == "pluto" and device_add_spec is not None:
1287
+ rc = _device_add_pluto(device_add_spec)
1288
+ if rc != 0:
1289
+ return rc
1290
+ did_any = True
1291
+
1292
+ if device_edit_gid is not None:
1293
+ rc = _device_edit_name(device_edit_gid, device_edit_new_name or "")
1294
+ if rc != 0:
1295
+ return rc
1296
+ did_any = True
1297
+
1298
+ if device_remove_gid is not None:
1299
+ rc = _device_remove(device_remove_gid)
1300
+ if rc != 0:
1301
+ return rc
1302
+ did_any = True
1303
+
1304
+ if not did_any:
1305
+ print_help()
1306
+ return 2
1307
+
1308
+ return 0
1309
+
1310
+ # ----------------
1311
+ # Config mode execution
1312
+ # ----------------
1313
+ if config_mode:
1314
+ if config_wipe and (config_show or main_port is not None or cert_port is not None):
1315
+ print("ERROR: cannot combine --wipe with --show or port. Use '-c -w' only.", file=sys.stderr)
1316
+ return 2
1317
+
1318
+ if config_wipe:
1319
+ return int(_config_wipe_ports(yes=yes))
1320
+
1321
+ did_any = False
1322
+
1323
+ if config_show:
1324
+ rc = _config_show_ports()
1325
+ if rc != 0:
1326
+ return rc
1327
+ did_any = True
1328
+
1329
+ if main_port is not None or cert_port is not None:
1330
+ rc = _config_set_ports(main_port=main_port, cert_port=cert_port)
1331
+ if rc != 0:
1332
+ return rc
1333
+ did_any = True
1334
+
1335
+ if not did_any:
1336
+ print_help()
1337
+ return 2
1338
+
1339
+ return 0
1340
+
1341
+ # ----------------
1342
+ # Cert mode execution
1343
+ # ----------------
1344
+ if wipe_certs and show_certs:
1345
+ print("ERROR: cannot combine --show-certs with --wipe-certs", file=sys.stderr)
1346
+ return 2
1347
+
1348
+ if wipe_certs:
1349
+ return int(_wipe_certs(yes=yes))
1350
+
1351
+ if show_certs and gen_ip is None:
1352
+ return int(_show_certs(verbose=verbose))
1353
+
1354
+ if gen_ip is None:
1355
+ print_help()
1356
+ return 2
1357
+
1358
+ rc = _gen_certs(
1359
+ static_ip=gen_ip,
1360
+ days=days,
1361
+ ca_days=ca_days,
1362
+ bits=bits,
1363
+ dns=dns,
1364
+ cn=cn,
1365
+ force=force,
1366
+ no_detect_ip=no_detect_ip,
1367
+ )
1368
+ if rc != 0:
1369
+ return rc
1370
+
1371
+ if show_certs:
1372
+ return int(_show_certs(verbose=verbose))
1373
+
1374
+ return 0
1375
+
1376
+ if __name__ == "__main__":
1377
+ raise SystemExit(main())