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.
- remoteRF_server/__init__.py +0 -0
- remoteRF_server/common/__init__.py +0 -0
- remoteRF_server/common/grpc/__init__.py +1 -0
- remoteRF_server/common/grpc/grpc_host_pb2.py +63 -0
- remoteRF_server/common/grpc/grpc_host_pb2_grpc.py +97 -0
- remoteRF_server/common/grpc/grpc_pb2.py +59 -0
- remoteRF_server/common/grpc/grpc_pb2_grpc.py +97 -0
- remoteRF_server/common/idl/__init__.py +1 -0
- remoteRF_server/common/idl/device_schema.py +39 -0
- remoteRF_server/common/idl/pluto_schema.py +174 -0
- remoteRF_server/common/idl/schema.py +358 -0
- remoteRF_server/common/utils/__init__.py +6 -0
- remoteRF_server/common/utils/ansi_codes.py +120 -0
- remoteRF_server/common/utils/api_token.py +21 -0
- remoteRF_server/common/utils/db_connection.py +35 -0
- remoteRF_server/common/utils/db_location.py +24 -0
- remoteRF_server/common/utils/list_string.py +5 -0
- remoteRF_server/common/utils/process_arg.py +80 -0
- remoteRF_server/drivers/__init__.py +0 -0
- remoteRF_server/drivers/adalm_pluto/__init__.py +0 -0
- remoteRF_server/drivers/adalm_pluto/pluto_remote_server.py +105 -0
- remoteRF_server/host/__init__.py +0 -0
- remoteRF_server/host/host_auth_token.py +292 -0
- remoteRF_server/host/host_directory_store.py +142 -0
- remoteRF_server/host/host_tunnel_server.py +1388 -0
- remoteRF_server/server/__init__.py +0 -0
- remoteRF_server/server/acc_perms.py +317 -0
- remoteRF_server/server/cert_provider.py +184 -0
- remoteRF_server/server/device_manager.py +688 -0
- remoteRF_server/server/grpc_server.py +1023 -0
- remoteRF_server/server/reservation.py +811 -0
- remoteRF_server/server/rpc_manager.py +104 -0
- remoteRF_server/server/user_group_cli.py +723 -0
- remoteRF_server/server/user_group_handler.py +1120 -0
- remoteRF_server/serverrf_cli.py +1377 -0
- remoteRF_server/tools/__init__.py +191 -0
- remoteRF_server/tools/gen_certs.py +274 -0
- remoteRF_server/tools/gist_status.py +139 -0
- remoteRF_server/tools/gist_status_testing.py +67 -0
- remoterf_server_testing-0.0.0.dist-info/METADATA +612 -0
- remoterf_server_testing-0.0.0.dist-info/RECORD +44 -0
- remoterf_server_testing-0.0.0.dist-info/WHEEL +5 -0
- remoterf_server_testing-0.0.0.dist-info/entry_points.txt +2 -0
- remoterf_server_testing-0.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
from ...server.device_manager import set_pluto, get_device as device_pluto
|
|
2
|
+
from ...common.utils.process_arg import unmap_arg, map_arg
|
|
3
|
+
|
|
4
|
+
class PlutoServer:
|
|
5
|
+
|
|
6
|
+
@staticmethod
|
|
7
|
+
def handle_call(*, function_name, args):
|
|
8
|
+
try:
|
|
9
|
+
# print(f'fn {function_name}')
|
|
10
|
+
|
|
11
|
+
function_name_args = function_name.split(':')
|
|
12
|
+
function_name = function_name_args[1]
|
|
13
|
+
function_type = function_name_args[2]
|
|
14
|
+
if function_name == "ip":
|
|
15
|
+
set_pluto(ip=args[function_name].string_value.split(":")[1])
|
|
16
|
+
elif function_type == "GET":
|
|
17
|
+
return PlutoServer.get(function_name=function_name, token=unmap_arg(args['a']))
|
|
18
|
+
elif function_type == "SET":
|
|
19
|
+
PlutoServer.set(function_name=function_name, value=args[function_name], token=unmap_arg(args['a']))
|
|
20
|
+
|
|
21
|
+
elif function_type in ("CALL0", "CALL1"):
|
|
22
|
+
return PlutoServer.call_func(
|
|
23
|
+
function_name=function_name,
|
|
24
|
+
function_type=function_type,
|
|
25
|
+
args=args,
|
|
26
|
+
token=unmap_arg(args['a'])
|
|
27
|
+
)
|
|
28
|
+
else:
|
|
29
|
+
raise ValueError(f"Unknown function_type: {function_type}. Is that a function you can call?")
|
|
30
|
+
except Exception as e:
|
|
31
|
+
return {function_name:map_arg('None'), "Error":map_arg(f'{e}')}
|
|
32
|
+
|
|
33
|
+
@staticmethod
|
|
34
|
+
def call_func(*, function_name, function_type, args, token):
|
|
35
|
+
device = device_pluto(api_token=token)
|
|
36
|
+
if device is None:
|
|
37
|
+
return {function_name: map_arg('None'), "UE": map_arg("API Token Invalid")}
|
|
38
|
+
|
|
39
|
+
method = getattr(device, function_name)
|
|
40
|
+
if not callable(method):
|
|
41
|
+
raise ValueError(f"Device attribute '{function_name}' is not callable.")
|
|
42
|
+
|
|
43
|
+
# if PlutoServer.DEBUG:
|
|
44
|
+
|
|
45
|
+
# print(f"[DEBUG] call_func: calling {function_name} on {device} with type={function_type}")
|
|
46
|
+
|
|
47
|
+
# print(f"raw values: {args['arg1']}")
|
|
48
|
+
|
|
49
|
+
if function_type == "CALL0":
|
|
50
|
+
# Zero-argument function call
|
|
51
|
+
result = method()
|
|
52
|
+
elif function_type == "CALL1":
|
|
53
|
+
# Single-argument function call
|
|
54
|
+
if 'arg1' not in args:
|
|
55
|
+
raise ValueError("Client Package: CALL1 type requires 'arg1' in args.")
|
|
56
|
+
python_arg = unmap_arg(args['arg1'])
|
|
57
|
+
# if PlutoServer.DEBUG:
|
|
58
|
+
# print(f"[DEBUG] call_func: unmap_arg(args['arg1']) => {python_arg}")
|
|
59
|
+
# print(f"Pluto.{method} \n unmapped arg: {python_arg}")
|
|
60
|
+
# print(f"unmapped values: {python_arg}")
|
|
61
|
+
# print(f"{function_name} is called on {device}")
|
|
62
|
+
result = method(python_arg)
|
|
63
|
+
else:
|
|
64
|
+
raise ValueError(f"Unsupported call type: {function_type} (arg length not supported)")
|
|
65
|
+
|
|
66
|
+
# If the function returns something, map it back
|
|
67
|
+
if result is not None:
|
|
68
|
+
return {function_name: map_arg(result)}
|
|
69
|
+
else:
|
|
70
|
+
# If there's no return value, return some placeholder
|
|
71
|
+
return {function_name: map_arg('None')}
|
|
72
|
+
|
|
73
|
+
@staticmethod
|
|
74
|
+
def get(*, function_name, token):
|
|
75
|
+
try:
|
|
76
|
+
device = device_pluto(api_token=token)
|
|
77
|
+
if device == None:
|
|
78
|
+
return {function_name:map_arg('None'), "UE":map_arg("API Token Invalid")}
|
|
79
|
+
if not hasattr(device, function_name):
|
|
80
|
+
return {function_name:map_arg('None'), "UE":map_arg(f'{function_name} is not a gettable value.')}
|
|
81
|
+
|
|
82
|
+
attr = getattr(device, function_name)
|
|
83
|
+
if callable(attr):
|
|
84
|
+
return {function_name:map_arg(attr())}
|
|
85
|
+
else:
|
|
86
|
+
return {function_name:map_arg(attr)}
|
|
87
|
+
except Exception as e:
|
|
88
|
+
|
|
89
|
+
# print(f"UE: Pluto_server ln:35: {e}")
|
|
90
|
+
return {function_name:map_arg('None'), "UE":map_arg(f'{e}')}
|
|
91
|
+
|
|
92
|
+
@staticmethod
|
|
93
|
+
def set(*, function_name, value, token):
|
|
94
|
+
try:
|
|
95
|
+
device = device_pluto(api_token=token)
|
|
96
|
+
if device == None:
|
|
97
|
+
return {function_name:map_arg('None'), "UE":map_arg("API Token Invalid")}
|
|
98
|
+
if not hasattr(device, function_name):
|
|
99
|
+
return {function_name:map_arg('None'), "UE":map_arg(f'{function_name} is not a settable value.')}
|
|
100
|
+
|
|
101
|
+
setattr(device, function_name, unmap_arg(value))
|
|
102
|
+
return {}
|
|
103
|
+
except Exception as e:
|
|
104
|
+
# print(f"UE: Pluto_server ln:44: {e}")
|
|
105
|
+
return {function_name:map_arg('None'), "UE":map_arg(f'{e}')}
|
|
File without changes
|
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
# remoteRF_server/host/host_auth_token.py
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import re
|
|
7
|
+
import sys
|
|
8
|
+
import time
|
|
9
|
+
import threading
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Dict, Tuple, List
|
|
12
|
+
from datetime import datetime, timezone
|
|
13
|
+
import hashlib
|
|
14
|
+
|
|
15
|
+
from ..common.utils import *
|
|
16
|
+
|
|
17
|
+
_HOST_ID_RE = re.compile(r"^[A-Za-z0-9_.-]{1,64}$")
|
|
18
|
+
|
|
19
|
+
def _validate_host_id(raw: str) -> str:
|
|
20
|
+
hid = (raw or "").strip()
|
|
21
|
+
if not hid:
|
|
22
|
+
raise ValueError("Missing host_id")
|
|
23
|
+
if hid == "unknown-host":
|
|
24
|
+
raise ValueError("host_id cannot be 'unknown-host'")
|
|
25
|
+
if not _HOST_ID_RE.fullmatch(hid):
|
|
26
|
+
raise ValueError("Invalid host_id (allowed: 1..64 chars of [A-Za-z0-9_.-])")
|
|
27
|
+
return hid
|
|
28
|
+
|
|
29
|
+
def _hosts_auth_path() -> Path:
|
|
30
|
+
return get_db_dir() / "hosts_auth.env"
|
|
31
|
+
|
|
32
|
+
def _utc_now_s() -> str:
|
|
33
|
+
return datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
34
|
+
|
|
35
|
+
def _host_key(host_id: str) -> str:
|
|
36
|
+
return hashlib.sha256(host_id.encode("utf-8")).hexdigest()[:16]
|
|
37
|
+
|
|
38
|
+
# Minimal env read/write
|
|
39
|
+
|
|
40
|
+
def _read_env_kv(path: Path) -> Dict[str, str]:
|
|
41
|
+
out: Dict[str, str] = {}
|
|
42
|
+
if not path.exists():
|
|
43
|
+
return out
|
|
44
|
+
for raw in path.read_text(encoding="utf-8").splitlines():
|
|
45
|
+
line = raw.strip()
|
|
46
|
+
if not line or line.startswith("#") or "=" not in line:
|
|
47
|
+
continue
|
|
48
|
+
k, v = line.split("=", 1)
|
|
49
|
+
out[k.strip()] = v.strip().strip('"').strip("'")
|
|
50
|
+
return out
|
|
51
|
+
|
|
52
|
+
def _write_env_kv(path: Path, kv: Dict[str, str]) -> None:
|
|
53
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
54
|
+
# deterministic output makes diffs/debugging easier
|
|
55
|
+
lines: List[str] = []
|
|
56
|
+
for k in sorted(kv.keys()):
|
|
57
|
+
v = str(kv[k])
|
|
58
|
+
if any(c.isspace() for c in v) or any(c in v for c in ['"', "'"]):
|
|
59
|
+
v = v.replace('"', '\\"')
|
|
60
|
+
lines.append(f'{k}="{v}"')
|
|
61
|
+
else:
|
|
62
|
+
lines.append(f"{k}={v}")
|
|
63
|
+
path.write_text("\n".join(lines) + "\n", encoding="utf-8")
|
|
64
|
+
|
|
65
|
+
# Cached DB load (thread-safe)
|
|
66
|
+
|
|
67
|
+
_DB_LOCK = threading.RLock()
|
|
68
|
+
_DB_CACHE_MTIME: float = 0.0
|
|
69
|
+
_DB_CACHE: Dict[str, str] = {}
|
|
70
|
+
|
|
71
|
+
def _load_db_kv() -> Dict[str, str]:
|
|
72
|
+
global _DB_CACHE_MTIME, _DB_CACHE
|
|
73
|
+
p = _hosts_auth_path()
|
|
74
|
+
|
|
75
|
+
try:
|
|
76
|
+
st = p.stat()
|
|
77
|
+
mtime = float(st.st_mtime)
|
|
78
|
+
except FileNotFoundError:
|
|
79
|
+
with _DB_LOCK:
|
|
80
|
+
_DB_CACHE_MTIME = 0.0
|
|
81
|
+
_DB_CACHE = {}
|
|
82
|
+
return {}
|
|
83
|
+
except Exception:
|
|
84
|
+
# if stat fails oddly, fall back to best-effort read
|
|
85
|
+
mtime = -1.0
|
|
86
|
+
|
|
87
|
+
with _DB_LOCK:
|
|
88
|
+
if mtime > 0 and _DB_CACHE and _DB_CACHE_MTIME == mtime:
|
|
89
|
+
return dict(_DB_CACHE)
|
|
90
|
+
|
|
91
|
+
kv = _read_env_kv(p)
|
|
92
|
+
_DB_CACHE = dict(kv)
|
|
93
|
+
_DB_CACHE_MTIME = mtime if mtime > 0 else time.time()
|
|
94
|
+
return dict(kv)
|
|
95
|
+
|
|
96
|
+
def _save_db_kv(kv: Dict[str, str]) -> None:
|
|
97
|
+
global _DB_CACHE_MTIME, _DB_CACHE
|
|
98
|
+
p = _hosts_auth_path()
|
|
99
|
+
_write_env_kv(p, kv)
|
|
100
|
+
# refresh cache immediately
|
|
101
|
+
try:
|
|
102
|
+
mtime = float(p.stat().st_mtime)
|
|
103
|
+
except Exception:
|
|
104
|
+
mtime = time.time()
|
|
105
|
+
with _DB_LOCK:
|
|
106
|
+
_DB_CACHE = dict(kv)
|
|
107
|
+
_DB_CACHE_MTIME = mtime
|
|
108
|
+
|
|
109
|
+
# Public API
|
|
110
|
+
|
|
111
|
+
def create_host_token(
|
|
112
|
+
host_id: str,
|
|
113
|
+
*,
|
|
114
|
+
length: int = 8,
|
|
115
|
+
status: str = "approved",
|
|
116
|
+
overwrite: bool = True,
|
|
117
|
+
) -> str:
|
|
118
|
+
hid = _validate_host_id(host_id)
|
|
119
|
+
hk = _host_key(hid)
|
|
120
|
+
|
|
121
|
+
salt, hashed, token = generate_token(length=length)
|
|
122
|
+
now = _utc_now_s()
|
|
123
|
+
|
|
124
|
+
kv = _load_db_kv()
|
|
125
|
+
|
|
126
|
+
# If not overwriting and host exists, refuse
|
|
127
|
+
if not overwrite and kv.get(f"HOST_{hk}_ID", "").strip():
|
|
128
|
+
raise ValueError(f"Host token already exists for host_id={hid!r} (use --force to overwrite)")
|
|
129
|
+
|
|
130
|
+
kv[f"HOST_{hk}_ID"] = hid
|
|
131
|
+
kv[f"HOST_{hk}_SALT"] = salt
|
|
132
|
+
kv[f"HOST_{hk}_HASH"] = hashed
|
|
133
|
+
kv[f"HOST_{hk}_STATUS"] = (status or "approved").strip().lower()
|
|
134
|
+
kv[f"HOST_{hk}_UPDATED_UTC"] = now
|
|
135
|
+
if f"HOST_{hk}_CREATED_UTC" not in kv:
|
|
136
|
+
kv[f"HOST_{hk}_CREATED_UTC"] = now
|
|
137
|
+
|
|
138
|
+
_save_db_kv(kv)
|
|
139
|
+
return token
|
|
140
|
+
|
|
141
|
+
def set_host_status(host_id: str, status: str) -> None:
|
|
142
|
+
hid = _validate_host_id(host_id)
|
|
143
|
+
hk = _host_key(hid)
|
|
144
|
+
|
|
145
|
+
kv = _load_db_kv()
|
|
146
|
+
if kv.get(f"HOST_{hk}_ID", "").strip() != hid:
|
|
147
|
+
raise ValueError(f"Unknown host_id={hid!r} (no record)")
|
|
148
|
+
|
|
149
|
+
kv[f"HOST_{hk}_STATUS"] = (status or "").strip().lower()
|
|
150
|
+
kv[f"HOST_{hk}_UPDATED_UTC"] = _utc_now_s()
|
|
151
|
+
_save_db_kv(kv)
|
|
152
|
+
|
|
153
|
+
def is_host_token_valid(host_id: str, token: str, *, require_status: str = "approved") -> bool:
|
|
154
|
+
try:
|
|
155
|
+
hid = _validate_host_id(host_id)
|
|
156
|
+
except Exception:
|
|
157
|
+
return False
|
|
158
|
+
|
|
159
|
+
tok = (token or "").strip()
|
|
160
|
+
if not tok:
|
|
161
|
+
return False
|
|
162
|
+
|
|
163
|
+
hk = _host_key(hid)
|
|
164
|
+
kv = _load_db_kv()
|
|
165
|
+
|
|
166
|
+
rid = (kv.get(f"HOST_{hk}_ID", "") or "").strip()
|
|
167
|
+
if rid != hid:
|
|
168
|
+
return False
|
|
169
|
+
|
|
170
|
+
status = (kv.get(f"HOST_{hk}_STATUS", "") or "").strip().lower()
|
|
171
|
+
if require_status and status != require_status.strip().lower():
|
|
172
|
+
return False
|
|
173
|
+
|
|
174
|
+
salt = (kv.get(f"HOST_{hk}_SALT", "") or "").strip()
|
|
175
|
+
hashed = (kv.get(f"HOST_{hk}_HASH", "") or "").strip()
|
|
176
|
+
if not salt or not hashed:
|
|
177
|
+
return False
|
|
178
|
+
|
|
179
|
+
try:
|
|
180
|
+
return bool(validate_token(salt, hashed, tok))
|
|
181
|
+
except Exception:
|
|
182
|
+
return False
|
|
183
|
+
|
|
184
|
+
def list_hosts() -> List[Tuple[str, str]]:
|
|
185
|
+
kv = _load_db_kv()
|
|
186
|
+
out: List[Tuple[str, str]] = []
|
|
187
|
+
# Scan by HOST_<hk>_ID keys
|
|
188
|
+
for k, v in kv.items():
|
|
189
|
+
if not k.startswith("HOST_") or not k.endswith("_ID"):
|
|
190
|
+
continue
|
|
191
|
+
hk = k[len("HOST_") : -len("_ID")]
|
|
192
|
+
hid = (v or "").strip()
|
|
193
|
+
st = (kv.get(f"HOST_{hk}_STATUS", "") or "").strip().lower()
|
|
194
|
+
if hid:
|
|
195
|
+
out.append((hid, st or ""))
|
|
196
|
+
out.sort(key=lambda t: t[0])
|
|
197
|
+
return out
|
|
198
|
+
|
|
199
|
+
def _host_record_keys(hk: str) -> list[str]:
|
|
200
|
+
# all keys stored for this host_key
|
|
201
|
+
return [
|
|
202
|
+
f"HOST_{hk}_ID",
|
|
203
|
+
f"HOST_{hk}_SALT",
|
|
204
|
+
f"HOST_{hk}_HASH",
|
|
205
|
+
f"HOST_{hk}_STATUS",
|
|
206
|
+
f"HOST_{hk}_CREATED_UTC",
|
|
207
|
+
f"HOST_{hk}_UPDATED_UTC",
|
|
208
|
+
]
|
|
209
|
+
|
|
210
|
+
def list_host_token_records(*, include_secrets: bool = False) -> list[dict[str, str]]:
|
|
211
|
+
kv = _load_db_kv()
|
|
212
|
+
out: list[dict[str, str]] = []
|
|
213
|
+
|
|
214
|
+
# Find all host keys by scanning HOST_<hk>_ID
|
|
215
|
+
for k, v in kv.items():
|
|
216
|
+
if not k.startswith("HOST_") or not k.endswith("_ID"):
|
|
217
|
+
continue
|
|
218
|
+
|
|
219
|
+
hk = k[len("HOST_") : -len("_ID")]
|
|
220
|
+
host_id = (v or "").strip()
|
|
221
|
+
if not host_id:
|
|
222
|
+
continue
|
|
223
|
+
|
|
224
|
+
rec: dict[str, str] = {
|
|
225
|
+
"host_id": host_id,
|
|
226
|
+
"status": (kv.get(f"HOST_{hk}_STATUS", "") or "").strip().lower(),
|
|
227
|
+
"created_utc": (kv.get(f"HOST_{hk}_CREATED_UTC", "") or "").strip(),
|
|
228
|
+
"updated_utc": (kv.get(f"HOST_{hk}_UPDATED_UTC", "") or "").strip(),
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
if include_secrets:
|
|
232
|
+
rec["salt"] = (kv.get(f"HOST_{hk}_SALT", "") or "").strip()
|
|
233
|
+
rec["hash"] = (kv.get(f"HOST_{hk}_HASH", "") or "").strip()
|
|
234
|
+
|
|
235
|
+
out.append(rec)
|
|
236
|
+
|
|
237
|
+
out.sort(key=lambda r: r.get("host_id", ""))
|
|
238
|
+
return out
|
|
239
|
+
|
|
240
|
+
def delete_host_token(host_id: str) -> bool:
|
|
241
|
+
hid = _validate_host_id(host_id)
|
|
242
|
+
hk = _host_key(hid)
|
|
243
|
+
|
|
244
|
+
kv = _load_db_kv()
|
|
245
|
+
rid = (kv.get(f"HOST_{hk}_ID", "") or "").strip()
|
|
246
|
+
if rid != hid:
|
|
247
|
+
return False
|
|
248
|
+
|
|
249
|
+
removed = False
|
|
250
|
+
for k in _host_record_keys(hk):
|
|
251
|
+
if k in kv:
|
|
252
|
+
kv.pop(k, None)
|
|
253
|
+
removed = True
|
|
254
|
+
|
|
255
|
+
if removed:
|
|
256
|
+
_save_db_kv(kv)
|
|
257
|
+
return removed
|
|
258
|
+
|
|
259
|
+
def wipe_all_host_tokens() -> int:
|
|
260
|
+
kv = _load_db_kv()
|
|
261
|
+
|
|
262
|
+
# Count how many hosts are present
|
|
263
|
+
host_keys: list[str] = []
|
|
264
|
+
for k in kv.keys():
|
|
265
|
+
if k.startswith("HOST_") and k.endswith("_ID"):
|
|
266
|
+
hk = k[len("HOST_") : -len("_ID")]
|
|
267
|
+
host_keys.append(hk)
|
|
268
|
+
|
|
269
|
+
# Remove all per-host keys
|
|
270
|
+
for hk in host_keys:
|
|
271
|
+
for kk in _host_record_keys(hk):
|
|
272
|
+
kv.pop(kk, None)
|
|
273
|
+
|
|
274
|
+
_save_db_kv(kv)
|
|
275
|
+
return len(host_keys)
|
|
276
|
+
|
|
277
|
+
def show_host_tokens(*, include_secrets: bool = False, file=sys.stdout) -> None:
|
|
278
|
+
recs = list_host_token_records(include_secrets=include_secrets)
|
|
279
|
+
if not recs:
|
|
280
|
+
print(f"No host tokens found ({_hosts_auth_path()}).", file=file)
|
|
281
|
+
return
|
|
282
|
+
|
|
283
|
+
print(f"Host tokens ({_hosts_auth_path()}):", file=file)
|
|
284
|
+
for r in recs:
|
|
285
|
+
host_id = r.get("host_id", "")
|
|
286
|
+
status = r.get("status", "")
|
|
287
|
+
created = r.get("created_utc", "")
|
|
288
|
+
updated = r.get("updated_utc", "")
|
|
289
|
+
print(f" - {host_id} status={status} created={created} updated={updated}", file=file)
|
|
290
|
+
if include_secrets:
|
|
291
|
+
print(f" salt={r.get('salt','')}", file=file)
|
|
292
|
+
print(f" hash={r.get('hash','')}", file=file)
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
# src/remoteRF_host/host/host_directory_store.py
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import re
|
|
7
|
+
import threading
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Dict, List, Optional
|
|
10
|
+
|
|
11
|
+
_ENV_LINE = re.compile(r"^\s*([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*?)\s*$")
|
|
12
|
+
|
|
13
|
+
def now_ms() -> int:
|
|
14
|
+
import time
|
|
15
|
+
return int(time.time() * 1000)
|
|
16
|
+
|
|
17
|
+
def repo_root_from_file(file: str) -> Path:
|
|
18
|
+
p = Path(file).resolve()
|
|
19
|
+
for parent in p.parents:
|
|
20
|
+
if parent.name == "src":
|
|
21
|
+
return parent.parent
|
|
22
|
+
return p.parents[3]
|
|
23
|
+
|
|
24
|
+
def cfg_dir_from_file(file: str) -> Path:
|
|
25
|
+
return repo_root_from_file(file) / ".config"
|
|
26
|
+
|
|
27
|
+
def sanitize_env_key(s: str) -> str:
|
|
28
|
+
s = (s or "").strip().upper()
|
|
29
|
+
s = re.sub(r"[^A-Z0-9_]", "_", s)
|
|
30
|
+
s = re.sub(r"_+", "_", s)
|
|
31
|
+
return s[:120]
|
|
32
|
+
|
|
33
|
+
def strip_quotes(v: str) -> str:
|
|
34
|
+
v = (v or "").strip()
|
|
35
|
+
if len(v) >= 2 and ((v[0] == v[-1] == '"') or (v[0] == v[-1] == "'")):
|
|
36
|
+
return v[1:-1]
|
|
37
|
+
return v
|
|
38
|
+
|
|
39
|
+
def csv_split(v: str) -> List[str]:
|
|
40
|
+
return [x.strip() for x in (v or "").split(",") if x.strip()]
|
|
41
|
+
|
|
42
|
+
class DeviceIdConflictError(RuntimeError):
|
|
43
|
+
def __init__(self, *, device_id: str, existing_host: str, new_host: str) -> None:
|
|
44
|
+
super().__init__(
|
|
45
|
+
f"device_id '{device_id}' already owned by host '{existing_host}', rejecting host '{new_host}'"
|
|
46
|
+
)
|
|
47
|
+
self.device_id = device_id
|
|
48
|
+
self.existing_host = existing_host
|
|
49
|
+
self.new_host = new_host
|
|
50
|
+
|
|
51
|
+
class EnvStore:
|
|
52
|
+
def __init__(self, path: Path) -> None:
|
|
53
|
+
self.path = Path(path)
|
|
54
|
+
self._lock = threading.Lock()
|
|
55
|
+
|
|
56
|
+
def _read_lines(self) -> List[str]:
|
|
57
|
+
if not self.path.exists():
|
|
58
|
+
return []
|
|
59
|
+
return self.path.read_text(encoding="utf-8").splitlines()
|
|
60
|
+
|
|
61
|
+
def _write_lines(self, lines: List[str]) -> None:
|
|
62
|
+
self.path.parent.mkdir(parents=True, exist_ok=True)
|
|
63
|
+
self.path.write_text("\n".join(lines) + ("\n" if lines else ""), encoding="utf-8")
|
|
64
|
+
|
|
65
|
+
def read_kv(self) -> Dict[str, str]:
|
|
66
|
+
out: Dict[str, str] = {}
|
|
67
|
+
for raw in self._read_lines():
|
|
68
|
+
line = raw.strip()
|
|
69
|
+
if not line or line.startswith("#"):
|
|
70
|
+
continue
|
|
71
|
+
m = _ENV_LINE.match(line)
|
|
72
|
+
if not m:
|
|
73
|
+
continue
|
|
74
|
+
k = m.group(1).strip()
|
|
75
|
+
v = strip_quotes(m.group(2))
|
|
76
|
+
out[k] = v
|
|
77
|
+
return out
|
|
78
|
+
|
|
79
|
+
def append_to_csv_list(self, key: str, value: str) -> None:
|
|
80
|
+
key = sanitize_env_key(key)
|
|
81
|
+
value = (value or "").strip()
|
|
82
|
+
if not value:
|
|
83
|
+
return
|
|
84
|
+
|
|
85
|
+
with self._lock:
|
|
86
|
+
lines = self._read_lines()
|
|
87
|
+
idx: Optional[int] = None
|
|
88
|
+
cur = ""
|
|
89
|
+
|
|
90
|
+
for i, ln in enumerate(lines):
|
|
91
|
+
if ln.startswith(key + "="):
|
|
92
|
+
idx = i
|
|
93
|
+
cur = ln[len(key) + 1:].strip()
|
|
94
|
+
break
|
|
95
|
+
|
|
96
|
+
existing = csv_split(cur)
|
|
97
|
+
if value in existing:
|
|
98
|
+
return
|
|
99
|
+
|
|
100
|
+
existing.append(value)
|
|
101
|
+
new_line = f"{key}=" + ",".join(existing)
|
|
102
|
+
|
|
103
|
+
if idx is None:
|
|
104
|
+
lines.append(new_line)
|
|
105
|
+
else:
|
|
106
|
+
lines[idx] = new_line
|
|
107
|
+
|
|
108
|
+
self._write_lines(lines)
|
|
109
|
+
|
|
110
|
+
def upsert_kv(self, key: str, value: str) -> None:
|
|
111
|
+
key = sanitize_env_key(key)
|
|
112
|
+
value = (value or "").strip()
|
|
113
|
+
|
|
114
|
+
with self._lock:
|
|
115
|
+
lines = self._read_lines()
|
|
116
|
+
idx: Optional[int] = None
|
|
117
|
+
for i, ln in enumerate(lines):
|
|
118
|
+
if ln.startswith(key + "="):
|
|
119
|
+
idx = i
|
|
120
|
+
break
|
|
121
|
+
|
|
122
|
+
new_line = f"{key}={value}"
|
|
123
|
+
if idx is None:
|
|
124
|
+
lines.append(new_line)
|
|
125
|
+
else:
|
|
126
|
+
lines[idx] = new_line
|
|
127
|
+
|
|
128
|
+
self._write_lines(lines)
|
|
129
|
+
|
|
130
|
+
def set_kv_if_absent(self, key: str, value: str) -> None:
|
|
131
|
+
key = sanitize_env_key(key)
|
|
132
|
+
value = (value or "").strip()
|
|
133
|
+
if not value:
|
|
134
|
+
return
|
|
135
|
+
|
|
136
|
+
with self._lock:
|
|
137
|
+
lines = self._read_lines()
|
|
138
|
+
for ln in lines:
|
|
139
|
+
if ln.startswith(key + "="):
|
|
140
|
+
return
|
|
141
|
+
lines.append(f"{key}={value}")
|
|
142
|
+
self._write_lines(lines)
|