fleetmesh-node 0.2.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.
- fleetmesh_node-0.2.0.dist-info/METADATA +57 -0
- fleetmesh_node-0.2.0.dist-info/RECORD +338 -0
- fleetmesh_node-0.2.0.dist-info/WHEEL +5 -0
- fleetmesh_node-0.2.0.dist-info/entry_points.txt +2 -0
- fleetmesh_node-0.2.0.dist-info/licenses/LICENSE +121 -0
- fleetmesh_node-0.2.0.dist-info/top_level.txt +2 -0
- node/RELEASE +1 -0
- node/__init__.py +35 -0
- node/app/__init__.py +49 -0
- node/app/commands.py +265 -0
- node/app/config.py +131 -0
- node/app/control.py +670 -0
- node/app/core.py +867 -0
- node/app/plugins.py +76 -0
- node/app/router.py +83 -0
- node/app/socket.py +431 -0
- node/app/systemd.py +36 -0
- node/app/views.py +204 -0
- node/channels/__init__.py +378 -0
- node/channels/bus.py +331 -0
- node/channels/journal.py +46 -0
- node/commands/__init__.py +140 -0
- node/commands/adapter.py +210 -0
- node/commands/base.py +278 -0
- node/commands/cluster_cmds.py +183 -0
- node/commands/compute_cmds.py +401 -0
- node/commands/core.py +474 -0
- node/commands/gossip_cmds.py +106 -0
- node/commands/nsite_cmds.py +141 -0
- node/commands/peering.py +402 -0
- node/commands/plugin_cmds.py +135 -0
- node/commands/sidecar_cmds.py +120 -0
- node/commands/sync.py +388 -0
- node/commands/transport_cmds.py +451 -0
- node/commands/video_cmds.py +99 -0
- node/compartment/__init__.py +124 -0
- node/compartment/base.py +868 -0
- node/compartment/command.py +458 -0
- node/compartment/context.py +163 -0
- node/compartment/every.py +108 -0
- node/compartment/handler.py +131 -0
- node/compartment/lifecycle.py +460 -0
- node/compartment/lint.py +388 -0
- node/compartment/need.py +158 -0
- node/compartment/problem.py +521 -0
- node/compartment/refusals.py +120 -0
- node/compartment/registry.py +790 -0
- node/compartment/schema.py +290 -0
- node/compartment/sidecar.py +499 -0
- node/compartment/stores.py +662 -0
- node/compartment/wire.py +225 -0
- node/compartment/worker.py +61 -0
- node/config/__init__.py +43 -0
- node/config/base.py +116 -0
- node/config/change.py +441 -0
- node/config/changestore.py +201 -0
- node/config/compartments.py +1144 -0
- node/config/node.py +318 -0
- node/config/parameter.py +563 -0
- node/config/protocols.py +274 -0
- node/config/records.py +171 -0
- node/examples/__init__.py +1 -0
- node/examples/demo.py +226 -0
- node/examples/follower.py +46 -0
- node/interfaces.py +235 -0
- node/keystore/__init__.py +27 -0
- node/keystore/core.py +103 -0
- node/keystore/enclave.py +688 -0
- node/meshnode.py +111 -0
- node/metrics/__init__.py +396 -0
- node/metrics/adapters.py +102 -0
- node/metrics/face.py +110 -0
- node/planes/__init__.py +21 -0
- node/planes/identity/__init__.py +20 -0
- node/planes/identity/announce/__init__.py +9 -0
- node/planes/identity/announce/compartment.py +447 -0
- node/planes/identity/announce/types.py +26 -0
- node/planes/identity/discovery/__init__.py +9 -0
- node/planes/identity/discovery/compartment.py +522 -0
- node/planes/identity/discovery/types.py +13 -0
- node/planes/identity/linked/__init__.py +1 -0
- node/planes/identity/linked/compartment.py +270 -0
- node/planes/identity/linked/types.py +25 -0
- node/planes/identity/presence/__init__.py +7 -0
- node/planes/identity/presence/compartment.py +409 -0
- node/planes/identity/presence/types.py +14 -0
- node/planes/operations/__init__.py +13 -0
- node/planes/operations/scheduler/__init__.py +9 -0
- node/planes/operations/scheduler/compartment.py +353 -0
- node/planes/operations/scheduler/types.py +21 -0
- node/planes/operations/updates/__init__.py +17 -0
- node/planes/operations/updates/core.py +310 -0
- node/planes/operations/updates/store.py +79 -0
- node/planes/operations/updates/types.py +20 -0
- node/planes/peering/__init__.py +28 -0
- node/planes/peering/budget/__init__.py +43 -0
- node/planes/peering/budget/chain.py +43 -0
- node/planes/peering/budget/core.py +537 -0
- node/planes/peering/budget/evaluation.py +80 -0
- node/planes/peering/budget/meter.py +100 -0
- node/planes/peering/budget/receipts.py +439 -0
- node/planes/peering/budget/shape.py +101 -0
- node/planes/peering/budget/tick.py +110 -0
- node/planes/peering/budget/types.py +67 -0
- node/planes/peering/budget/windows.py +346 -0
- node/planes/peering/capacity/__init__.py +7 -0
- node/planes/peering/capacity/compartment.py +342 -0
- node/planes/peering/capacity/types.py +24 -0
- node/planes/peering/peering/__init__.py +25 -0
- node/planes/peering/peering/core.py +464 -0
- node/planes/peering/peering/envelope.py +142 -0
- node/planes/peering/peering/handshake.py +198 -0
- node/planes/peering/peering/shape.py +80 -0
- node/planes/peering/peering/types.py +39 -0
- node/planes/peering/reporter/__init__.py +42 -0
- node/planes/peering/reporter/constants.py +29 -0
- node/planes/peering/reporter/core.py +299 -0
- node/planes/peering/reporter/finding.py +74 -0
- node/planes/peering/reporter/findings.py +171 -0
- node/planes/peering/reporter/publish.py +285 -0
- node/planes/peering/reporter/reading.py +193 -0
- node/planes/peering/reporter/shape.py +86 -0
- node/planes/peering/reporter/types.py +50 -0
- node/planes/peering/strain/__init__.py +9 -0
- node/planes/peering/strain/compartment.py +204 -0
- node/planes/peering/strain/types.py +21 -0
- node/planes/peering/trust/__init__.py +9 -0
- node/planes/peering/trust/compartment.py +239 -0
- node/planes/peering/trust/types.py +30 -0
- node/planes/services/__init__.py +33 -0
- node/planes/services/compute/__init__.py +160 -0
- node/planes/services/compute/common.py +134 -0
- node/planes/services/compute/compartment.py +681 -0
- node/planes/services/compute/executors/__init__.py +121 -0
- node/planes/services/compute/executors/agent.py +840 -0
- node/planes/services/compute/executors/bacalhau.py +605 -0
- node/planes/services/compute/executors/base.py +127 -0
- node/planes/services/compute/executors/harness.py +439 -0
- node/planes/services/compute/executors/mcp.py +454 -0
- node/planes/services/compute/executors/media.py +898 -0
- node/planes/services/compute/executors/nondet.py +283 -0
- node/planes/services/compute/executors/providers.py +499 -0
- node/planes/services/compute/executors/registry.py +140 -0
- node/planes/services/compute/executors/wasm.py +244 -0
- node/planes/services/compute/executors/zk.py +604 -0
- node/planes/services/compute/jobs.py +227 -0
- node/planes/services/compute/load.py +180 -0
- node/planes/services/compute/market.py +433 -0
- node/planes/services/compute/sandbox.py +282 -0
- node/planes/services/compute/types.py +84 -0
- node/planes/services/compute_wire/__init__.py +58 -0
- node/planes/services/compute_wire/compartment.py +256 -0
- node/planes/services/compute_wire/messages.py +320 -0
- node/planes/services/compute_wire/sealing.py +147 -0
- node/planes/services/compute_wire/session.py +572 -0
- node/planes/services/compute_wire/types.py +49 -0
- node/planes/services/compute_wire/verify.py +331 -0
- node/planes/services/dvm/__init__.py +24 -0
- node/planes/services/dvm/gateway.py +526 -0
- node/planes/services/dvm/protocol.py +35 -0
- node/planes/services/dvm/translate.py +15 -0
- node/planes/services/dvm/types.py +25 -0
- node/planes/services/gateway/__init__.py +40 -0
- node/planes/services/gateway/settle.py +279 -0
- node/planes/services/gateway/translate.py +644 -0
- node/planes/services/gossip/__init__.py +22 -0
- node/planes/services/gossip/core.py +929 -0
- node/planes/services/gossip/envelope.py +154 -0
- node/planes/services/gossip/mesh.py +127 -0
- node/planes/services/gossip/types.py +50 -0
- node/planes/services/nsite/__init__.py +7 -0
- node/planes/services/nsite/client.py +62 -0
- node/planes/services/nsite/compartment.py +752 -0
- node/planes/services/nsite/types.py +44 -0
- node/planes/services/paidfetch/__init__.py +7 -0
- node/planes/services/paidfetch/compartment.py +862 -0
- node/planes/services/paidfetch/types.py +99 -0
- node/planes/services/settle/__init__.py +27 -0
- node/planes/services/settle/compartment.py +384 -0
- node/planes/services/settle/engine.py +456 -0
- node/planes/services/settle/gateways.py +541 -0
- node/planes/services/settle/payment.py +512 -0
- node/planes/services/settle/store.py +74 -0
- node/planes/services/settle/types.py +58 -0
- node/planes/services/sidecars/__init__.py +8 -0
- node/planes/services/sidecars/compartment.py +461 -0
- node/planes/services/sidecars/manifest.py +304 -0
- node/planes/services/sidecars/manifests/10-grasp.json +37 -0
- node/planes/services/sidecars/manifests/20-ncontainer.json +31 -0
- node/planes/services/sidecars/manifests/30-magpie.json +28 -0
- node/planes/services/sidecars/types.py +43 -0
- node/planes/services/video/__init__.py +106 -0
- node/planes/services/video/barter.py +407 -0
- node/planes/services/video/cli.py +158 -0
- node/planes/services/video/compartment.py +932 -0
- node/planes/services/video/ingest.py +524 -0
- node/planes/services/video/ledger.py +705 -0
- node/planes/services/video/packager.py +847 -0
- node/planes/services/video/player.py +456 -0
- node/planes/services/video/timed_segments.py +124 -0
- node/planes/services/video/transit.py +478 -0
- node/planes/store/__init__.py +24 -0
- node/planes/store/blob_replication/__init__.py +7 -0
- node/planes/store/blob_replication/compartment.py +496 -0
- node/planes/store/blob_replication/types.py +35 -0
- node/planes/store/blob_wire/__init__.py +48 -0
- node/planes/store/blob_wire/client.py +57 -0
- node/planes/store/blob_wire/core.py +359 -0
- node/planes/store/blob_wire/intake.py +74 -0
- node/planes/store/blob_wire/messages.py +85 -0
- node/planes/store/projected/__init__.py +7 -0
- node/planes/store/projected/compartment.py +258 -0
- node/planes/store/projected/types.py +29 -0
- node/planes/store/retention/__init__.py +7 -0
- node/planes/store/retention/compartment.py +214 -0
- node/planes/store/retention/types.py +12 -0
- node/planes/store/sync/__init__.py +35 -0
- node/planes/store/sync/core.py +311 -0
- node/planes/store/sync/envelope.py +106 -0
- node/planes/store/sync/handover.py +206 -0
- node/planes/store/sync/initiator.py +128 -0
- node/planes/store/sync/responder.py +114 -0
- node/planes/store/sync/rules.py +179 -0
- node/planes/store/sync/shape.py +80 -0
- node/planes/store/sync/types.py +51 -0
- node/planes/transport/__init__.py +23 -0
- node/planes/transport/bridge/__init__.py +13 -0
- node/planes/transport/bridge/compartment.py +785 -0
- node/planes/transport/bridge/ratelimit.py +44 -0
- node/planes/transport/bridge/types.py +43 -0
- node/planes/transport/lan/__init__.py +9 -0
- node/planes/transport/lan/compartment.py +446 -0
- node/planes/transport/lan/types.py +25 -0
- node/planes/transport/link/__init__.py +7 -0
- node/planes/transport/link/compartment.py +257 -0
- node/planes/transport/link/types.py +27 -0
- node/planes/transport/mediation/__init__.py +45 -0
- node/planes/transport/mediation/compartment.py +238 -0
- node/planes/transport/mediation/leaf.py +107 -0
- node/planes/transport/mediation/messages.py +302 -0
- node/planes/transport/mediation/service.py +233 -0
- node/planes/transport/mediation/store.py +37 -0
- node/planes/transport/mediation/types.py +20 -0
- node/planes/transport/p2p/__init__.py +7 -0
- node/planes/transport/p2p/compartment.py +349 -0
- node/planes/transport/p2p/types.py +22 -0
- node/protocols/__init__.py +277 -0
- node/protocols/activitypub/__init__.py +56 -0
- node/protocols/activitypub/actor.py +230 -0
- node/protocols/activitypub/federation.py +490 -0
- node/protocols/activitypub/media.py +145 -0
- node/protocols/activitypub/models.py +286 -0
- node/protocols/activitypub/package.py +271 -0
- node/protocols/activitypub/store.py +564 -0
- node/protocols/arcade/__init__.py +26 -0
- node/protocols/atproto/__init__.py +13 -0
- node/protocols/atproto/firehose.py +126 -0
- node/protocols/atproto/metering.py +165 -0
- node/protocols/atproto/package.py +155 -0
- node/protocols/atproto/repo.py +250 -0
- node/protocols/atproto/xrpc.py +285 -0
- node/protocols/base.py +109 -0
- node/protocols/circles/__init__.py +29 -0
- node/protocols/dm/__init__.py +50 -0
- node/protocols/grasp/__init__.py +22 -0
- node/protocols/isotime.py +15 -0
- node/protocols/nostr/__init__.py +51 -0
- node/protocols/nostr/blobstore.py +383 -0
- node/protocols/nostr/blossom.py +627 -0
- node/protocols/nostr/client.py +377 -0
- node/protocols/nostr/descriptor.py +211 -0
- node/protocols/nostr/eventstore.py +304 -0
- node/protocols/nostr/package.py +182 -0
- node/protocols/nostr/peers.py +109 -0
- node/protocols/nostr/relayserver.py +656 -0
- node/protocols/p2panda/__init__.py +22 -0
- node/protocols/p2panda/identity.py +19 -0
- node/protocols/p2panda/package.py +236 -0
- node/protocols/p2panda/rung.py +152 -0
- node/protocols/p2panda/store.py +273 -0
- node/protocols/rhio/__init__.py +33 -0
- node/protocols/rhio/package.py +275 -0
- node/protocols/rhio/rung.py +136 -0
- node/protocols/rhio/store.py +1423 -0
- node/protocols/ssb/muxrpc.py +256 -0
- node/protocols/ssb/package.py +267 -0
- node/protocols/textile/__init__.py +111 -0
- node/protocols/textile/transducers.py +259 -0
- node/release.py +119 -0
- node/storage/__init__.py +88 -0
- node/storage/accounts.py +305 -0
- node/storage/adapter.py +161 -0
- node/storage/base.py +385 -0
- node/storage/dialect.py +209 -0
- node/storage/grants.py +317 -0
- node/storage/locator.py +150 -0
- node/storage/postgres.py +294 -0
- node/storage/projected.py +953 -0
- node/storage/sharding.py +448 -0
- node/storage/standing.py +568 -0
- node/testnet/__init__.py +9 -0
- node/testnet/cluster.py +731 -0
- node/testnet/fips_daemon.py +230 -0
- node/transport/__init__.py +94 -0
- node/transport/const.py +110 -0
- node/transport/didcomm_inbound.py +251 -0
- node/transport/dnstxt.py +169 -0
- node/transport/fips_rung/__init__.py +67 -0
- node/transport/fips_rung/core.py +666 -0
- node/transport/fips_rung/listener.py +96 -0
- node/transport/fips_rung/types.py +12 -0
- node/transport/host.py +334 -0
- node/transport/iroh_rung/__init__.py +29 -0
- node/transport/iroh_rung/core.py +477 -0
- node/transport/iroh_rung/quic.py +942 -0
- node/transport/iroh_rung/types.py +22 -0
- node/transport/ladder.py +145 -0
- node/transport/lineproto.py +49 -0
- node/transport/mdns.py +303 -0
- node/transport/netfetch.py +283 -0
- node/transport/relay_rung/__init__.py +23 -0
- node/transport/relay_rung/cooldown.py +87 -0
- node/transport/relay_rung/core.py +138 -0
- node/transport/relay_rung/inbound.py +161 -0
- node/transport/relay_rung/ledger.py +104 -0
- node/transport/relay_rung/listen.py +121 -0
- node/transport/relay_rung/outbound.py +181 -0
- node/transport/relay_rung/shape.py +71 -0
- node/transport/relay_rung/types.py +58 -0
- node/transport/relay_rung/wire.py +63 -0
- node/transport/router.py +761 -0
- node/transport/rung.py +276 -0
- node/transport/ssb_rung.py +166 -0
- node/transport/ws.py +502 -0
- sidecar/fleetmesh_announce.py +338 -0
- sidecar/fleetmesh_p2p.py +373 -0
- sidecar/fleetmesh_rhio.py +352 -0
- sidecar/fleetmesh_ssb.py +603 -0
node/commands/core.py
ADDED
|
@@ -0,0 +1,474 @@
|
|
|
1
|
+
"""Core node lifecycle, health, identity, and operator diagnostics."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import argparse
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
import signal
|
|
8
|
+
import stat
|
|
9
|
+
import time
|
|
10
|
+
from typing import Any, MutableMapping, Optional, TypedDict
|
|
11
|
+
|
|
12
|
+
from fleetmesh_core.identity import identity
|
|
13
|
+
from node import app as app_mod, keystore, storage
|
|
14
|
+
from node.app import socket as control
|
|
15
|
+
from fleetmesh_core.wire import validate
|
|
16
|
+
from node import release as release_mod
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class WhoamiDescriptorSummary(TypedDict, total=False):
|
|
20
|
+
id: str
|
|
21
|
+
pow_bits: int
|
|
22
|
+
expires: Optional[int]
|
|
23
|
+
protocol: Optional[str]
|
|
24
|
+
implements: list[str]
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class FaceDict(TypedDict, total=False):
|
|
28
|
+
host: str
|
|
29
|
+
port: int
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class EndpointDict(TypedDict, total=False):
|
|
33
|
+
transport: str
|
|
34
|
+
address: str
|
|
35
|
+
ticket: str
|
|
36
|
+
priority: int
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
WhoamiReport = TypedDict('WhoamiReport', {
|
|
40
|
+
'did': str,
|
|
41
|
+
'pubkey': str,
|
|
42
|
+
'npub': str,
|
|
43
|
+
'agreement': str,
|
|
44
|
+
'transport': str,
|
|
45
|
+
'config_error': Optional[str],
|
|
46
|
+
'descriptor': Optional[WhoamiDescriptorSummary],
|
|
47
|
+
'running': bool,
|
|
48
|
+
'class': Optional[str],
|
|
49
|
+
'name': Optional[str],
|
|
50
|
+
'operator': Optional[str],
|
|
51
|
+
'relays': list[str],
|
|
52
|
+
'face': FaceDict,
|
|
53
|
+
'endpoints': list[EndpointDict],
|
|
54
|
+
}, total=False)
|
|
55
|
+
from .base import (
|
|
56
|
+
_ask,
|
|
57
|
+
_bech32,
|
|
58
|
+
_live,
|
|
59
|
+
_load_doc,
|
|
60
|
+
_short,
|
|
61
|
+
_status_file,
|
|
62
|
+
_table,
|
|
63
|
+
_trouble,
|
|
64
|
+
_when,
|
|
65
|
+
add_command,
|
|
66
|
+
emit,
|
|
67
|
+
fail,
|
|
68
|
+
status_health,
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def cmd_init(a: argparse.Namespace) -> int:
|
|
73
|
+
cfg: MutableMapping[str, Any]
|
|
74
|
+
if a.manifest:
|
|
75
|
+
cfg = dict(app_mod.config_from_manifests(_load_doc(a.manifest), [_load_doc(p) for p in a.peering or []]))
|
|
76
|
+
else:
|
|
77
|
+
if not a.operator:
|
|
78
|
+
fail('--operator <pubkey> is required without a manifest')
|
|
79
|
+
cfg = dict(app_mod.defaults())
|
|
80
|
+
cfg.update({'class': a.node_class, 'operator': a.operator})
|
|
81
|
+
cfg['relays'] = list(a.relay or cfg.get('relays') or [])
|
|
82
|
+
if a.endpoint:
|
|
83
|
+
cfg['endpoints'] = []
|
|
84
|
+
for e in a.endpoint:
|
|
85
|
+
scheme, _, rest = e.partition('=')
|
|
86
|
+
addr, _, pref = rest.rpartition(':') if rest.count(':') > 1 and rest.rsplit(':', 1)[1].isdigit() else (rest, '', '50')
|
|
87
|
+
cfg['endpoints'].append({'transport': scheme, 'address': addr, 'priority': int(pref or 50)})
|
|
88
|
+
if a.face_port is not None:
|
|
89
|
+
cfg['face'] = {'host': a.face_host, 'port': a.face_port}
|
|
90
|
+
if a.pow_bits is not None:
|
|
91
|
+
cfg['pow_bits'] = a.pow_bits
|
|
92
|
+
cfg['name'] = a.name or os.path.basename(os.path.abspath(a.dir))
|
|
93
|
+
problems = app_mod.validate_config(cfg)
|
|
94
|
+
if problems:
|
|
95
|
+
fail('refusing to write a config the node could not run on:\n ' + '\n '.join(problems))
|
|
96
|
+
had_keys = os.path.exists(os.path.join(a.dir, keystore.FILE))
|
|
97
|
+
if had_keys and os.path.exists(os.path.join(a.dir, 'config.json')):
|
|
98
|
+
# A node already. init is what a deploy runs on every pass, and a pass
|
|
99
|
+
# that changes nothing must say so rather than re-mine and republish
|
|
100
|
+
# a descriptor the relays already hold.
|
|
101
|
+
ks = keystore.Keystore.load(a.dir)
|
|
102
|
+
return emit(a, {'action': 'init', 'changed': False, 'did': ks.did, 'pubkey': ks.node_pk,
|
|
103
|
+
'config': os.path.join(a.dir, 'config.json')},
|
|
104
|
+
lambda _: print('already a node: %s (%s); config.json left as it is' % (ks.did, a.dir)))
|
|
105
|
+
app_mod.save_config(a.dir, cfg)
|
|
106
|
+
ks = keystore.Keystore.load_or_generate(a.dir)
|
|
107
|
+
told: dict[str, tuple[bool, str]] = {}
|
|
108
|
+
node = app_mod.NodeApp(a.dir, cfg)
|
|
109
|
+
node.face.start()
|
|
110
|
+
try:
|
|
111
|
+
desc = node.build_descriptor()
|
|
112
|
+
if cfg['relays']:
|
|
113
|
+
from node.protocols.nostr import client as relay
|
|
114
|
+
told = dict(relay.publish_all(cfg['relays'], desc))
|
|
115
|
+
finally:
|
|
116
|
+
node.face.stop()
|
|
117
|
+
nonce = [t for t in desc['tags'] if t[0] == 'nonce']
|
|
118
|
+
|
|
119
|
+
def human(_: Any) -> None:
|
|
120
|
+
print('keys %s' % os.path.join(a.dir, keystore.FILE))
|
|
121
|
+
print('did %s' % ks.did)
|
|
122
|
+
print('agreement %s' % ks.multikeys()['agreement'])
|
|
123
|
+
print('transport %s' % ks.multikeys()['transport'])
|
|
124
|
+
print('descriptor %s (%d tags, pow %s)' % (desc['id'], len(desc['tags']), nonce[0][2] if nonce else 'none'))
|
|
125
|
+
if told:
|
|
126
|
+
for url, (okay, note) in told.items():
|
|
127
|
+
print(' %-8s %s %s' % ('sent' if okay else 'FAILED', url, note))
|
|
128
|
+
else:
|
|
129
|
+
print(' no relays configured; the descriptor is not published yet')
|
|
130
|
+
print('config %s' % os.path.join(a.dir, 'config.json'))
|
|
131
|
+
return emit(a, {'action': 'init', 'changed': True, 'did': ks.did, 'pubkey': ks.node_pk,
|
|
132
|
+
'descriptor': desc['id'], 'pow': nonce[0][2] if nonce else None,
|
|
133
|
+
'published': {u: {'ok': okay, 'note': note} for u, (okay, note) in told.items()},
|
|
134
|
+
'config': os.path.join(a.dir, 'config.json')}, human)
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def cmd_run(a: argparse.Namespace) -> int:
|
|
138
|
+
if not os.path.isfile(os.path.join(a.dir, 'config.json')):
|
|
139
|
+
fail('%s has no config.json; run `meshnode init` first' % a.dir)
|
|
140
|
+
if control.running(a.dir):
|
|
141
|
+
try:
|
|
142
|
+
control.call(a.dir, 'health', timeout=5)
|
|
143
|
+
except control.ControlError:
|
|
144
|
+
pass # a stale socket from a node that died; the server replaces it
|
|
145
|
+
else:
|
|
146
|
+
fail('a node is already running in %s' % a.dir)
|
|
147
|
+
try:
|
|
148
|
+
node = app_mod.NodeApp(a.dir).start()
|
|
149
|
+
except (app_mod.ConfigError, PermissionError, ValueError) as e:
|
|
150
|
+
fail(str(e))
|
|
151
|
+
print('meshnode %s' % node.ks.did)
|
|
152
|
+
print(' relay face %s' % node.face.url)
|
|
153
|
+
print(' relays %s' % (', '.join(node.relays) or '(none)'))
|
|
154
|
+
print(' peers %d configured' % len(node.cfg.get('peers', [])))
|
|
155
|
+
print(' control %s' % node.control.path)
|
|
156
|
+
stop = lambda *_: node.stop()
|
|
157
|
+
signal.signal(signal.SIGTERM, stop)
|
|
158
|
+
signal.signal(signal.SIGINT, stop)
|
|
159
|
+
try:
|
|
160
|
+
node.run()
|
|
161
|
+
finally:
|
|
162
|
+
node.stop()
|
|
163
|
+
return 0
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def cmd_status(a: argparse.Namespace) -> int:
|
|
167
|
+
st = _live(a, 'status') or _status_file(a.dir)
|
|
168
|
+
if a.json:
|
|
169
|
+
return emit(a, st)
|
|
170
|
+
age = int(time.time()) - int(st.get('at', 0))
|
|
171
|
+
print('%s class %s written %ds ago%s' % (st.get('did'), st.get('class'), age,
|
|
172
|
+
' (STALE: is the node running?)' if age > 300 else ''))
|
|
173
|
+
rel = st.get('release') or {}
|
|
174
|
+
if rel:
|
|
175
|
+
print('%s %s (%s%s)' % (rel.get('name'), rel.get('version'), rel.get('source'),
|
|
176
|
+
', ' + rel['commit'][:12] if rel.get('commit') else ''))
|
|
177
|
+
print('relay face %s relays %s' % (st.get('face'), ', '.join(st.get('relays') or []) or '(none)'))
|
|
178
|
+
print('descriptor %s events stored %s' % (_short(st.get('descriptor')) + '…', st.get('events_stored')))
|
|
179
|
+
rows = [(g['peer'], g['direction'], g['tier'], g['status']) for g in st.get('grants', [])]
|
|
180
|
+
if rows:
|
|
181
|
+
print('\ngrants')
|
|
182
|
+
_table(('peer', 'direction', 'tier', 'status'), rows)
|
|
183
|
+
if st.get('pending_grants'):
|
|
184
|
+
print('\nwaiting for the boundary')
|
|
185
|
+
_table(('peer', 'tier', 'from window'), [(p['peer'], p['tier'], '%s (%s UTC)' % (p['from_epoch'], _when(p['from_epoch'])))
|
|
186
|
+
for p in st['pending_grants']])
|
|
187
|
+
ev = st.get('evaluations') or []
|
|
188
|
+
if ev:
|
|
189
|
+
print('\nlast windows priced')
|
|
190
|
+
_table(('peer', 'window', 'closed', 'severity', 'state'),
|
|
191
|
+
[(e['peer'], e['epoch'], _when(e['epoch'] + 1), e['severity'], e['state']) for e in ev[-6:]])
|
|
192
|
+
rp = st.get('repriced') or []
|
|
193
|
+
if rp:
|
|
194
|
+
# A window read again because the subject's receipt for it arrived
|
|
195
|
+
# after the grace had run. Shown because an operator who sees a
|
|
196
|
+
# severity change with no new traffic has nowhere else to look.
|
|
197
|
+
print('\nrepriced after a late receipt')
|
|
198
|
+
_table(('peer', 'window', 'was', 'now', 'remedy'),
|
|
199
|
+
[(r['peer'], r['epoch'], r['was'], r['severity'],
|
|
200
|
+
'cancelled' if r['remedy_cancelled'] else 'stands') for r in rp])
|
|
201
|
+
print('\nreports published %s findings pending %s sessions refused %s'
|
|
202
|
+
% (st.get('reports_published'), st.get('pending_findings'), st.get('refused_sessions')))
|
|
203
|
+
if st.get('last_tick'):
|
|
204
|
+
print('last tick: ' + '; '.join(' '.join(_short(x) if validate.hex64(x) else x for x in d) for d in st['last_tick']))
|
|
205
|
+
trouble = _trouble(st.get('health'))
|
|
206
|
+
print('\n' + ('\n'.join('!! ' + t for t in trouble) if trouble else 'no trouble reported'))
|
|
207
|
+
return 0
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def cmd_health(a: argparse.Namespace) -> int:
|
|
211
|
+
h = _live(a, 'health')
|
|
212
|
+
live = h is not None
|
|
213
|
+
if h is None:
|
|
214
|
+
h = status_health(a.dir)
|
|
215
|
+
if a.json:
|
|
216
|
+
return emit(a, h)
|
|
217
|
+
print('from %s' % ('the running node' if live else 'status.json (the node is not answering)'))
|
|
218
|
+
for url, r in (h.get('relays') or {}).items():
|
|
219
|
+
print('relay %s: %s, reconnects %s%s%s' % (
|
|
220
|
+
url, 'up' if r.get('connected') else 'DOWN', r.get('reconnects'),
|
|
221
|
+
(', last event %ds ago' % (int(time.time()) - r['last_event_at'])) if r.get('last_event_at') else ', no event yet',
|
|
222
|
+
(', last error: %s' % r['last_error']) if r.get('last_error') else ''))
|
|
223
|
+
print('inbox %s unfinished envelopes %s sessions open %s refused %s awaiting ack %s' % (
|
|
224
|
+
h.get('inbox'), h.get('unfinished_envelopes'), h.get('sessions_open'), h.get('sessions_refused'),
|
|
225
|
+
len(h.get('awaiting_ack') or [])))
|
|
226
|
+
if h.get('unacknowledged'):
|
|
227
|
+
# The publish said OK and the peer, who has spoken since, never said
|
|
228
|
+
# it arrived. This is the relay dropping the stored path, and it is
|
|
229
|
+
# the one fault the transport could not see until it asked.
|
|
230
|
+
print('stored messages never acknowledged by a peer that has spoken since:')
|
|
231
|
+
for u in h['unacknowledged']:
|
|
232
|
+
print(' %s %s sent %s UTC, peer heard %s UTC, via %s' % (
|
|
233
|
+
u['peer'], u['type'].rpartition('/')[2],
|
|
234
|
+
time.strftime('%m-%d %H:%M', time.gmtime(u['at'])),
|
|
235
|
+
time.strftime('%m-%d %H:%M', time.gmtime(u['heard_at'])) if u.get('heard_at') else '?',
|
|
236
|
+
', '.join(u.get('relays') or []) or 'no relay'))
|
|
237
|
+
if h.get('throttled'):
|
|
238
|
+
print('throttled, so quiet is expected until the window turns:')
|
|
239
|
+
for t in h['throttled']:
|
|
240
|
+
print(' %s %s: %s, until %s UTC' % (
|
|
241
|
+
t['peer'][:16], t.get('shard', ''), t.get('why', ''),
|
|
242
|
+
time.strftime('%H:%M', time.gmtime(t['until'])) if t.get('until') else '?'))
|
|
243
|
+
if h.get('receipts_refused'):
|
|
244
|
+
print('receipts refused because the two ends disagree about which grant is current:')
|
|
245
|
+
for r in h['receipts_refused']:
|
|
246
|
+
print(' %s window %s named %s, this node would take %s' % (
|
|
247
|
+
r['peer'], r['window'], r['named'], ' or '.join(r['acceptable']) or '(none)'))
|
|
248
|
+
for label, key in (('dropped by the transport', 'dropped'), ('rejected at the door', 'rejected'),
|
|
249
|
+
('rejected by the budget', 'budget_rejected')):
|
|
250
|
+
if h.get(key):
|
|
251
|
+
print('%s:' % label)
|
|
252
|
+
for k, v in sorted(h[key].items(), key=lambda kv: -kv[1]):
|
|
253
|
+
print(' %6d %s' % (v, k))
|
|
254
|
+
print('handler failures %s tick failures %s%s' % (
|
|
255
|
+
h.get('handler_failures'), h.get('tick_failures'),
|
|
256
|
+
(' last: %s' % h['last_tick_error']) if h.get('last_tick_error') else ''))
|
|
257
|
+
if h.get('config_error'):
|
|
258
|
+
print('config.json REJECTED, last good one in force: %s' % h['config_error'])
|
|
259
|
+
trouble = _trouble(h)
|
|
260
|
+
print('\n' + ('\n'.join('!! ' + t for t in trouble) if trouble else 'no trouble reported'))
|
|
261
|
+
return 0
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def cmd_whoami(a: argparse.Namespace) -> int:
|
|
265
|
+
try:
|
|
266
|
+
ks = keystore.Keystore.load(a.dir)
|
|
267
|
+
except (OSError, ValueError, PermissionError) as e:
|
|
268
|
+
fail('cannot read the keys: %s' % e)
|
|
269
|
+
cfg, cfg_error = None, None
|
|
270
|
+
try:
|
|
271
|
+
cfg = app_mod.load_config(a.dir)
|
|
272
|
+
except (OSError, ValueError) as e:
|
|
273
|
+
cfg_error = str(e)
|
|
274
|
+
who: WhoamiReport = {'did': ks.did, 'pubkey': ks.node_pk, 'npub': _bech32('npub', bytes.fromhex(ks.node_pk)),
|
|
275
|
+
'agreement': ks.multikeys()['agreement'], 'transport': ks.multikeys()['transport'],
|
|
276
|
+
'config_error': cfg_error, 'descriptor': None, 'running': control.running(a.dir)}
|
|
277
|
+
if cfg:
|
|
278
|
+
who.update({'class': cfg.get('class'), 'name': cfg.get('name'), 'operator': cfg.get('operator'),
|
|
279
|
+
'relays': cfg.get('relays') or [], 'face': cfg.get('face', {}), 'endpoints': cfg.get('endpoints') or []})
|
|
280
|
+
dpath = os.path.join(a.dir, app_mod.NodeApp.DESCRIPTOR_FILE)
|
|
281
|
+
if os.path.exists(dpath):
|
|
282
|
+
with open(dpath, encoding='utf-8') as _fh:
|
|
283
|
+
d = json.load(_fh)
|
|
284
|
+
desc: WhoamiDescriptorSummary = {
|
|
285
|
+
'id': d['id'], 'pow_bits': identity.pow_bits(d['id']),
|
|
286
|
+
'expires': next((int(t[1]) for t in d['tags'] if t[0] == 'expiration'), None),
|
|
287
|
+
'protocol': next((t[1] for t in d['tags'] if t[0] == 'protocol'), None),
|
|
288
|
+
'implements': [t[1] for t in d['tags'] if t[0] == 'c' and len(t) > 1]}
|
|
289
|
+
who['descriptor'] = desc
|
|
290
|
+
if a.json:
|
|
291
|
+
return emit(a, who)
|
|
292
|
+
if cfg_error:
|
|
293
|
+
print('config unreadable: %s' % cfg_error)
|
|
294
|
+
print('did %s' % who['did'])
|
|
295
|
+
print('pubkey %s' % who['pubkey'])
|
|
296
|
+
print('npub %s' % who['npub'])
|
|
297
|
+
print('agreement %s' % who['agreement'])
|
|
298
|
+
print('transport %s' % who['transport'])
|
|
299
|
+
if cfg:
|
|
300
|
+
print('class %s name %s operator %s' % (cfg.get('class'), cfg.get('name'), _short(cfg.get('operator')) + '…'))
|
|
301
|
+
print('relays %s' % (', '.join(cfg.get('relays') or []) or '(none)'))
|
|
302
|
+
print('face ws://%s:%s' % (cfg.get('face', {}).get('host'), cfg.get('face', {}).get('port')))
|
|
303
|
+
print('endpoints %s' % (', '.join('%s=%s' % (e['transport'], e['address']) for e in cfg.get('endpoints') or []) or '(none: reachable over the nostr transport only)'))
|
|
304
|
+
shown: WhoamiDescriptorSummary | None = who['descriptor']
|
|
305
|
+
if shown:
|
|
306
|
+
print('descriptor %s pow %d bits expires %s protocol %s'
|
|
307
|
+
% (shown['id'], shown['pow_bits'],
|
|
308
|
+
time.strftime('%Y-%m-%d %H:%M UTC', time.gmtime(shown['expires'])) if shown['expires'] else '?',
|
|
309
|
+
(shown['protocol'] or '')[:8] + '…'))
|
|
310
|
+
print('implements %s' % ', '.join('/'.join(c.rstrip('/').rsplit('/', 2)[-2:]) if '/' in c else c for c in shown['implements']))
|
|
311
|
+
else:
|
|
312
|
+
print('descriptor none yet (mined on the first run)')
|
|
313
|
+
print('running %s' % ('yes' if who['running'] else 'no'))
|
|
314
|
+
return 0
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
def cmd_check(a: argparse.Namespace) -> int:
|
|
318
|
+
"""Everything that would stop `run`, named at once. Exits 1 when it cannot."""
|
|
319
|
+
problems, notes = [], []
|
|
320
|
+
if not os.path.isdir(a.dir):
|
|
321
|
+
fail('%s is not a directory' % a.dir)
|
|
322
|
+
kpath = os.path.join(a.dir, keystore.FILE)
|
|
323
|
+
if not os.path.exists(kpath):
|
|
324
|
+
problems.append('no %s (run `meshnode init`)' % keystore.FILE)
|
|
325
|
+
else:
|
|
326
|
+
mode = stat.S_IMODE(os.stat(kpath).st_mode)
|
|
327
|
+
if mode & 0o077:
|
|
328
|
+
problems.append('%s is mode %o; it must be 0600' % (keystore.FILE, mode))
|
|
329
|
+
else:
|
|
330
|
+
try:
|
|
331
|
+
keystore.Keystore.load(a.dir)
|
|
332
|
+
notes.append('keys load')
|
|
333
|
+
except (ValueError, PermissionError, OSError) as e:
|
|
334
|
+
problems.append('keys do not load: %s' % e)
|
|
335
|
+
cpath = os.path.join(a.dir, 'config.json')
|
|
336
|
+
if not os.path.exists(cpath):
|
|
337
|
+
problems.append('no config.json (run `meshnode init`)')
|
|
338
|
+
else:
|
|
339
|
+
try:
|
|
340
|
+
cfg = app_mod.load_config(a.dir)
|
|
341
|
+
notes.append('config.json is valid: class %s, %d relay(s), %d peer(s), %d shard(s) claimed'
|
|
342
|
+
% (cfg['class'], len(cfg['relays']), len(cfg.get('peers', [])), len(cfg.get('coverage', []))))
|
|
343
|
+
if not cfg['relays']:
|
|
344
|
+
notes.append('no relays: the descriptor is not published and no peer can reach this node')
|
|
345
|
+
except app_mod.ConfigError as e:
|
|
346
|
+
problems.extend(str(e).replace('config.json: ', '').split('; '))
|
|
347
|
+
except ValueError as e:
|
|
348
|
+
problems.append('config.json is not JSON: %s' % e)
|
|
349
|
+
dpath = os.path.join(a.dir, app_mod.NodeApp.DESCRIPTOR_FILE)
|
|
350
|
+
if os.path.exists(dpath):
|
|
351
|
+
try:
|
|
352
|
+
with open(dpath, encoding='utf-8') as _fh:
|
|
353
|
+
d = json.load(_fh)
|
|
354
|
+
proto = next((t for t in d['tags'] if t[0] == 'protocol'), None)
|
|
355
|
+
from node.protocols.nostr import descriptor as _d
|
|
356
|
+
if proto and proto[2] != _d.DIGEST:
|
|
357
|
+
notes.append('descriptor.json declares digest %s…, this code is %s…: it will be re-mined on the next run'
|
|
358
|
+
% (proto[2][:8], _d.DIGEST[:8]))
|
|
359
|
+
else:
|
|
360
|
+
notes.append('descriptor.json matches this code\'s digest')
|
|
361
|
+
except (ValueError, KeyError, IndexError) as e:
|
|
362
|
+
problems.append('descriptor.json is unreadable: %s' % e)
|
|
363
|
+
# Soundness asked of the dialect rather than run as a PRAGMA here:
|
|
364
|
+
# `quick_check` is SQLite's, and a file-integrity check is a file-backed
|
|
365
|
+
# idea. A server backend answers for its own soundness.
|
|
366
|
+
for db in ('events.sqlite', 'grants.sqlite', 'accounts.sqlite'):
|
|
367
|
+
p = os.path.join(a.dir, db)
|
|
368
|
+
if os.path.exists(p):
|
|
369
|
+
try:
|
|
370
|
+
st = storage.SqliteStore(p, readonly=True)
|
|
371
|
+
try:
|
|
372
|
+
bad = st.integrity_check()
|
|
373
|
+
finally:
|
|
374
|
+
st.close()
|
|
375
|
+
if bad:
|
|
376
|
+
problems.append('%s: %s' % (db, bad))
|
|
377
|
+
except Exception as e:
|
|
378
|
+
problems.append('%s does not open: %s' % (db, e))
|
|
379
|
+
notes.append('a node is %srunning here' % ('' if control.running(a.dir) else 'not '))
|
|
380
|
+
if a.json:
|
|
381
|
+
emit(a, {'ok': not problems, 'problems': problems, 'notes': notes})
|
|
382
|
+
return 1 if problems else 0
|
|
383
|
+
for n in notes:
|
|
384
|
+
print(' ' + n)
|
|
385
|
+
for p in problems:
|
|
386
|
+
print('!! ' + p)
|
|
387
|
+
print('\n%s' % ('this directory can run' if not problems else '%d problem(s)' % len(problems)))
|
|
388
|
+
return 1 if problems else 0
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
def cmd_tick(a: argparse.Namespace) -> int:
|
|
392
|
+
did = _ask(a.dir, 'tick', timeout=120)
|
|
393
|
+
|
|
394
|
+
def human(did: list) -> None:
|
|
395
|
+
if not did:
|
|
396
|
+
print('nothing to do')
|
|
397
|
+
return
|
|
398
|
+
for d in did:
|
|
399
|
+
print(' ' + ' '.join(str(x)[:16] + '…' if validate.hex64(str(x)) else str(x) for x in d))
|
|
400
|
+
return emit(a, did, human)
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
def cmd_api(a: argparse.Namespace) -> int:
|
|
404
|
+
"""The control socket described by itself."""
|
|
405
|
+
if control.running(a.dir):
|
|
406
|
+
try:
|
|
407
|
+
v = control.call(a.dir, 'version', timeout=10)
|
|
408
|
+
cmds = control.call(a.dir, 'commands', timeout=10)
|
|
409
|
+
live = True
|
|
410
|
+
except control.ControlError:
|
|
411
|
+
v, cmds, live = None, None, False
|
|
412
|
+
else:
|
|
413
|
+
v, cmds, live = None, None, False
|
|
414
|
+
if v is None:
|
|
415
|
+
v = {'protocol': control.PROTOCOL, 'fleetmesh': app_mod.descriptor.VERSION, 'digest': app_mod.descriptor.DIGEST,
|
|
416
|
+
'release': release_mod.running().as_dict(), 'commands': list(app_mod.CONTROL)}
|
|
417
|
+
cmds = [dict(name=n, **spec) for n, spec in app_mod.CONTROL.items()]
|
|
418
|
+
if a.json:
|
|
419
|
+
return emit(a, {'version': v, 'commands': cmds, 'live': live})
|
|
420
|
+
print('%s over %s (%s)' % (v['protocol'], control.socket_path(a.dir),
|
|
421
|
+
'from the running node' if live else 'no node running; from the table it would dispatch on'))
|
|
422
|
+
print('%s@%s' % (v['fleetmesh'], v['digest'][:16] + '…'))
|
|
423
|
+
rel = v.get('release') or {}
|
|
424
|
+
if rel:
|
|
425
|
+
print('%s %s (%s%s)' % (rel.get('name'), rel.get('version'), rel.get('source'),
|
|
426
|
+
', ' + rel['commit'][:12] if rel.get('commit') else ''))
|
|
427
|
+
print('request {"cmd": name, "args": {...}} reply {"ok": true, "protocol": ..., "result": ...} | {"ok": false, "protocol": ..., "error": ...}')
|
|
428
|
+
print()
|
|
429
|
+
_table(('command', 'args', 'answers'),
|
|
430
|
+
[(c['name'], ' '.join('%s%s' % (k, '' if v_ == 'required' else '?') for k, v_ in c['args'].items()) or '-',
|
|
431
|
+
c['does']) for c in cmds])
|
|
432
|
+
return 0
|
|
433
|
+
|
|
434
|
+
|
|
435
|
+
from ..config.parameter import Parameter
|
|
436
|
+
from .adapter import add_parameter_argument
|
|
437
|
+
|
|
438
|
+
PARAM_INIT_CLASS = Parameter(key='node_class', cli_name='--class', name='node_class', description='node class', type=str, default='edge', choices=('leaf', 'edge', 'anchor'))
|
|
439
|
+
PARAM_INIT_OPERATOR = Parameter(key='operator', name='operator', description='operator pubkey, hex', type=str, default='')
|
|
440
|
+
PARAM_INIT_RELAY = Parameter(key='relay', name='relay', description='control-plane relay, repeatable', type=list, default=None)
|
|
441
|
+
PARAM_INIT_ENDPOINT = Parameter(key='endpoint', name='endpoint', description='scheme=address[:preference], repeatable', type=list, default=None)
|
|
442
|
+
PARAM_INIT_FACE_HOST = Parameter(key='face_host', cli_name='--face-host', name='face_host', description='relay face host', type=str, default='127.0.0.1')
|
|
443
|
+
PARAM_INIT_FACE_PORT = Parameter(key='face_port', cli_name='--face-port', name='face_port', description='relay face port', type=int, default=None)
|
|
444
|
+
PARAM_INIT_POW_BITS = Parameter(key='pow_bits', cli_name='--pow-bits', name='pow_bits', description='PoW difficulty bits', type=int, default=None)
|
|
445
|
+
PARAM_INIT_NAME = Parameter(key='name', name='name', description='node name', type=str, default='')
|
|
446
|
+
PARAM_INIT_MANIFEST = Parameter(key='manifest', name='manifest', description='a MeshNode manifest (yaml or json)', type=str, default='')
|
|
447
|
+
PARAM_INIT_PEERING = Parameter(key='peering', name='peering', description='a Peering manifest, repeatable', type=list, default=None)
|
|
448
|
+
|
|
449
|
+
|
|
450
|
+
class CoreCommandProvider:
|
|
451
|
+
"""Registers core lifecycle, health, identity, and operator commands."""
|
|
452
|
+
|
|
453
|
+
@classmethod
|
|
454
|
+
def register(cls, sub: argparse._SubParsersAction) -> None:
|
|
455
|
+
i = add_command(sub, 'init', cmd_init, 'generate keys, write config, mine and publish the descriptor', js=True)
|
|
456
|
+
add_parameter_argument(i, PARAM_INIT_CLASS)
|
|
457
|
+
add_parameter_argument(i, PARAM_INIT_OPERATOR)
|
|
458
|
+
add_parameter_argument(i, PARAM_INIT_RELAY)
|
|
459
|
+
add_parameter_argument(i, PARAM_INIT_ENDPOINT)
|
|
460
|
+
add_parameter_argument(i, PARAM_INIT_FACE_HOST)
|
|
461
|
+
add_parameter_argument(i, PARAM_INIT_FACE_PORT)
|
|
462
|
+
add_parameter_argument(i, PARAM_INIT_POW_BITS)
|
|
463
|
+
add_parameter_argument(i, PARAM_INIT_NAME)
|
|
464
|
+
add_parameter_argument(i, PARAM_INIT_MANIFEST)
|
|
465
|
+
add_parameter_argument(i, PARAM_INIT_PEERING)
|
|
466
|
+
|
|
467
|
+
add_command(sub, 'run', cmd_run, 'start the node and tick until SIGTERM')
|
|
468
|
+
add_command(sub, 'status', cmd_status, 'what the running node last wrote, and its trouble', js=True)
|
|
469
|
+
add_command(sub, 'health', cmd_health, 'every error plane: relays, drops, rejections, failures', js=True)
|
|
470
|
+
add_command(sub, 'whoami', cmd_whoami, "the node's identity: DID, npub, keys, descriptor", js=True)
|
|
471
|
+
|
|
472
|
+
add_command(sub, 'check', cmd_check, 'is this directory a node that can run? exits 1 if not', js=True)
|
|
473
|
+
add_command(sub, 'tick', cmd_tick, 'one tick now, and what it did (needs the node running)', js=True)
|
|
474
|
+
add_command(sub, 'api', cmd_api, 'the control socket described by itself: protocol, commands, arguments, replies', js=True)
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
"""Gossip topics: create and publish rosters, admit another admin's, leave,
|
|
2
|
+
and flood a sealed wrap into a mesh. Every verb is the running daemon's
|
|
3
|
+
(it holds the keys and the mesh); this module is the operator's words for
|
|
4
|
+
them."""
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import argparse
|
|
8
|
+
|
|
9
|
+
from node.config.parameter import Parameter
|
|
10
|
+
from node.planes.services.gossip.core import GossipMesh
|
|
11
|
+
from .adapter import CommandDispatcher
|
|
12
|
+
from .base import _ask, _peer, _short, _table, add_command, emit, fail, load_json_arg
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _show(r: dict) -> None:
|
|
16
|
+
print('Gossip: %d topic(s), %d seen id(s), %d published, %d delivered, %d refused'
|
|
17
|
+
% (len(r.get('topics', [])), r.get('seen', 0), r.get('published', 0),
|
|
18
|
+
r.get('delivered', 0), r.get('refused', 0)))
|
|
19
|
+
for t in r.get('topics', []):
|
|
20
|
+
print(' %-40s mesh %d' % (t[:40], (r.get('mesh') or {}).get(t, 0)))
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _topics(rows: list) -> None:
|
|
24
|
+
_table(['topic', 'admin', 'members', 'mesh', 'expires'],
|
|
25
|
+
[[_short(r['admin']) + ':' + r['topic'].split(':', 1)[1] if ':' in r['topic'] else r['topic'],
|
|
26
|
+
_short(r['admin']), len(r['members']), len(r['mesh']), r['expires']] for r in rows])
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _cmd_create(a: argparse.Namespace) -> int:
|
|
30
|
+
if not a.name:
|
|
31
|
+
fail('create requires --name <topic-name>')
|
|
32
|
+
members = [_peer(a.dir, m) for m in (a.member or [])]
|
|
33
|
+
if not members:
|
|
34
|
+
fail('create requires at least one --member besides this node')
|
|
35
|
+
r = _ask(a.dir, 'gossip.create', name=a.name, members=members, ttl_seconds=a.ttl,
|
|
36
|
+
no_relay=bool(a.no_relay), timeout=60)
|
|
37
|
+
|
|
38
|
+
def created(r: dict) -> None:
|
|
39
|
+
print('topic %s' % r['topic'])
|
|
40
|
+
print('roster %s, %d member(s), expires %s' % (r['event']['id'][:16], len(members) + 1,
|
|
41
|
+
next(t[1] for t in r['event']['tags'] if t[0] == 'expiration')))
|
|
42
|
+
for url, (ok, why) in (r.get('relays') or {}).items():
|
|
43
|
+
print(' %-40s %s %s' % (url, 'accepted' if ok else 'REFUSED', why))
|
|
44
|
+
if not r.get('relays') and not a.no_relay:
|
|
45
|
+
print(' (no relays configured; give the roster to the members with --json)')
|
|
46
|
+
return emit(a, r, created)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _cmd_admit(a: argparse.Namespace) -> int:
|
|
50
|
+
if not a.event:
|
|
51
|
+
fail('admit requires --event <json|@file>')
|
|
52
|
+
r = _ask(a.dir, 'gossip.admit', event=load_json_arg(a.event, '--event'), timeout=15)
|
|
53
|
+
return emit(a, r, lambda r: print('admitted %s' % r['topic']))
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _cmd_publish(a: argparse.Namespace) -> int:
|
|
57
|
+
if not a.topic or not a.event:
|
|
58
|
+
fail('publish requires --topic <id> and --event <sealed wrap json|@file>')
|
|
59
|
+
r = _ask(a.dir, 'gossip.publish', topic=a.topic, envelope=load_json_arg(a.event, '--event'), timeout=30)
|
|
60
|
+
return emit(a, r, lambda r: print('published %s into %s: sent to %d of %d mesh peer(s)'
|
|
61
|
+
% (r['event_id'][:16], r['topic'][:40], len(r['sent_to']), r['mesh'])))
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
_cmd_map = {c.key: c for c in GossipMesh.COMMANDS}
|
|
65
|
+
dispatcher = CommandDispatcher('gossip', default_action='status')
|
|
66
|
+
dispatcher.add_command('status', _cmd_map['show'], human=_show, timeout=10)
|
|
67
|
+
dispatcher.add_command('topics', _cmd_map['topics'], human=_topics, timeout=10)
|
|
68
|
+
dispatcher.add_command('leave', _cmd_map['leave'], human=lambda r, a: print('left %s, pruned %d peer(s)' % (r['topic'], r['pruned'])), timeout=15)
|
|
69
|
+
dispatcher.add_custom(
|
|
70
|
+
'create',
|
|
71
|
+
handler=_cmd_create,
|
|
72
|
+
parameters=(
|
|
73
|
+
Parameter(key='name', name='name', description='topic name for create; the id becomes <this node pk>:<name>', type=str, default=''),
|
|
74
|
+
Parameter(key='member', name='member', description='a member pubkey/did/npub/prefix for create (repeatable)', type=list, default=list, cli_name='--member'),
|
|
75
|
+
Parameter(key='ttl', name='ttl', description='roster lifetime in seconds for create (default 30 days)', type=int, default=30 * 86400),
|
|
76
|
+
Parameter(key='no_relay', name='no_relay', description='create: do not publish the roster to the configured relays', type=bool, default=False, cli_name='--no-relay'),
|
|
77
|
+
),
|
|
78
|
+
)
|
|
79
|
+
dispatcher.add_custom(
|
|
80
|
+
'admit',
|
|
81
|
+
handler=_cmd_admit,
|
|
82
|
+
parameters=(
|
|
83
|
+
Parameter(key='event', name='event', description='JSON event, or @file: a kind 30804 roster for admit, a sealed wrap for publish', type=str, default=''),
|
|
84
|
+
),
|
|
85
|
+
)
|
|
86
|
+
dispatcher.add_custom(
|
|
87
|
+
'publish',
|
|
88
|
+
handler=_cmd_publish,
|
|
89
|
+
parameters=(
|
|
90
|
+
Parameter(key='topic', name='topic', description='topic id for leave / publish', type=str, default=''),
|
|
91
|
+
Parameter(key='event', name='event', description='JSON event, or @file: a kind 30804 roster for admit, a sealed wrap for publish', type=str, default=''),
|
|
92
|
+
),
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def cmd_gossip(a: argparse.Namespace) -> int:
|
|
97
|
+
return dispatcher.dispatch(a)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
class GossipCommandProvider:
|
|
101
|
+
"""Registers the gossip topic commands."""
|
|
102
|
+
|
|
103
|
+
@classmethod
|
|
104
|
+
def register(cls, sub: argparse._SubParsersAction) -> None:
|
|
105
|
+
g = add_command(sub, 'gossip', cmd_gossip, 'gossip/1.0 group topics: create and publish rosters, flood sealed wraps', js=True)
|
|
106
|
+
dispatcher.register_arguments(g, action_choices=('status', 'topics', 'create', 'admit', 'leave', 'publish'))
|