aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-node/src/meshbay_node/cli/settings.py
blob: 94c7a1b7cb14d9ecb29afbe904ae87a50599142a (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
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)