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,723 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
from typing import Any, Dict, List, Optional
|
|
3
|
+
import csv
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
import re
|
|
7
|
+
from datetime import datetime
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
from .user_group_handler import user_group_handler
|
|
11
|
+
|
|
12
|
+
from .user_group_handler import user_group_handler
|
|
13
|
+
|
|
14
|
+
CANCEL = {"c", "e", "exit", "q", "quit"}
|
|
15
|
+
|
|
16
|
+
def make_user_group_local(session) -> None:
|
|
17
|
+
def _prompt_str(msg: str) -> Optional[str]:
|
|
18
|
+
s = (session.prompt(msg) or "").strip()
|
|
19
|
+
if s.lower() in CANCEL:
|
|
20
|
+
return None
|
|
21
|
+
return s
|
|
22
|
+
|
|
23
|
+
def _prompt_yes_no(msg: str, default_no: bool = True) -> bool:
|
|
24
|
+
s = (session.prompt(msg) or "").strip().lower()
|
|
25
|
+
if not s:
|
|
26
|
+
return not default_no
|
|
27
|
+
return s in {"y", "yes"}
|
|
28
|
+
|
|
29
|
+
def _prompt_int(msg: str, *, default: Optional[int] = None, min_v: Optional[int] = 0) -> Optional[int]:
|
|
30
|
+
while True:
|
|
31
|
+
s = (session.prompt(msg) or "").strip()
|
|
32
|
+
if s.lower() in CANCEL:
|
|
33
|
+
return None
|
|
34
|
+
if not s:
|
|
35
|
+
if default is None:
|
|
36
|
+
print("Please enter a value (or 'c' to cancel).")
|
|
37
|
+
continue
|
|
38
|
+
val = default
|
|
39
|
+
else:
|
|
40
|
+
try:
|
|
41
|
+
val = int(s)
|
|
42
|
+
except ValueError:
|
|
43
|
+
print("Please enter an integer (or 'c' to cancel).")
|
|
44
|
+
continue
|
|
45
|
+
|
|
46
|
+
if min_v is not None and val < min_v:
|
|
47
|
+
print(f"Value must be >= {min_v}.")
|
|
48
|
+
continue
|
|
49
|
+
return val
|
|
50
|
+
|
|
51
|
+
def _prompt_int_list(msg: str) -> Optional[List[int]]:
|
|
52
|
+
while True:
|
|
53
|
+
s = (session.prompt(msg) or "").strip()
|
|
54
|
+
if s.lower() in CANCEL:
|
|
55
|
+
return None
|
|
56
|
+
|
|
57
|
+
if not s:
|
|
58
|
+
print("Please enter at least one device id (e.g. '0,1,2') or 'c' to cancel.")
|
|
59
|
+
continue
|
|
60
|
+
|
|
61
|
+
# Allow both commas and whitespace
|
|
62
|
+
parts = [p for p in s.replace(",", " ").split() if p]
|
|
63
|
+
try:
|
|
64
|
+
vals = [int(p) for p in parts]
|
|
65
|
+
except ValueError:
|
|
66
|
+
print("Invalid list. Use e.g. '0,1,2' or '0 1 2'.")
|
|
67
|
+
continue
|
|
68
|
+
|
|
69
|
+
if any(v < 0 for v in vals):
|
|
70
|
+
print("Device IDs must be >= 0.")
|
|
71
|
+
continue
|
|
72
|
+
|
|
73
|
+
# Dedup while preserving order
|
|
74
|
+
seen = set()
|
|
75
|
+
out = []
|
|
76
|
+
for v in vals:
|
|
77
|
+
if v not in seen:
|
|
78
|
+
seen.add(v)
|
|
79
|
+
out.append(v)
|
|
80
|
+
|
|
81
|
+
if len(out) == 0:
|
|
82
|
+
print("Please enter at least one device id (or 'c' to cancel).")
|
|
83
|
+
continue
|
|
84
|
+
|
|
85
|
+
return out
|
|
86
|
+
|
|
87
|
+
# ---- Group name ----
|
|
88
|
+
group_name = _prompt_str("Enter group name (c to cancel): ")
|
|
89
|
+
if not group_name:
|
|
90
|
+
return
|
|
91
|
+
|
|
92
|
+
if not _prompt_yes_no(f"Confirm group name '{group_name}'? (y/n): ", default_no=True):
|
|
93
|
+
return
|
|
94
|
+
|
|
95
|
+
# ---- Parameters ----
|
|
96
|
+
devices_whitelist = _prompt_int_list("Devices whitelist (e.g. 0,1,2) (c to cancel): ")
|
|
97
|
+
if devices_whitelist is None:
|
|
98
|
+
return
|
|
99
|
+
|
|
100
|
+
max_reservation_time_sec = _prompt_int("Max reservation time (sec) [1800] (c to cancel): ", default=1800, min_v=1)
|
|
101
|
+
if max_reservation_time_sec is None:
|
|
102
|
+
return
|
|
103
|
+
|
|
104
|
+
max_reservations = _prompt_int("Max concurrent reservations [3] (c to cancel): ", default=3, min_v=1)
|
|
105
|
+
if max_reservations is None:
|
|
106
|
+
return
|
|
107
|
+
|
|
108
|
+
lifetime_sec = _prompt_int("Group lifetime (sec) [0=forever] (c to cancel): ", default=0, min_v=0)
|
|
109
|
+
if lifetime_sec is None:
|
|
110
|
+
return
|
|
111
|
+
|
|
112
|
+
# ---- Final confirmation ----
|
|
113
|
+
wl_str = ",".join(map(str, devices_whitelist))
|
|
114
|
+
print("\nAbout to create user group with:")
|
|
115
|
+
print(f" group_name : {group_name}")
|
|
116
|
+
print(f" devices_whitelist : {wl_str}")
|
|
117
|
+
print(f" max_reservation_time_sec : {max_reservation_time_sec}")
|
|
118
|
+
print(f" max_reservations : {max_reservations}")
|
|
119
|
+
print(f" lifetime_sec : {lifetime_sec}")
|
|
120
|
+
|
|
121
|
+
if not _prompt_yes_no("Proceed? (y/n): ", default_no=True):
|
|
122
|
+
return
|
|
123
|
+
|
|
124
|
+
# ---- Create ----
|
|
125
|
+
ok = user_group_handler.create_user_group(
|
|
126
|
+
group_name=group_name,
|
|
127
|
+
devices_whitelist=devices_whitelist,
|
|
128
|
+
max_reservation_time_sec=max_reservation_time_sec,
|
|
129
|
+
max_reservations=max_reservations,
|
|
130
|
+
lifetime_sec=lifetime_sec,
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
if ok:
|
|
134
|
+
print("User group created.")
|
|
135
|
+
else:
|
|
136
|
+
print("Failed to create user group.")
|
|
137
|
+
|
|
138
|
+
def remove_user_group_local(session) -> None:
|
|
139
|
+
|
|
140
|
+
def _prompt_int(msg: str) -> Optional[int]:
|
|
141
|
+
while True:
|
|
142
|
+
s = (session.prompt(msg) or "").strip()
|
|
143
|
+
if s.lower() in CANCEL:
|
|
144
|
+
return None
|
|
145
|
+
try:
|
|
146
|
+
v = int(s)
|
|
147
|
+
except ValueError:
|
|
148
|
+
print("Please enter an integer group ID (or 'c' to cancel).")
|
|
149
|
+
continue
|
|
150
|
+
if v < 0:
|
|
151
|
+
print("Group ID must be >= 0.")
|
|
152
|
+
continue
|
|
153
|
+
return v
|
|
154
|
+
|
|
155
|
+
def _prompt_yes_no(msg: str) -> bool:
|
|
156
|
+
s = (session.prompt(msg) or "").strip().lower()
|
|
157
|
+
return s in {"y", "yes"}
|
|
158
|
+
|
|
159
|
+
group_id = _prompt_int("Enter group ID to delete (c to cancel): ")
|
|
160
|
+
if group_id is None:
|
|
161
|
+
return
|
|
162
|
+
|
|
163
|
+
if not _prompt_yes_no(f"Confirm delete group_id={group_id}? (y/n): "):
|
|
164
|
+
return
|
|
165
|
+
|
|
166
|
+
ok = user_group_handler.delete_user_group(group_id=group_id)
|
|
167
|
+
|
|
168
|
+
if ok:
|
|
169
|
+
print(f"Deleted user group {group_id}.")
|
|
170
|
+
else:
|
|
171
|
+
print(f"Failed to delete user group {group_id}.")
|
|
172
|
+
|
|
173
|
+
def list_user_groups_local(session) -> None:
|
|
174
|
+
|
|
175
|
+
def _prompt_yes_no(msg: str, default_no: bool = True) -> bool:
|
|
176
|
+
s = (session.prompt(msg) or "").strip().lower()
|
|
177
|
+
if not s:
|
|
178
|
+
return not default_no
|
|
179
|
+
return s in {"y", "yes"}
|
|
180
|
+
|
|
181
|
+
# include_users = _prompt_yes_no("Include users in output? (y/n) [n]: ", default_no=True)
|
|
182
|
+
include_users = False
|
|
183
|
+
|
|
184
|
+
groups: List[Dict[str, Any]] = user_group_handler.get_all_user_groups(include_users=include_users) or []
|
|
185
|
+
|
|
186
|
+
if not groups:
|
|
187
|
+
print("No user groups found.")
|
|
188
|
+
return
|
|
189
|
+
|
|
190
|
+
# Print a compact table-like view.
|
|
191
|
+
def _fmt(v: Any) -> str:
|
|
192
|
+
if v is None:
|
|
193
|
+
return ""
|
|
194
|
+
if isinstance(v, (list, tuple)):
|
|
195
|
+
return ",".join(str(x) for x in v)
|
|
196
|
+
if isinstance(v, dict):
|
|
197
|
+
return str(v)
|
|
198
|
+
return str(v)
|
|
199
|
+
|
|
200
|
+
# Prefer common keys if present
|
|
201
|
+
preferred = [
|
|
202
|
+
"group_id",
|
|
203
|
+
"id",
|
|
204
|
+
"group_name",
|
|
205
|
+
"name",
|
|
206
|
+
"devices_whitelist",
|
|
207
|
+
"max_reservation_time_sec",
|
|
208
|
+
"max_reservations",
|
|
209
|
+
"lifetime_sec",
|
|
210
|
+
"created_at",
|
|
211
|
+
"users",
|
|
212
|
+
]
|
|
213
|
+
|
|
214
|
+
# Choose columns: preferred keys that exist in at least one record, then any remaining keys.
|
|
215
|
+
present = set().union(*(g.keys() for g in groups))
|
|
216
|
+
columns = [k for k in preferred if k in present]
|
|
217
|
+
columns += [k for k in sorted(present) if k not in columns]
|
|
218
|
+
|
|
219
|
+
# If not including users, drop users column if present to keep output clean.
|
|
220
|
+
if not include_users and "users" in columns:
|
|
221
|
+
columns.remove("users")
|
|
222
|
+
|
|
223
|
+
# Compute widths (cap long values)
|
|
224
|
+
def _cap(s: str, n: int = 48) -> str:
|
|
225
|
+
return s if len(s) <= n else s[: n - 3] + "..."
|
|
226
|
+
|
|
227
|
+
widths = {c: len(c) for c in columns}
|
|
228
|
+
rows: List[List[str]] = []
|
|
229
|
+
for g in groups:
|
|
230
|
+
row = []
|
|
231
|
+
for c in columns:
|
|
232
|
+
val = _fmt(g.get(c, ""))
|
|
233
|
+
val = _cap(val)
|
|
234
|
+
widths[c] = max(widths[c], len(val))
|
|
235
|
+
row.append(val)
|
|
236
|
+
rows.append(row)
|
|
237
|
+
|
|
238
|
+
# Header
|
|
239
|
+
header = " ".join(c.ljust(widths[c]) for c in columns)
|
|
240
|
+
sep = " ".join("-" * widths[c] for c in columns)
|
|
241
|
+
print(header)
|
|
242
|
+
print(sep)
|
|
243
|
+
|
|
244
|
+
# Rows
|
|
245
|
+
for row in rows:
|
|
246
|
+
print(" ".join(row[i].ljust(widths[columns[i]]) for i in range(len(columns))))
|
|
247
|
+
|
|
248
|
+
# If include_users, print expanded user lists (if present) for readability
|
|
249
|
+
# if include_users:
|
|
250
|
+
# # Find a reasonable key name for users if exists
|
|
251
|
+
# user_key = "users" if any("users" in g for g in groups) else None
|
|
252
|
+
# if user_key:
|
|
253
|
+
# print("\nUsers per group:")
|
|
254
|
+
# for g in groups:
|
|
255
|
+
# gid = g.get("group_id", g.get("id", "?"))
|
|
256
|
+
# name = g.get("group_name", g.get("name", ""))
|
|
257
|
+
# users = g.get(user_key, [])
|
|
258
|
+
# if not users:
|
|
259
|
+
# continue
|
|
260
|
+
# print(f" {gid} {name}:")
|
|
261
|
+
# if isinstance(users, list):
|
|
262
|
+
# for u in users:
|
|
263
|
+
# print(f" - {u}")
|
|
264
|
+
# else:
|
|
265
|
+
# print(f" - {users}")
|
|
266
|
+
|
|
267
|
+
def edit_user_group_local(session) -> None:
|
|
268
|
+
|
|
269
|
+
def _prompt_raw(msg: str) -> Optional[str]:
|
|
270
|
+
s = (session.prompt(msg) or "").strip()
|
|
271
|
+
if s.lower() in CANCEL:
|
|
272
|
+
return None
|
|
273
|
+
return s
|
|
274
|
+
|
|
275
|
+
def _prompt_int_optional(msg: str, *, min_v: Optional[int] = 0) -> Optional[Optional[int]]:
|
|
276
|
+
s = _prompt_raw(msg)
|
|
277
|
+
if s is None:
|
|
278
|
+
return None # cancelled
|
|
279
|
+
if s == "":
|
|
280
|
+
return "" # sentinel for unchanged
|
|
281
|
+
try:
|
|
282
|
+
v = int(s)
|
|
283
|
+
except ValueError:
|
|
284
|
+
print("Please enter an integer, or press Enter to keep unchanged, or 'c' to cancel.")
|
|
285
|
+
return _prompt_int_optional(msg, min_v=min_v)
|
|
286
|
+
if min_v is not None and v < min_v:
|
|
287
|
+
print(f"Value must be >= {min_v}.")
|
|
288
|
+
return _prompt_int_optional(msg, min_v=min_v)
|
|
289
|
+
return v
|
|
290
|
+
|
|
291
|
+
def _prompt_devices_optional(msg: str) -> Optional[object]:
|
|
292
|
+
s = _prompt_raw(msg)
|
|
293
|
+
if s is None:
|
|
294
|
+
return None # cancelled
|
|
295
|
+
if s == "":
|
|
296
|
+
return "" # unchanged
|
|
297
|
+
if s.lower() in {"all", "*"}:
|
|
298
|
+
return []
|
|
299
|
+
parts = [p for p in s.replace(",", " ").split() if p]
|
|
300
|
+
try:
|
|
301
|
+
vals = [int(p) for p in parts]
|
|
302
|
+
except ValueError:
|
|
303
|
+
print("Invalid list. Use e.g. '0,1,2' or '0 1 2' or 'all'.")
|
|
304
|
+
return _prompt_devices_optional(msg)
|
|
305
|
+
if any(v < 0 for v in vals):
|
|
306
|
+
print("Device IDs must be >= 0.")
|
|
307
|
+
return _prompt_devices_optional(msg)
|
|
308
|
+
# dedup preserve order
|
|
309
|
+
seen = set()
|
|
310
|
+
out: List[int] = []
|
|
311
|
+
for v in vals:
|
|
312
|
+
if v not in seen:
|
|
313
|
+
seen.add(v)
|
|
314
|
+
out.append(v)
|
|
315
|
+
return out
|
|
316
|
+
|
|
317
|
+
def _prompt_yes_no(msg: str) -> bool:
|
|
318
|
+
s = (session.prompt(msg) or "").strip().lower()
|
|
319
|
+
return s in {"y", "yes"}
|
|
320
|
+
|
|
321
|
+
# ---- Required: group_id ----
|
|
322
|
+
gid_raw = _prompt_raw("Enter group ID to edit (c to cancel): ")
|
|
323
|
+
if gid_raw is None:
|
|
324
|
+
return
|
|
325
|
+
try:
|
|
326
|
+
group_id = int(gid_raw)
|
|
327
|
+
except ValueError:
|
|
328
|
+
print("Group ID must be an integer.")
|
|
329
|
+
return
|
|
330
|
+
if group_id < 0:
|
|
331
|
+
print("Group ID must be >= 0.")
|
|
332
|
+
return
|
|
333
|
+
|
|
334
|
+
# ---- Optional fields ----
|
|
335
|
+
group_name = _prompt_raw("New group name (Enter to keep unchanged, c to cancel): ")
|
|
336
|
+
if group_name is None:
|
|
337
|
+
return
|
|
338
|
+
if group_name == "":
|
|
339
|
+
group_name = None
|
|
340
|
+
|
|
341
|
+
devices_whitelist = _prompt_devices_optional(
|
|
342
|
+
"New devices whitelist (e.g. 0,1,2 | 'all' => [] | Enter unchanged, c cancel): "
|
|
343
|
+
)
|
|
344
|
+
if devices_whitelist is None:
|
|
345
|
+
return
|
|
346
|
+
if devices_whitelist == "":
|
|
347
|
+
devices_whitelist = None # unchanged
|
|
348
|
+
|
|
349
|
+
max_res_time = _prompt_int_optional("New max reservation time sec (Enter unchanged, c cancel): ", min_v=1)
|
|
350
|
+
if max_res_time is None:
|
|
351
|
+
return
|
|
352
|
+
if max_res_time == "":
|
|
353
|
+
max_res_time = None
|
|
354
|
+
|
|
355
|
+
max_reservations = _prompt_int_optional("New max reservations (Enter unchanged, c cancel): ", min_v=1)
|
|
356
|
+
if max_reservations is None:
|
|
357
|
+
return
|
|
358
|
+
if max_reservations == "":
|
|
359
|
+
max_reservations = None
|
|
360
|
+
|
|
361
|
+
lifetime_sec = _prompt_int_optional("New lifetime sec (0=forever) (Enter unchanged, c cancel): ", min_v=0)
|
|
362
|
+
if lifetime_sec is None:
|
|
363
|
+
return
|
|
364
|
+
if lifetime_sec == "":
|
|
365
|
+
lifetime_sec = None
|
|
366
|
+
|
|
367
|
+
# Ensure at least one change
|
|
368
|
+
if (
|
|
369
|
+
group_name is None
|
|
370
|
+
and devices_whitelist is None
|
|
371
|
+
and max_res_time is None
|
|
372
|
+
and max_reservations is None
|
|
373
|
+
and lifetime_sec is None
|
|
374
|
+
):
|
|
375
|
+
print("No changes specified. Nothing to do.")
|
|
376
|
+
return
|
|
377
|
+
|
|
378
|
+
# ---- Summary + confirm ----
|
|
379
|
+
def _fmt_devices(v: Optional[List[int]]) -> str:
|
|
380
|
+
if v is None:
|
|
381
|
+
return "(unchanged)"
|
|
382
|
+
return "ALL([])" if len(v) == 0 else ",".join(map(str, v))
|
|
383
|
+
|
|
384
|
+
print("\nAbout to edit user group:")
|
|
385
|
+
print(f" group_id : {group_id}")
|
|
386
|
+
print(f" group_name : {group_name if group_name is not None else '(unchanged)'}")
|
|
387
|
+
print(f" devices_whitelist : {_fmt_devices(devices_whitelist)}")
|
|
388
|
+
print(f" max_reservation_time_sec: {max_res_time if max_res_time is not None else '(unchanged)'}")
|
|
389
|
+
print(f" max_reservations : {max_reservations if max_reservations is not None else '(unchanged)'}")
|
|
390
|
+
print(f" lifetime_sec : {lifetime_sec if lifetime_sec is not None else '(unchanged)'}")
|
|
391
|
+
|
|
392
|
+
if not _prompt_yes_no("Proceed? (y/n): "):
|
|
393
|
+
return
|
|
394
|
+
|
|
395
|
+
ok = user_group_handler.edit_user_group(
|
|
396
|
+
group_id=group_id,
|
|
397
|
+
group_name=group_name,
|
|
398
|
+
devices_whitelist=devices_whitelist, # type: ignore[arg-type]
|
|
399
|
+
max_reservation_time_sec=max_res_time, # type: ignore[arg-type]
|
|
400
|
+
max_reservations=max_reservations, # type: ignore[arg-type]
|
|
401
|
+
lifetime_sec=lifetime_sec, # type: ignore[arg-type]
|
|
402
|
+
)
|
|
403
|
+
|
|
404
|
+
if ok:
|
|
405
|
+
print("User group updated.")
|
|
406
|
+
else:
|
|
407
|
+
print("Failed to update user group.")
|
|
408
|
+
|
|
409
|
+
def make_enrollment_codes_local(session) -> None:
|
|
410
|
+
|
|
411
|
+
def _prompt_raw(msg: str) -> Optional[str]:
|
|
412
|
+
s = (session.prompt(msg) or "").strip()
|
|
413
|
+
if s.lower() in CANCEL:
|
|
414
|
+
return None
|
|
415
|
+
return s
|
|
416
|
+
|
|
417
|
+
def _prompt_yes_no(msg: str, default_no: bool = True) -> bool:
|
|
418
|
+
s = (session.prompt(msg) or "").strip().lower()
|
|
419
|
+
if not s:
|
|
420
|
+
return not default_no
|
|
421
|
+
return s in {"y", "yes"}
|
|
422
|
+
|
|
423
|
+
def _prompt_int(msg: str, *, default: Optional[int] = None, min_v: Optional[int] = 0) -> Optional[int]:
|
|
424
|
+
while True:
|
|
425
|
+
s = _prompt_raw(msg)
|
|
426
|
+
if s is None:
|
|
427
|
+
return None
|
|
428
|
+
if s == "":
|
|
429
|
+
if default is None:
|
|
430
|
+
print("Please enter a value (or 'c' to cancel).")
|
|
431
|
+
continue
|
|
432
|
+
v = default
|
|
433
|
+
else:
|
|
434
|
+
try:
|
|
435
|
+
v = int(s)
|
|
436
|
+
except ValueError:
|
|
437
|
+
print("Please enter an integer (or 'c' to cancel).")
|
|
438
|
+
continue
|
|
439
|
+
if min_v is not None and v < min_v:
|
|
440
|
+
print(f"Value must be >= {min_v}.")
|
|
441
|
+
continue
|
|
442
|
+
return v
|
|
443
|
+
|
|
444
|
+
count = _prompt_int("How many enrollment codes? (c to cancel): ", min_v=1)
|
|
445
|
+
if count is None:
|
|
446
|
+
return
|
|
447
|
+
|
|
448
|
+
group_id = _prompt_int("Group ID to attach codes to? (c to cancel): ", min_v=0)
|
|
449
|
+
if group_id is None:
|
|
450
|
+
return
|
|
451
|
+
|
|
452
|
+
char_count = _prompt_int("Characters per code name? [10] (c to cancel): ", default=10, min_v=4)
|
|
453
|
+
if char_count is None:
|
|
454
|
+
return
|
|
455
|
+
|
|
456
|
+
try:
|
|
457
|
+
code_names: List[str] = user_group_handler.generate_random_enrollment_code_names(
|
|
458
|
+
count=count,
|
|
459
|
+
char_count=char_count,
|
|
460
|
+
) or []
|
|
461
|
+
except Exception as e:
|
|
462
|
+
print(f"Failed to generate code names: {e}")
|
|
463
|
+
return
|
|
464
|
+
|
|
465
|
+
if not code_names:
|
|
466
|
+
print("No codes generated.")
|
|
467
|
+
return
|
|
468
|
+
|
|
469
|
+
print("\nGenerated code names:")
|
|
470
|
+
for i, name in enumerate(code_names, 1):
|
|
471
|
+
print(f" {i:>2}) {name}")
|
|
472
|
+
|
|
473
|
+
if not _prompt_yes_no("\nUse these codes? (y/n): ", default_no=True):
|
|
474
|
+
return
|
|
475
|
+
|
|
476
|
+
uses = _prompt_int("Uses per code? [1] (c to cancel): ", default=1, min_v=1)
|
|
477
|
+
if uses is None:
|
|
478
|
+
return
|
|
479
|
+
|
|
480
|
+
duration_sec = _prompt_int(
|
|
481
|
+
"Duration in seconds? [3600] (0=never expires) (c to cancel): ",
|
|
482
|
+
default=3600,
|
|
483
|
+
min_v=0,
|
|
484
|
+
)
|
|
485
|
+
if duration_sec is None:
|
|
486
|
+
return
|
|
487
|
+
|
|
488
|
+
print("\nAbout to create enrollment codes with:")
|
|
489
|
+
print(f" group_id : {group_id}")
|
|
490
|
+
print(f" count : {len(code_names)}")
|
|
491
|
+
print(f" uses : {uses}")
|
|
492
|
+
print(f" duration_sec : {duration_sec}")
|
|
493
|
+
|
|
494
|
+
if not _prompt_yes_no("Proceed? (y/n): ", default_no=True):
|
|
495
|
+
return
|
|
496
|
+
|
|
497
|
+
result = user_group_handler.create_enrollment_codes(
|
|
498
|
+
code_names=code_names,
|
|
499
|
+
group_id=group_id,
|
|
500
|
+
uses=uses,
|
|
501
|
+
duration_sec=duration_sec,
|
|
502
|
+
)
|
|
503
|
+
print("\nCreated enrollment codes:")
|
|
504
|
+
print(result)
|
|
505
|
+
|
|
506
|
+
def remove_enrollment_code_local(session) -> None:
|
|
507
|
+
|
|
508
|
+
def _prompt(msg: str):
|
|
509
|
+
s = (session.prompt(msg) or "").strip()
|
|
510
|
+
return None if s.lower() in CANCEL else s
|
|
511
|
+
|
|
512
|
+
def _yes(msg: str) -> bool:
|
|
513
|
+
return (session.prompt(msg) or "").strip().lower() in {"y", "yes"}
|
|
514
|
+
|
|
515
|
+
mode = _prompt("Delete by (n)ame or (g)roup_id? [n] (c to cancel): ")
|
|
516
|
+
if mode is None:
|
|
517
|
+
return
|
|
518
|
+
mode = (mode or "n").lower()
|
|
519
|
+
|
|
520
|
+
if mode.startswith("n"):
|
|
521
|
+
code_name = _prompt("Enter code_name (c to cancel): ")
|
|
522
|
+
if not code_name:
|
|
523
|
+
return
|
|
524
|
+
if not _yes(f"Delete enrollment code '{code_name}'? (y/n): "):
|
|
525
|
+
return
|
|
526
|
+
ok = user_group_handler.delete_enrollment_code_name(code_name=code_name)
|
|
527
|
+
print("Deleted." if ok else "Not found / failed.")
|
|
528
|
+
return
|
|
529
|
+
|
|
530
|
+
if mode.startswith("g"):
|
|
531
|
+
gid_s = _prompt("Enter group_id to delete ALL codes for that group (c to cancel): ")
|
|
532
|
+
if not gid_s:
|
|
533
|
+
return
|
|
534
|
+
try:
|
|
535
|
+
group_id = int(gid_s)
|
|
536
|
+
except ValueError:
|
|
537
|
+
print("group_id must be an integer.")
|
|
538
|
+
return
|
|
539
|
+
if group_id < 0:
|
|
540
|
+
print("group_id must be >= 0.")
|
|
541
|
+
return
|
|
542
|
+
if not _yes(f"Delete ALL enrollment codes for group_id={group_id}? (y/n): "):
|
|
543
|
+
return
|
|
544
|
+
ok = user_group_handler.delete_enrollment_code_id(group_id=group_id)
|
|
545
|
+
print("Deleted." if ok else "None deleted / failed.")
|
|
546
|
+
return
|
|
547
|
+
|
|
548
|
+
print("Unknown option. Use 'n' or 'g'.")
|
|
549
|
+
|
|
550
|
+
def list_enrollment_codes_local(session) -> None:
|
|
551
|
+
rows: List[Dict[str, Any]] = user_group_handler.get_all_enrollment_codes() or []
|
|
552
|
+
|
|
553
|
+
if not rows:
|
|
554
|
+
print("No enrollment codes found.")
|
|
555
|
+
return
|
|
556
|
+
|
|
557
|
+
preferred = [
|
|
558
|
+
"code_name",
|
|
559
|
+
"group_id",
|
|
560
|
+
"uses",
|
|
561
|
+
"uses_left",
|
|
562
|
+
"remaining_uses",
|
|
563
|
+
"duration_sec",
|
|
564
|
+
"expires_at",
|
|
565
|
+
"created_at",
|
|
566
|
+
]
|
|
567
|
+
present = set().union(*(r.keys() for r in rows))
|
|
568
|
+
cols = [c for c in preferred if c in present] + [c for c in sorted(present) if c not in preferred]
|
|
569
|
+
|
|
570
|
+
def _fmt(v: Any) -> str:
|
|
571
|
+
if v is None:
|
|
572
|
+
return ""
|
|
573
|
+
if isinstance(v, (list, tuple)):
|
|
574
|
+
return ",".join(str(x) for x in v)
|
|
575
|
+
return str(v)
|
|
576
|
+
|
|
577
|
+
def _cap(s: str, n: int = 48) -> str:
|
|
578
|
+
return s if len(s) <= n else s[: n - 3] + "..."
|
|
579
|
+
|
|
580
|
+
# Compute widths
|
|
581
|
+
widths = {c: len(c) for c in cols}
|
|
582
|
+
table: List[List[str]] = []
|
|
583
|
+
for r in rows:
|
|
584
|
+
row = []
|
|
585
|
+
for c in cols:
|
|
586
|
+
cell = _cap(_fmt(r.get(c, "")))
|
|
587
|
+
widths[c] = max(widths[c], len(cell))
|
|
588
|
+
row.append(cell)
|
|
589
|
+
table.append(row)
|
|
590
|
+
|
|
591
|
+
# Print
|
|
592
|
+
header = " ".join(c.ljust(widths[c]) for c in cols)
|
|
593
|
+
sep = " ".join("-" * widths[c] for c in cols)
|
|
594
|
+
print(header)
|
|
595
|
+
print(sep)
|
|
596
|
+
for row in table:
|
|
597
|
+
print(" ".join(row[i].ljust(widths[cols[i]]) for i in range(len(cols))))
|
|
598
|
+
|
|
599
|
+
def _resolve_download_dir() -> Path:
|
|
600
|
+
"""
|
|
601
|
+
Best-effort Downloads folder resolution:
|
|
602
|
+
1) XDG_DOWNLOAD_DIR env (if set)
|
|
603
|
+
2) ~/.config/user-dirs.dirs XDG_DOWNLOAD_DIR (Linux desktops)
|
|
604
|
+
3) ~/Downloads
|
|
605
|
+
4) fallback: cwd
|
|
606
|
+
"""
|
|
607
|
+
# 1) Env override (rare, but cheap to support)
|
|
608
|
+
xdg_env = os.getenv("XDG_DOWNLOAD_DIR")
|
|
609
|
+
if xdg_env:
|
|
610
|
+
p = Path(os.path.expandvars(xdg_env)).expanduser()
|
|
611
|
+
return p
|
|
612
|
+
|
|
613
|
+
# 2) user-dirs.dirs (common on Linux w/ xdg-user-dirs)
|
|
614
|
+
try:
|
|
615
|
+
user_dirs = Path.home() / ".config" / "user-dirs.dirs"
|
|
616
|
+
if user_dirs.exists():
|
|
617
|
+
txt = user_dirs.read_text(encoding="utf-8", errors="ignore")
|
|
618
|
+
m = re.search(r'^XDG_DOWNLOAD_DIR\s*=\s*"(.*)"\s*$', txt, flags=re.MULTILINE)
|
|
619
|
+
if m:
|
|
620
|
+
raw = m.group(1)
|
|
621
|
+
raw = raw.replace("$HOME", str(Path.home()))
|
|
622
|
+
p = Path(os.path.expandvars(raw)).expanduser()
|
|
623
|
+
return p
|
|
624
|
+
except Exception:
|
|
625
|
+
pass
|
|
626
|
+
|
|
627
|
+
# 3) Default
|
|
628
|
+
p = Path.home() / "Downloads"
|
|
629
|
+
return p
|
|
630
|
+
|
|
631
|
+
|
|
632
|
+
def _coerce_csv_cell(v: Any) -> str:
|
|
633
|
+
if v is None:
|
|
634
|
+
return ""
|
|
635
|
+
if isinstance(v, (dict, list, tuple)):
|
|
636
|
+
# stable, readable cell content
|
|
637
|
+
return json.dumps(v, ensure_ascii=False, separators=(",", ":"))
|
|
638
|
+
return str(v)
|
|
639
|
+
|
|
640
|
+
|
|
641
|
+
def _write_csv(
|
|
642
|
+
rows: List[Dict[str, Any]],
|
|
643
|
+
out_path: Path,
|
|
644
|
+
*,
|
|
645
|
+
preferred_columns: Optional[List[str]] = None,
|
|
646
|
+
) -> None:
|
|
647
|
+
if not rows:
|
|
648
|
+
raise ValueError("No rows to write.")
|
|
649
|
+
|
|
650
|
+
preferred_columns = preferred_columns or []
|
|
651
|
+
|
|
652
|
+
present = set().union(*(r.keys() for r in rows))
|
|
653
|
+
columns = [c for c in preferred_columns if c in present]
|
|
654
|
+
columns += [c for c in sorted(present) if c not in columns]
|
|
655
|
+
|
|
656
|
+
out_path.parent.mkdir(parents=True, exist_ok=True)
|
|
657
|
+
|
|
658
|
+
with out_path.open("w", newline="", encoding="utf-8") as f:
|
|
659
|
+
w = csv.DictWriter(f, fieldnames=columns, extrasaction="ignore")
|
|
660
|
+
w.writeheader()
|
|
661
|
+
for r in rows:
|
|
662
|
+
w.writerow({c: _coerce_csv_cell(r.get(c)) for c in columns})
|
|
663
|
+
|
|
664
|
+
|
|
665
|
+
def export_user_groups_csv_local() -> Optional[Path]:
|
|
666
|
+
# include_users=True so CSV truly contains "everything"
|
|
667
|
+
groups: List[Dict[str, Any]] = user_group_handler.get_all_user_groups(include_users=True) or []
|
|
668
|
+
if not groups:
|
|
669
|
+
print("No user groups found.")
|
|
670
|
+
return None
|
|
671
|
+
|
|
672
|
+
ts = datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
673
|
+
out_dir = _resolve_download_dir()
|
|
674
|
+
out_path = out_dir / f"remoterf_user_groups_{ts}.csv"
|
|
675
|
+
|
|
676
|
+
preferred = [
|
|
677
|
+
"group_id", "id",
|
|
678
|
+
"group_name", "name",
|
|
679
|
+
"devices_whitelist",
|
|
680
|
+
"max_reservation_time_sec",
|
|
681
|
+
"max_reservations",
|
|
682
|
+
"lifetime_sec",
|
|
683
|
+
"created_at",
|
|
684
|
+
"users",
|
|
685
|
+
]
|
|
686
|
+
|
|
687
|
+
try:
|
|
688
|
+
_write_csv(groups, out_path, preferred_columns=preferred)
|
|
689
|
+
print(f"Exported user groups CSV -> {out_path}")
|
|
690
|
+
return out_path
|
|
691
|
+
except Exception as e:
|
|
692
|
+
print(f"Failed to export user groups CSV: {e}")
|
|
693
|
+
return None
|
|
694
|
+
|
|
695
|
+
|
|
696
|
+
def export_enrollment_codes_csv_local() -> Optional[Path]:
|
|
697
|
+
rows: List[Dict[str, Any]] = user_group_handler.get_all_enrollment_codes() or []
|
|
698
|
+
if not rows:
|
|
699
|
+
print("No enrollment codes found.")
|
|
700
|
+
return None
|
|
701
|
+
|
|
702
|
+
ts = datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
703
|
+
out_dir = _resolve_download_dir()
|
|
704
|
+
out_path = out_dir / f"remoterf_enrollment_codes_{ts}.csv"
|
|
705
|
+
|
|
706
|
+
preferred = [
|
|
707
|
+
"code_name",
|
|
708
|
+
"group_id",
|
|
709
|
+
"uses",
|
|
710
|
+
"uses_left",
|
|
711
|
+
"remaining_uses",
|
|
712
|
+
"duration_sec",
|
|
713
|
+
"expires_at",
|
|
714
|
+
"created_at",
|
|
715
|
+
]
|
|
716
|
+
|
|
717
|
+
try:
|
|
718
|
+
_write_csv(rows, out_path, preferred_columns=preferred)
|
|
719
|
+
print(f"Exported enrollment codes CSV -> {out_path}")
|
|
720
|
+
return out_path
|
|
721
|
+
except Exception as e:
|
|
722
|
+
print(f"Failed to export enrollment codes CSV: {e}")
|
|
723
|
+
return None
|