diff options
Diffstat (limited to 'packages/meshbay-node/src/meshbay_node/cli/settings.py')
| -rw-r--r-- | packages/meshbay-node/src/meshbay_node/cli/settings.py | 227 |
1 files changed, 227 insertions, 0 deletions
diff --git a/packages/meshbay-node/src/meshbay_node/cli/settings.py b/packages/meshbay-node/src/meshbay_node/cli/settings.py new file mode 100644 index 0000000..94c7a1b --- /dev/null +++ b/packages/meshbay-node/src/meshbay_node/cli/settings.py @@ -0,0 +1,227 @@ +"""Node-wide settings: the denylist, STUN servers, transfer caps.""" + +import sys + +from meshbay_node.cli.api import _daemon_api, _resolve_group +from meshbay_node.config import DEFAULT_CONFIG_PATH, load_config + + +def denylist(args) -> None: + cfg = load_config(args.config or DEFAULT_CONFIG_PATH) + sub = args.subcommand or "show" + + if sub == "show": + out = _daemon_api(cfg, "/api/denylist") + total = out.get("count", 0) + if not total: + print("denylist empty — nothing is being refused") + return + for kind in ("users", "groups", "jtis"): + for entry in out.get(kind, []): + print(f" {kind[:-1]:<6} {entry}") + print(f"\n{total} entr(y/ies). These survive a restart (finding H4).") + return + + if sub == "clear": + if not args.yes: + what = args.target or "EVERY entry" + print(f"Clearing the denylist re-admits {what}.") + print("A revocation the hub sent will not come back on its own.") + if input("clear now? [y/N] ").strip().lower() not in ("y", "yes"): + print("cancelled") + return + out = _daemon_api(cfg, f"/api/denylist/clear?subject={args.target or ''}", + method="POST") + print(f"removed {out['removed']} entr(y/ies) ({out['subject']})") + return + + print("usage: meshbay-node denylist show|clear [identifier] [--yes]") + sys.exit(1) + + +def stun(args) -> None: + cfg = load_config(args.config or DEFAULT_CONFIG_PATH) + sub = args.subcommand or "list" + + if sub == "list": + out = _daemon_api(cfg, "/api/node-settings") + servers = out.get("stun_servers", []) + if not servers: + print("stun servers (none configured)") + return + for i, s in enumerate(servers, 1): + print(f" {i}. {s}") + return + + if sub == "add": + url = args.target + if not url: + print("usage: meshbay-node stun add <stun:host:port>") + sys.exit(1) + if not url.startswith("stun:"): + print(f"error: STUN URL must start with stun: — got {url!r}") + sys.exit(1) + out = _daemon_api(cfg, "/api/node-settings") + servers = out.get("stun_servers", []) + if url in servers: + print(f"already present: {url}") + return + servers.append(url) + _daemon_api(cfg, "/api/node-settings", method="PUT", + body={"stun_servers": servers}) + print(f"added {url} ({len(servers)} servers total)") + return + + if sub == "remove": + url = args.target + if not url: + print("usage: meshbay-node stun remove <stun:host:port>") + sys.exit(1) + out = _daemon_api(cfg, "/api/node-settings") + servers = out.get("stun_servers", []) + if url not in servers: + print(f"not found: {url}") + sys.exit(1) + servers.remove(url) + _daemon_api(cfg, "/api/node-settings", method="PUT", + body={"stun_servers": servers}) + print(f"removed {url} ({len(servers)} servers remaining)") + return + + if sub == "reset": + from meshbay_node.config import DEFAULT_STUN_SERVERS + _daemon_api(cfg, "/api/node-settings", method="PUT", + body={"stun_servers": list(DEFAULT_STUN_SERVERS)}) + print("STUN servers reset to defaults:") + for s in DEFAULT_STUN_SERVERS: + print(f" {s}") + return + + print("usage: meshbay-node stun list|add|remove|reset [url]") + sys.exit(1) + + +def transfers(args) -> None: + cfg = load_config(args.config or DEFAULT_CONFIG_PATH) + sub = args.subcommand or "show" + + if sub == "show": + out = _daemon_api(cfg, "/api/transfers") + for kind, pool in out.get("pools", {}).items(): + print(f" {kind:<9} {pool['in_use']}/{pool['cap']} in use, " + f"{pool['queued']} queued (node-wide)") + # From the daemon, not from node.toml: a change made on the Node + # page is live before it is written back, and the number to print + # is the one being enforced. The local file is the fallback rather + # than a literal, so there is no second copy of the default here. + settings = _daemon_api(cfg, "/api/node-settings") + gb = settings.get("max_upload_gb") or cfg.node.max_upload_gb + print(f"\n largest single upload: {gb:g} GB per file " + f"(meshbay-node transfers max-size <GB>)") + # Per group, because that is the cap that decides how many one + # person runs at once — and it is not the node-wide number. An + # operator raising `transfers set 8 8` and still seeing two at a + # time is looking at this line, which used to print the node's + # default and say nothing about where it came from. + groups = out.get("groups") or [] + if groups: + print("\n per member, per group " + "(meshbay-node transfers per-member <dl> <ul> --group X):") + for g in groups: + how = "set" if g["set"] else "default" + print(f" {g['name']:<20} {g['download']} download(s), " + f"{g['upload']} upload(s) [{how}]") + leases = out.get("leases", []) + if not leases: + print("\n nothing transferring") + return + print(f"\n {'transfer':<14}{'kind':<10}{'state':<9}" + f"{'user':<12}{'bytes':>12}") + for x in leases: + where = f" (#{x['ahead'] + 1} in queue)" if x["state"] == "queued" else "" + print(f" {x['tr']:<14}{x['kind']:<10}{x['state']:<9}" + f"{x['user_id'][:10]:<12}{x['bytes']:>12}{where}") + return + + if sub == "set": + # `transfers set 4 2` — downloads, then uploads. Node-wide; the + # per-member cap is a group's setting and is signed, so it is not + # settable from here (see `ops.set_transfer_limits`). + values = [v for v in (args.target, args.value) if v] + if len(values) != 2: + print("usage: meshbay-node transfers set <downloads> <uploads>") + sys.exit(1) + try: + downloads, uploads = int(values[0]), int(values[1]) + except ValueError: + print("error: both values must be whole numbers") + sys.exit(1) + if downloads < 1 or uploads < 1: + print("error: a cap below 1 is not 'unlimited'; it would stop " + "every transfer. Revoke the member instead.") + sys.exit(1) + out = _daemon_api(cfg, "/api/node-settings", method="PUT", + body={"max_concurrent_downloads": downloads, + "max_concurrent_uploads": uploads}) + print(f"downloads: {downloads}, uploads: {uploads} " + f"(applied now, and kept in node.toml)") + return + + if sub == "max-size": + # The largest single file a member may upload here. Not a + # concurrency cap like `set` — it is the one limit that bounds what + # a member writes to the operator's disk, which is why it lives + # beside them rather than under a verb of its own. + if not args.target: + print("usage: meshbay-node transfers max-size <GB>") + sys.exit(1) + try: + gb = float(args.target) + except ValueError: + print("error: the size must be a number of GB (e.g. 8, or 0.5)") + sys.exit(1) + if gb <= 0: + print("error: a ceiling of zero is not 'unlimited'; it would " + "refuse every upload. Make the root read-only instead.") + sys.exit(1) + _daemon_api(cfg, "/api/node-settings", method="PUT", + body={"max_upload_gb": gb}) + print(f"largest single upload: {gb:g} GB per file " + f"(applied now, and kept in node.toml)") + return + + if sub == "per-member": + # How many transfers ONE member may run at once in this group. Not + # the same knob as `set`, which is the machine's total — and the + # reason "I set 8 8 and still only get two" is the commonest + # confusion here: per-member is checked first, by design. + values = [v for v in (args.target, args.value) if v] + if len(values) != 2: + print("usage: meshbay-node transfers per-member <downloads> " + "<uploads> [--group NAME]") + sys.exit(1) + try: + downloads, uploads = int(values[0]), int(values[1]) + except ValueError: + print("error: both values must be whole numbers") + sys.exit(1) + if downloads < 1 or uploads < 1: + print("error: a cap below 1 is not 'unlimited'; it would stop " + "every transfer for that member. Revoke them instead.") + sys.exit(1) + group_id = _resolve_group(cfg, args.group) + out = _daemon_api(cfg, f"/api/groups/{group_id}/transfer-limits", + method="PUT", + body={"downloads": downloads, "uploads": uploads}) + got = out.get("limits", {}) + started = out.get("started") or [] + print(f"each member of this group may now run " + f"{got.get('download')} download(s) and " + f"{got.get('upload')} upload(s) at once") + if started: + print(f"{len(started)} waiting transfer(s) started at once") + return + + print("usage: meshbay-node transfers show|set <downloads> <uploads>|" + "max-size <GB>|per-member <downloads> <uploads> [--group NAME]") + sys.exit(1) |