summaryrefslogtreecommitdiffstats
path: root/packages/meshbay-node/src/meshbay_node/cli/settings.py
diff options
context:
space:
mode:
authorChristophe Besson <cbesson@gmail.com>2026-09-24 18:19:13 +0200
committerChristophe Besson <cbesson@gmail.com>2026-09-24 18:19:13 +0200
commit6e9afe3e54ab6fb0162393150b7892655b4561d4 (patch)
treef1cf20d891bf0d75dd206953bcec781f47ab7b15 /packages/meshbay-node/src/meshbay_node/cli/settings.py
parenta408a25af7b91abf729ec5fac7e5506437a86049 (diff)
downloadmeshbay-6e9afe3e54ab6fb0162393150b7892655b4561d4.tar.gz
refactor(node): move the CLI out of daemon.py into cli/
Each `if args.command == ...` branch of main() becomes a function in cli/ (one module per family of verbs); the parser, the process setup and a VERBS table go to cli/parser.py and cli/dispatch.py. daemon.main runs the verb or starts the daemon. Tests patch the CLI through conftest.patch_cli; the CLI golden is unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Diffstat (limited to 'packages/meshbay-node/src/meshbay_node/cli/settings.py')
-rw-r--r--packages/meshbay-node/src/meshbay_node/cli/settings.py227
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)