RemoteRF 0.0.7.46__tar.gz → 0.0.7.48__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {remoterf-0.0.7.46/src/remoteRF.egg-info → remoterf-0.0.7.48}/PKG-INFO +2 -1
- {remoterf-0.0.7.46 → remoterf-0.0.7.48}/setup.py +18 -7
- remoterf-0.0.7.48/src/remoteRF/cli.py +141 -0
- remoterf-0.0.7.48/src/remoteRF/config/cert_fetcher.py +118 -0
- remoterf-0.0.7.48/src/remoteRF/config/config_cli.py +174 -0
- {remoterf-0.0.7.46 → remoterf-0.0.7.48}/src/remoteRF/core/acc_login.py +1 -1
- {remoterf-0.0.7.46 → remoterf-0.0.7.48}/src/remoteRF/core/app.py +34 -2
- {remoterf-0.0.7.46 → remoterf-0.0.7.48}/src/remoteRF/core/grpc_acc.py +8 -1
- remoterf-0.0.7.48/src/remoteRF/core/grpc_client.py +320 -0
- {remoterf-0.0.7.46 → remoterf-0.0.7.48}/src/remoteRF/core/version.py +1 -1
- {remoterf-0.0.7.46 → remoterf-0.0.7.48/src/remoteRF.egg-info}/PKG-INFO +2 -1
- {remoterf-0.0.7.46 → remoterf-0.0.7.48}/src/remoteRF.egg-info/SOURCES.txt +4 -7
- {remoterf-0.0.7.46 → remoterf-0.0.7.48}/src/remoteRF.egg-info/entry_points.txt +2 -0
- {remoterf-0.0.7.46 → remoterf-0.0.7.48}/src/remoteRF.egg-info/requires.txt +1 -0
- remoterf-0.0.7.46/src/remoteRF/core/certs/ca.crt +0 -32
- remoterf-0.0.7.46/src/remoteRF/core/certs/ca.key +0 -52
- remoterf-0.0.7.46/src/remoteRF/core/certs/cert.pem +0 -19
- remoterf-0.0.7.46/src/remoteRF/core/certs/key.pem +0 -28
- remoterf-0.0.7.46/src/remoteRF/core/certs/server.crt +0 -19
- remoterf-0.0.7.46/src/remoteRF/core/certs/server.key +0 -28
- remoterf-0.0.7.46/src/remoteRF/core/grpc_client.py +0 -100
- {remoterf-0.0.7.46 → remoterf-0.0.7.48}/LICENSE +0 -0
- {remoterf-0.0.7.46 → remoterf-0.0.7.48}/MANIFEST.in +0 -0
- {remoterf-0.0.7.46 → remoterf-0.0.7.48}/README.md +0 -0
- {remoterf-0.0.7.46 → remoterf-0.0.7.48}/setup.cfg +0 -0
- {remoterf-0.0.7.46 → remoterf-0.0.7.48}/src/remoteRF/__init__.py +0 -0
- {remoterf-0.0.7.46 → remoterf-0.0.7.48}/src/remoteRF/common/__init__.py +0 -0
- {remoterf-0.0.7.46 → remoterf-0.0.7.48}/src/remoteRF/common/grpc/__init__.py +0 -0
- {remoterf-0.0.7.46 → remoterf-0.0.7.48}/src/remoteRF/common/grpc/grpc_pb2.py +0 -0
- {remoterf-0.0.7.46 → remoterf-0.0.7.48}/src/remoteRF/common/grpc/grpc_pb2_grpc.py +0 -0
- {remoterf-0.0.7.46 → remoterf-0.0.7.48}/src/remoteRF/common/utils/__init__.py +0 -0
- {remoterf-0.0.7.46 → remoterf-0.0.7.48}/src/remoteRF/common/utils/ansi_codes.py +0 -0
- {remoterf-0.0.7.46 → remoterf-0.0.7.48}/src/remoteRF/common/utils/api_token.py +0 -0
- {remoterf-0.0.7.46 → remoterf-0.0.7.48}/src/remoteRF/common/utils/list_string.py +0 -0
- {remoterf-0.0.7.46 → remoterf-0.0.7.48}/src/remoteRF/common/utils/process_arg.py +0 -0
- {remoterf-0.0.7.46/src/remoteRF/core/certs → remoterf-0.0.7.48/src/remoteRF/config}/__init__.py +0 -0
- {remoterf-0.0.7.46 → remoterf-0.0.7.48}/src/remoteRF/core/__init__.py +0 -0
- {remoterf-0.0.7.46 → remoterf-0.0.7.48}/src/remoteRF/drivers/__init__.py +0 -0
- {remoterf-0.0.7.46 → remoterf-0.0.7.48}/src/remoteRF/drivers/adalm_pluto/__init__.py +0 -0
- {remoterf-0.0.7.46 → remoterf-0.0.7.48}/src/remoteRF/drivers/adalm_pluto/pluto_remote.py +0 -0
- {remoterf-0.0.7.46 → remoterf-0.0.7.48}/src/remoteRF.egg-info/dependency_links.txt +0 -0
- {remoterf-0.0.7.46 → remoterf-0.0.7.48}/src/remoteRF.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: remoterf
|
|
3
|
-
Version: 0.0.7.
|
|
3
|
+
Version: 0.0.7.48
|
|
4
4
|
Summary: A python API to remotely access signal centric hardware. Client-side only! Courtesy of Wireless Lab @ UCLA & Prof. Ian Roberts.
|
|
5
5
|
Author: Ethan Ge
|
|
6
6
|
Author-email: ethoGalaxy@gmail.com
|
|
@@ -14,6 +14,7 @@ Requires-Dist: protobuf<6.0.0,>=5.0.0
|
|
|
14
14
|
Requires-Dist: numpy
|
|
15
15
|
Requires-Dist: prompt_toolkit
|
|
16
16
|
Requires-Dist: python-dotenv
|
|
17
|
+
Requires-Dist: prompt-toolkit
|
|
17
18
|
Dynamic: author
|
|
18
19
|
Dynamic: author-email
|
|
19
20
|
Dynamic: classifier
|
|
@@ -6,7 +6,7 @@ with open("README.md", "r", encoding="utf-8") as fh:
|
|
|
6
6
|
|
|
7
7
|
setup(
|
|
8
8
|
name="remoterf",
|
|
9
|
-
version="0.0.7.
|
|
9
|
+
version="0.0.7.48",
|
|
10
10
|
author="Ethan Ge",
|
|
11
11
|
author_email="ethoGalaxy@gmail.com",
|
|
12
12
|
description="A python API to remotely access signal centric hardware. Client-side only! Courtesy of Wireless Lab @ UCLA & Prof. Ian Roberts.",
|
|
@@ -17,7 +17,7 @@ setup(
|
|
|
17
17
|
license_file='MIT',
|
|
18
18
|
include_package_data=True, # Includes files specified in MANIFEST.in
|
|
19
19
|
install_requires=[
|
|
20
|
-
"grpcio==1.71.0", "protobuf>=5.0.0,<6.0.0", "numpy", "prompt_toolkit", "python-dotenv"
|
|
20
|
+
"grpcio==1.71.0", "protobuf>=5.0.0,<6.0.0", "numpy", "prompt_toolkit", "python-dotenv", "prompt-toolkit"
|
|
21
21
|
],
|
|
22
22
|
classifiers=[
|
|
23
23
|
"Programming Language :: Python :: 3",
|
|
@@ -25,10 +25,21 @@ setup(
|
|
|
25
25
|
"Operating System :: OS Independent",
|
|
26
26
|
],
|
|
27
27
|
python_requires='>=3.10',
|
|
28
|
+
# entry_points={
|
|
29
|
+
# 'console_scripts': [
|
|
30
|
+
# 'remoterf-login=remoteRF.core.acc_login:main',
|
|
31
|
+
# 'remoterf-v=remoteRF.core.version:main',
|
|
32
|
+
# 'remoterf-config=remoteRF.config.config_cli:main',
|
|
33
|
+
# ],
|
|
34
|
+
# },
|
|
35
|
+
|
|
28
36
|
entry_points={
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
37
|
+
"console_scripts": [
|
|
38
|
+
"remoterf=remoteRF.cli:main",
|
|
39
|
+
"remoterf-login=remoteRF.core.acc_login:main",
|
|
40
|
+
"remoterf-v=remoteRF.core.version:main",
|
|
41
|
+
"remoterf-config=remoteRF.config.config_cli:main",
|
|
42
|
+
],
|
|
43
|
+
},
|
|
44
|
+
|
|
34
45
|
)
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
# src/remoteRF/cli.py
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import argparse
|
|
5
|
+
import sys
|
|
6
|
+
from typing import Optional, Sequence
|
|
7
|
+
|
|
8
|
+
from remoteRF.core.acc_login import main as login_main
|
|
9
|
+
from remoteRF.core.version import main as version_main
|
|
10
|
+
from remoteRF.config.config_cli import main as config_main
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
14
|
+
p = argparse.ArgumentParser(
|
|
15
|
+
prog="remoterf",
|
|
16
|
+
formatter_class=argparse.RawTextHelpFormatter,
|
|
17
|
+
add_help=True,
|
|
18
|
+
description=(
|
|
19
|
+
"RemoteRF unified CLI\n\n"
|
|
20
|
+
"Examples:\n"
|
|
21
|
+
" remoterf --login\n"
|
|
22
|
+
" remoterf -l\n"
|
|
23
|
+
" remoterf --config --addr 164.67.195.207:61005\n"
|
|
24
|
+
" remoterf -c -a 164.67.195.207:61005\n"
|
|
25
|
+
" remoterf --config --wipe\n"
|
|
26
|
+
" remoterf -v\n"
|
|
27
|
+
),
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
mode = p.add_mutually_exclusive_group()
|
|
31
|
+
|
|
32
|
+
mode.add_argument("-l", "--login", action="store_true", help="Login / register (interactive).")
|
|
33
|
+
mode.add_argument("-c", "--config", action="store_true", help="Configure server target / CA cert.")
|
|
34
|
+
mode.add_argument("-v", "--version", action="store_true", help="Print version and exit.")
|
|
35
|
+
|
|
36
|
+
# These are only meaningful with --config, but we accept them at top-level
|
|
37
|
+
# so your UX works as requested.
|
|
38
|
+
p.add_argument(
|
|
39
|
+
"-a", "--addr", "-addr",
|
|
40
|
+
dest="addr",
|
|
41
|
+
default=None,
|
|
42
|
+
help="Server address (host:port). Used with --config / -c.",
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
p.add_argument(
|
|
46
|
+
"-w", "--wipe",
|
|
47
|
+
action="store_true",
|
|
48
|
+
help="Wipe local config (~/.config/remoterf). Used with --config / -c.",
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
p.add_argument(
|
|
52
|
+
"--yes",
|
|
53
|
+
action="store_true",
|
|
54
|
+
help="Skip confirmation prompts (use with --config --wipe).",
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
return p
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def main(argv: Optional[Sequence[str]] = None) -> int:
|
|
61
|
+
argv = list(sys.argv[1:] if argv is None else argv)
|
|
62
|
+
|
|
63
|
+
parser = build_parser()
|
|
64
|
+
|
|
65
|
+
# No args => help panel
|
|
66
|
+
if len(argv) == 0:
|
|
67
|
+
parser.print_help()
|
|
68
|
+
return 0
|
|
69
|
+
|
|
70
|
+
args, extra = parser.parse_known_args(argv)
|
|
71
|
+
|
|
72
|
+
# --- Version ---
|
|
73
|
+
if args.version:
|
|
74
|
+
if extra:
|
|
75
|
+
parser.error(f"Unexpected arguments for --version: {' '.join(extra)}")
|
|
76
|
+
# If your version_main supports argv, pass [].
|
|
77
|
+
try:
|
|
78
|
+
version_main([])
|
|
79
|
+
except TypeError:
|
|
80
|
+
version_main()
|
|
81
|
+
return 0
|
|
82
|
+
|
|
83
|
+
# --- Login ---
|
|
84
|
+
if args.login:
|
|
85
|
+
if extra:
|
|
86
|
+
parser.error(f"Unexpected arguments for --login: {' '.join(extra)}")
|
|
87
|
+
try:
|
|
88
|
+
login_main([])
|
|
89
|
+
except TypeError:
|
|
90
|
+
login_main()
|
|
91
|
+
return 0
|
|
92
|
+
|
|
93
|
+
# --- Config ---
|
|
94
|
+
if args.config:
|
|
95
|
+
cfg_argv: list[str] = []
|
|
96
|
+
|
|
97
|
+
# Normalize to whatever config_cli expects.
|
|
98
|
+
# If your config_cli supports --wipe/--yes, pass them through.
|
|
99
|
+
if args.wipe:
|
|
100
|
+
cfg_argv.append("--wipe")
|
|
101
|
+
if args.yes:
|
|
102
|
+
cfg_argv.append("--yes")
|
|
103
|
+
|
|
104
|
+
# Address handling
|
|
105
|
+
if args.addr:
|
|
106
|
+
# If config_cli expects a positional addr, pass it positionally:
|
|
107
|
+
cfg_argv.append(args.addr)
|
|
108
|
+
# If your config_cli instead expects --addr, switch to:
|
|
109
|
+
# cfg_argv += ["--addr", args.addr]
|
|
110
|
+
|
|
111
|
+
# Preserve any extra args for config_cli (if you want)
|
|
112
|
+
cfg_argv += extra
|
|
113
|
+
|
|
114
|
+
# If neither addr nor wipe was supplied, show config help-like message.
|
|
115
|
+
if not args.addr and not args.wipe and len(extra) == 0:
|
|
116
|
+
# Call config_cli with --help if it supports it; otherwise print ours.
|
|
117
|
+
try:
|
|
118
|
+
config_main(["--help"])
|
|
119
|
+
except Exception:
|
|
120
|
+
parser.error("Config mode requires --addr <host:port> or --wipe.")
|
|
121
|
+
return 0
|
|
122
|
+
|
|
123
|
+
try:
|
|
124
|
+
config_main(cfg_argv)
|
|
125
|
+
except TypeError:
|
|
126
|
+
# Fallback if config_main does not accept argv; last resort:
|
|
127
|
+
old = sys.argv[:]
|
|
128
|
+
try:
|
|
129
|
+
sys.argv = ["remoterf-config", *cfg_argv]
|
|
130
|
+
config_main()
|
|
131
|
+
finally:
|
|
132
|
+
sys.argv = old
|
|
133
|
+
return 0
|
|
134
|
+
|
|
135
|
+
# If user passed only -a/-w without -c/--config, show help
|
|
136
|
+
parser.print_help()
|
|
137
|
+
return 0
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
if __name__ == "__main__":
|
|
141
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# cert_fetcher.py
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import hashlib
|
|
6
|
+
import os
|
|
7
|
+
import socket
|
|
8
|
+
import urllib.request
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Optional
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
CERT_FILENAME_DEFAULT = "ca.crt"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _default_config_dir() -> Path:
|
|
17
|
+
# Cross-platform-ish default; adjust if you already have a standard in your project.
|
|
18
|
+
# Linux: ~/.config/remoterf
|
|
19
|
+
# macOS: also acceptable; if you want Apple-standard, use ~/Library/Application Support/remoterf
|
|
20
|
+
base = Path(os.path.expanduser("~")) / ".config" / "remoterf"
|
|
21
|
+
return base
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _ensure_parent_dir(p: Path) -> None:
|
|
25
|
+
p.parent.mkdir(parents=True, exist_ok=True)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _looks_like_pem_cert(data: bytes) -> bool:
|
|
29
|
+
return b"BEGIN CERTIFICATE" in data and b"END CERTIFICATE" in data
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def sha256_fingerprint_pem(pem_bytes: bytes) -> str:
|
|
33
|
+
h = hashlib.sha256(pem_bytes).hexdigest()
|
|
34
|
+
return ":".join(h[i:i+2] for i in range(0, len(h), 2))
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _fetch_http(host: str, port: int, timeout_sec: float) -> bytes:
|
|
38
|
+
url = f"http://{host}:{port}/ca.crt"
|
|
39
|
+
req = urllib.request.Request(url, method="GET")
|
|
40
|
+
with urllib.request.urlopen(req, timeout=timeout_sec) as resp:
|
|
41
|
+
data = resp.read()
|
|
42
|
+
return data
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _fetch_raw_tcp(host: str, port: int, timeout_sec: float) -> bytes:
|
|
46
|
+
chunks: list[bytes] = []
|
|
47
|
+
with socket.create_connection((host, port), timeout=timeout_sec) as s:
|
|
48
|
+
s.settimeout(timeout_sec)
|
|
49
|
+
while True:
|
|
50
|
+
try:
|
|
51
|
+
b = s.recv(4096)
|
|
52
|
+
except socket.timeout:
|
|
53
|
+
break
|
|
54
|
+
if not b:
|
|
55
|
+
break
|
|
56
|
+
chunks.append(b)
|
|
57
|
+
return b"".join(chunks)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def fetch_and_save_ca_cert(
|
|
61
|
+
host: str,
|
|
62
|
+
port: int,
|
|
63
|
+
*,
|
|
64
|
+
out_path: Optional[str | Path] = None,
|
|
65
|
+
profile: Optional[str] = None,
|
|
66
|
+
timeout_sec: float = 3.0,
|
|
67
|
+
overwrite: bool = True,
|
|
68
|
+
) -> bool:
|
|
69
|
+
"""
|
|
70
|
+
Fetch CA cert from the server bootstrap endpoint and save it to disk.
|
|
71
|
+
|
|
72
|
+
Args:
|
|
73
|
+
host: server host/ip running cert_provider
|
|
74
|
+
port: cert_provider port (NOT the TLS gRPC port)
|
|
75
|
+
out_path: explicit output path (overrides profile/default location)
|
|
76
|
+
profile: if provided, saves as ~/.config/remoterf/certs/<profile>.crt
|
|
77
|
+
timeout_sec: network timeout
|
|
78
|
+
overwrite: whether to overwrite existing file
|
|
79
|
+
|
|
80
|
+
Returns:
|
|
81
|
+
True on success, False on any failure.
|
|
82
|
+
"""
|
|
83
|
+
try:
|
|
84
|
+
if not isinstance(port, int):
|
|
85
|
+
port = int(port)
|
|
86
|
+
|
|
87
|
+
# Determine destination path
|
|
88
|
+
if out_path is not None:
|
|
89
|
+
dest = Path(out_path).expanduser().resolve()
|
|
90
|
+
else:
|
|
91
|
+
cfg = _default_config_dir()
|
|
92
|
+
certs_dir = cfg / "certs"
|
|
93
|
+
name = f"{profile}.crt" if profile else CERT_FILENAME_DEFAULT
|
|
94
|
+
dest = certs_dir / name
|
|
95
|
+
|
|
96
|
+
_ensure_parent_dir(dest)
|
|
97
|
+
|
|
98
|
+
if dest.exists() and not overwrite:
|
|
99
|
+
return True # already present, treat as success
|
|
100
|
+
|
|
101
|
+
# Fetch (HTTP first, then raw TCP fallback)
|
|
102
|
+
data = b""
|
|
103
|
+
try:
|
|
104
|
+
data = _fetch_http(host, port, timeout_sec)
|
|
105
|
+
except Exception:
|
|
106
|
+
data = _fetch_raw_tcp(host, port, timeout_sec)
|
|
107
|
+
|
|
108
|
+
if not data or not _looks_like_pem_cert(data):
|
|
109
|
+
return False
|
|
110
|
+
|
|
111
|
+
# Save
|
|
112
|
+
dest.write_bytes(data)
|
|
113
|
+
|
|
114
|
+
# Optional: you may want to return/print fingerprint, but requested API is bool.
|
|
115
|
+
return True
|
|
116
|
+
|
|
117
|
+
except Exception:
|
|
118
|
+
return False
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
# src/remoteRF/core/remoterf_config.py
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import sys
|
|
7
|
+
import shutil
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Tuple
|
|
10
|
+
|
|
11
|
+
from .cert_fetcher import fetch_and_save_ca_cert
|
|
12
|
+
|
|
13
|
+
# -----------------------------
|
|
14
|
+
# Local config locations
|
|
15
|
+
# -----------------------------
|
|
16
|
+
def _config_root() -> Path:
|
|
17
|
+
return Path(os.path.expanduser("~")) / ".config" / "remoterf"
|
|
18
|
+
|
|
19
|
+
def _env_path() -> Path:
|
|
20
|
+
return _config_root() / ".env"
|
|
21
|
+
|
|
22
|
+
def _certs_dir() -> Path:
|
|
23
|
+
return _config_root() / "certs"
|
|
24
|
+
|
|
25
|
+
def _parse_hostport(s: str) -> Tuple[str, int]:
|
|
26
|
+
s = s.strip()
|
|
27
|
+
if "://" in s:
|
|
28
|
+
s = s.split("://", 1)[1]
|
|
29
|
+
|
|
30
|
+
if ":" not in s:
|
|
31
|
+
raise ValueError("Expected format host:port")
|
|
32
|
+
|
|
33
|
+
host, port_str = s.rsplit(":", 1)
|
|
34
|
+
host = host.strip()
|
|
35
|
+
port = int(port_str.strip())
|
|
36
|
+
if not host:
|
|
37
|
+
raise ValueError("Host is empty")
|
|
38
|
+
if port <= 0 or port > 65535:
|
|
39
|
+
raise ValueError("Port out of range")
|
|
40
|
+
return host, port
|
|
41
|
+
|
|
42
|
+
def _write_env_kv(path: Path, kv: dict[str, str]) -> None:
|
|
43
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
44
|
+
lines: list[str] = []
|
|
45
|
+
for k, v in kv.items():
|
|
46
|
+
if any(c.isspace() for c in v) or any(c in v for c in ['"', "'"]):
|
|
47
|
+
v = v.replace('"', '\\"')
|
|
48
|
+
lines.append(f'{k}="{v}"')
|
|
49
|
+
else:
|
|
50
|
+
lines.append(f"{k}={v}")
|
|
51
|
+
path.write_text("\n".join(lines) + "\n", encoding="utf-8")
|
|
52
|
+
|
|
53
|
+
def _confirm_wipe(root: Path) -> bool:
|
|
54
|
+
prompt = (
|
|
55
|
+
f"This will permanently delete ALL RemoteRF config at:\n"
|
|
56
|
+
f" {root}\n\n"
|
|
57
|
+
f"Type 'wipe' to confirm: "
|
|
58
|
+
)
|
|
59
|
+
try:
|
|
60
|
+
return input(prompt).strip().lower() == "wipe"
|
|
61
|
+
except KeyboardInterrupt:
|
|
62
|
+
print("\nCancelled.")
|
|
63
|
+
return False
|
|
64
|
+
|
|
65
|
+
def _wipe_config(root: Path) -> None:
|
|
66
|
+
if not root.exists():
|
|
67
|
+
print(f"No config found at: {root}")
|
|
68
|
+
return
|
|
69
|
+
if not root.is_dir():
|
|
70
|
+
raise RuntimeError(f"Config root exists but is not a directory: {root}")
|
|
71
|
+
shutil.rmtree(root)
|
|
72
|
+
print(f"Wiped RemoteRF config: {root}")
|
|
73
|
+
|
|
74
|
+
def main(argv=None) -> None:
|
|
75
|
+
import argparse
|
|
76
|
+
|
|
77
|
+
parser = argparse.ArgumentParser(
|
|
78
|
+
prog="remoterf-config",
|
|
79
|
+
add_help=True,
|
|
80
|
+
formatter_class=argparse.RawTextHelpFormatter,
|
|
81
|
+
description=(
|
|
82
|
+
"Configure RemoteRF to talk to a server.\n"
|
|
83
|
+
"Fetches the server CA certificate and saves the gRPC target locally."
|
|
84
|
+
),
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
parser.add_argument(
|
|
88
|
+
"addr",
|
|
89
|
+
nargs="?",
|
|
90
|
+
help="Server address in host:port form. Example: 164.67.195.207:61005",
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
parser.add_argument(
|
|
94
|
+
"--wipe",
|
|
95
|
+
action="store_true",
|
|
96
|
+
help="Delete ALL existing RemoteRF config at ~/.config/remoterf and exit.",
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
parser.add_argument(
|
|
100
|
+
"--yes",
|
|
101
|
+
action="store_true",
|
|
102
|
+
help="Skip confirmation prompts (use with --wipe).",
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
args = parser.parse_args()
|
|
106
|
+
|
|
107
|
+
# ----- Wipe mode -----
|
|
108
|
+
if args.wipe:
|
|
109
|
+
root = _config_root()
|
|
110
|
+
if not args.yes:
|
|
111
|
+
if not _confirm_wipe(root):
|
|
112
|
+
print("Wipe aborted.")
|
|
113
|
+
sys.exit(1)
|
|
114
|
+
try:
|
|
115
|
+
_wipe_config(root)
|
|
116
|
+
sys.exit(0)
|
|
117
|
+
except Exception as e:
|
|
118
|
+
print(f"Error wiping config: {e}", file=sys.stderr)
|
|
119
|
+
sys.exit(1)
|
|
120
|
+
|
|
121
|
+
# ----- Normal configure mode -----
|
|
122
|
+
if not args.addr:
|
|
123
|
+
print(
|
|
124
|
+
"\nError: missing required argument: host:port\n\n"
|
|
125
|
+
"Usage:\n"
|
|
126
|
+
" remoterf-config <host:port>\n"
|
|
127
|
+
" remoterf-config --wipe [--yes]\n\n"
|
|
128
|
+
"Example:\n"
|
|
129
|
+
" remoterf-config 164.67.195.207:61005\n"
|
|
130
|
+
)
|
|
131
|
+
sys.exit(2)
|
|
132
|
+
|
|
133
|
+
try:
|
|
134
|
+
host, grpc_port = _parse_hostport(args.addr)
|
|
135
|
+
except Exception as e:
|
|
136
|
+
print(f"Error: invalid addr '{args.addr}': {e}", file=sys.stderr)
|
|
137
|
+
sys.exit(2)
|
|
138
|
+
|
|
139
|
+
cert_port = grpc_port + 1
|
|
140
|
+
profile = "default"
|
|
141
|
+
timeout_sec = 3.0
|
|
142
|
+
overwrite = True
|
|
143
|
+
|
|
144
|
+
certs_dir = _certs_dir()
|
|
145
|
+
certs_dir.mkdir(parents=True, exist_ok=True)
|
|
146
|
+
ca_out = certs_dir / f"{profile}.crt"
|
|
147
|
+
|
|
148
|
+
fetched_ok = fetch_and_save_ca_cert(
|
|
149
|
+
host,
|
|
150
|
+
cert_port,
|
|
151
|
+
out_path=ca_out,
|
|
152
|
+
timeout_sec=timeout_sec,
|
|
153
|
+
overwrite=overwrite,
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
if not fetched_ok:
|
|
157
|
+
print(f"Failed to fetch CA cert from {host}:{cert_port}.", file=sys.stderr)
|
|
158
|
+
sys.exit(1)
|
|
159
|
+
|
|
160
|
+
env_file = _env_path()
|
|
161
|
+
_write_env_kv(env_file, {
|
|
162
|
+
"REMOTERF_ADDR": f"{host}:{grpc_port}",
|
|
163
|
+
"REMOTERF_CA_CERT": str(ca_out),
|
|
164
|
+
"REMOTERF_PROFILE": profile,
|
|
165
|
+
})
|
|
166
|
+
|
|
167
|
+
print("RemoteRF configured successfully.")
|
|
168
|
+
print(f" gRPC target : {host}:{grpc_port}")
|
|
169
|
+
print(f" cert port : {host}:{cert_port}")
|
|
170
|
+
print(f" CA cert : {ca_out}")
|
|
171
|
+
print(f" env file : {env_file}")
|
|
172
|
+
|
|
173
|
+
if __name__ == "__main__":
|
|
174
|
+
main()
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
from remoteRF.core.grpc_client import handle_admin_command
|
|
1
2
|
from . import *
|
|
2
3
|
from ..common.utils import *
|
|
3
4
|
|
|
@@ -30,6 +31,7 @@ def welcome():
|
|
|
30
31
|
|
|
31
32
|
account.password = password
|
|
32
33
|
account.email = input("Email: ") # TODO: Email verification.
|
|
34
|
+
account.enrollment_code = input("Enrollment Code: ")
|
|
33
35
|
# check if login was valid
|
|
34
36
|
os.system('cls' if os.name == 'nt' else 'clear')
|
|
35
37
|
|
|
@@ -59,6 +61,7 @@ def commands():
|
|
|
59
61
|
printf("'getdev' ", Sty.MAGENTA, " : View devices", Sty.DEFAULT)
|
|
60
62
|
printf("'help' or 'h' ", Sty.MAGENTA, " : Show this help message", Sty.DEFAULT)
|
|
61
63
|
printf("'perms' ", Sty.MAGENTA, " : View permissions", Sty.DEFAULT)
|
|
64
|
+
printf("'enroll' ", Sty.MAGENTA, " : Enroll with an enrollment code", Sty.DEFAULT)
|
|
62
65
|
printf("'exit' or 'quit' ", Sty.MAGENTA, ": Exit", Sty.DEFAULT)
|
|
63
66
|
printf("'getres' ", Sty.MAGENTA, " : View all reservations", Sty.DEFAULT)
|
|
64
67
|
printf("'myres' ", Sty.MAGENTA, " : View my reservations", Sty.DEFAULT)
|
|
@@ -66,8 +69,20 @@ def commands():
|
|
|
66
69
|
printf("'resdev' ", Sty.MAGENTA, " : Reserve a device", Sty.DEFAULT)
|
|
67
70
|
# printf("'resdev -n' ", Sty.MAGENTA, "- naive reserve device", Sty.DEFAULT)
|
|
68
71
|
# printf("'resdev s' ", Sty.MAGENTA, "- Reserve a Device (by single date)", Sty.DEFAULT)
|
|
69
|
-
|
|
70
|
-
|
|
72
|
+
|
|
73
|
+
if account.is_admin:
|
|
74
|
+
print()
|
|
75
|
+
printf("Admin Commands:", Sty.BOLD)
|
|
76
|
+
printf("'admin printa' ", Sty.MAGENTA, " : Print all accounts", Sty.DEFAULT)
|
|
77
|
+
printf("'admin printr' ", Sty.MAGENTA, " : Print all reservations", Sty.DEFAULT)
|
|
78
|
+
printf("'admin printp' ", Sty.MAGENTA, " : Print all perms", Sty.DEFAULT)
|
|
79
|
+
printf("'admin printd' ", Sty.MAGENTA, " : Print all devices", Sty.DEFAULT)
|
|
80
|
+
printf("'admin rm a <username>' ", Sty.MAGENTA, " : Remove one account", Sty.DEFAULT)
|
|
81
|
+
printf("'admin rm aa' ", Sty.MAGENTA, " : Remove all accounts", Sty.DEFAULT)
|
|
82
|
+
printf("'admin rm ar' ", Sty.MAGENTA, " : Remove all reservations", Sty.DEFAULT)
|
|
83
|
+
# If you expose set_account remotely:
|
|
84
|
+
printf("'admin setacc <username> <U|P|A> [args...]' ", Sty.MAGENTA, " : Set perms", Sty.DEFAULT)
|
|
85
|
+
|
|
71
86
|
|
|
72
87
|
def clear():
|
|
73
88
|
os.system('cls' if os.name == 'nt' else 'clear')
|
|
@@ -267,6 +282,18 @@ def perms():
|
|
|
267
282
|
printf(f'No restrictions on reservation count or duration.', Sty.DEFAULT)
|
|
268
283
|
else:
|
|
269
284
|
printf(f"Error: Unknown permission level {results[0]}", Sty.BRIGHT_RED)
|
|
285
|
+
|
|
286
|
+
def enroll():
|
|
287
|
+
code = session.prompt(stylize("Enter your enrollment code: ", Sty.DEFAULT))
|
|
288
|
+
account.enrollment_code = code
|
|
289
|
+
data = account.set_enroll()
|
|
290
|
+
|
|
291
|
+
if 'ace' in data.results:
|
|
292
|
+
print(f"Error: {unmap_arg(data.results['ace'])}")
|
|
293
|
+
return
|
|
294
|
+
else:
|
|
295
|
+
# todo: print out specifics, like group name (ie: successfully enrolled in ECE 132A)
|
|
296
|
+
printf("Enrollment successful.", Sty.BOLD_GREEN)
|
|
270
297
|
|
|
271
298
|
# New block scheduling
|
|
272
299
|
|
|
@@ -484,6 +511,8 @@ while True:
|
|
|
484
511
|
commands()
|
|
485
512
|
elif inpu == "perms":
|
|
486
513
|
perms()
|
|
514
|
+
elif inpu == "enroll":
|
|
515
|
+
enroll()
|
|
487
516
|
elif inpu == "quit" or inpu == "exit":
|
|
488
517
|
break
|
|
489
518
|
elif inpu == "getres":
|
|
@@ -496,10 +525,13 @@ while True:
|
|
|
496
525
|
interactive_reserve_next_days(block_minutes=30)
|
|
497
526
|
elif inpu == 'cancelres':
|
|
498
527
|
cancel_my_reservation()
|
|
528
|
+
|
|
499
529
|
elif inpu == 'resdev -n':
|
|
500
530
|
# check if user is admin
|
|
501
531
|
# if account.get_perms().results['UC'] == 'Admin':
|
|
502
532
|
reserve()
|
|
533
|
+
elif account.is_admin and inpu.strip().startswith("admin"):
|
|
534
|
+
handle_admin_command(inpu)
|
|
503
535
|
else:
|
|
504
536
|
print(f"Unknown command: {inpu}")
|
|
505
537
|
except KeyboardInterrupt:
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import ast
|
|
1
2
|
from .grpc_client import rpc_client
|
|
2
3
|
from ..common.utils import *
|
|
3
4
|
|
|
@@ -8,9 +9,10 @@ class RemoteRFAccount:
|
|
|
8
9
|
self.username = username
|
|
9
10
|
self.password = password
|
|
10
11
|
self.email = email
|
|
12
|
+
self.enrollment_code = ""
|
|
11
13
|
|
|
12
14
|
def create_user(self):
|
|
13
|
-
response = rpc_client(function_name="ACC:create_user", args={"un":map_arg(self.username), "pw":map_arg(self.password), "em":map_arg(self.email)})
|
|
15
|
+
response = rpc_client(function_name="ACC:create_user", args={"un":map_arg(self.username), "pw":map_arg(self.password), "em":map_arg(self.email), "ec":map_arg(self.enrollment_code)})
|
|
14
16
|
if 'UC' in response.results:
|
|
15
17
|
print(f'User {unmap_arg(response.results["UC"])} successfully created.')
|
|
16
18
|
return True
|
|
@@ -24,6 +26,8 @@ class RemoteRFAccount:
|
|
|
24
26
|
response = rpc_client(function_name="ACC:login", args={"un":map_arg(username), "pw":map_arg(password)})
|
|
25
27
|
if 'UC' in response.results:
|
|
26
28
|
print(f'User {unmap_arg(response.results["UC"])} successful login.')
|
|
29
|
+
|
|
30
|
+
self.is_admin = (ast.literal_eval(unmap_arg(self.get_perms().results['UC']))[0][0] == 'Admin')
|
|
27
31
|
return True
|
|
28
32
|
elif 'UE' in response.results:
|
|
29
33
|
print(f'Error: {unmap_arg(response.results["UE"])}')
|
|
@@ -49,4 +53,7 @@ class RemoteRFAccount:
|
|
|
49
53
|
def get_perms(self):
|
|
50
54
|
return rpc_client(function_name='ACC:get_perms', args={"un":map_arg(self.username), "pw":map_arg(self.password)})
|
|
51
55
|
|
|
56
|
+
def set_enroll(self):
|
|
57
|
+
return rpc_client(function_name='ACC:set_enroll', args={"un":map_arg(self.username), "pw":map_arg(self.password), "ec":map_arg(self.enrollment_code)})
|
|
58
|
+
|
|
52
59
|
|