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
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
"""`plugin-check REF`: the conformance check a plugin author runs before an
|
|
2
|
+
operator ever lists the plugin in a config. No node, no keys: the module is
|
|
3
|
+
imported and the class is held to exactly the rules `node/app/core.py`
|
|
4
|
+
applies at startup (`load_plugins`, the PIURI origin rule, `plugin_sidecars`)
|
|
5
|
+
and `node/commands` applies to its verbs - the same functions, not a copy of
|
|
6
|
+
them, so the check cannot drift from the node. Exit 0 when every rule holds,
|
|
7
|
+
1 when one does not (each failure on its own line), 2 when REF cannot be
|
|
8
|
+
imported at all."""
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import argparse
|
|
12
|
+
import json
|
|
13
|
+
from typing import Optional, TypedDict
|
|
14
|
+
|
|
15
|
+
from .. import compartment as compartment_mod
|
|
16
|
+
from ..planes.services.sidecars.manifest import SidecarDict
|
|
17
|
+
from fleetmesh_core.identity import origin as origin_mod
|
|
18
|
+
from .base import fail
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class Declares(TypedDict, total=False):
|
|
22
|
+
builtin: bool
|
|
23
|
+
after: str
|
|
24
|
+
piuris: list[str]
|
|
25
|
+
sidecars: list[SidecarDict]
|
|
26
|
+
cli: list[str]
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class Report(TypedDict):
|
|
30
|
+
ref: str
|
|
31
|
+
name: Optional[str]
|
|
32
|
+
ok: bool
|
|
33
|
+
findings: list[str]
|
|
34
|
+
declares: Declares
|
|
35
|
+
|
|
36
|
+
NOT_CONFORMANT = 1
|
|
37
|
+
NOT_IMPORTABLE = 2
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def check_plugin(ref: str) -> Report:
|
|
41
|
+
"""Every rule, as {'ref', 'ok', 'name', 'findings': [..], 'declares': {...}}.
|
|
42
|
+
Raises ValueError when the ref does not import (nothing else to say)."""
|
|
43
|
+
from ..app import core as core_mod
|
|
44
|
+
from . import PROVIDERS, _import_ref
|
|
45
|
+
cls = _import_ref(ref)
|
|
46
|
+
findings: list[str] = []
|
|
47
|
+
declares: Declares = {}
|
|
48
|
+
# 1. the subsystem/1 declaration, held to the same rules as at startup.
|
|
49
|
+
# A built-in is checked as if it were a plugin spliced in where it ships:
|
|
50
|
+
# the manifest without it is the base, its predecessor the anchor. So
|
|
51
|
+
# "plugin" means who shipped the class, not a different set of rules.
|
|
52
|
+
builtin = cls in core_mod.MANIFEST
|
|
53
|
+
declares['builtin'] = builtin
|
|
54
|
+
base = tuple(c for c in core_mod.MANIFEST if c is not cls)
|
|
55
|
+
after = None
|
|
56
|
+
if builtin:
|
|
57
|
+
i = core_mod.MANIFEST.index(cls)
|
|
58
|
+
after = str(core_mod.MANIFEST[i - 1].name) if i else None
|
|
59
|
+
try:
|
|
60
|
+
from ..config.compartments import PluginSpec
|
|
61
|
+
if builtin and after is None:
|
|
62
|
+
manifest = (cls,) + base
|
|
63
|
+
else:
|
|
64
|
+
manifest = core_mod.load_plugins([PluginSpec(ref=ref, after=after)], base)
|
|
65
|
+
declares['after'] = str(manifest[manifest.index(cls) - 1].name) if manifest.index(cls) else '-'
|
|
66
|
+
except ValueError as e:
|
|
67
|
+
findings.append(str(e))
|
|
68
|
+
api = getattr(cls, 'api', None)
|
|
69
|
+
if api != compartment_mod.Compartment.API:
|
|
70
|
+
findings.append('api is %r; this node speaks %s' % (api, compartment_mod.Compartment.API))
|
|
71
|
+
if not callable(getattr(cls, 'from_context', None)):
|
|
72
|
+
findings.append('no from_context(ctx) constructor')
|
|
73
|
+
for attr in ('requires', 'uses'):
|
|
74
|
+
for dep in getattr(cls, attr, ()) or ():
|
|
75
|
+
if dep not in {c.name for c in core_mod.MANIFEST}:
|
|
76
|
+
findings.append('%s names %r, which is not a compartment' % (attr, dep))
|
|
77
|
+
# 2. dialects: its own origin
|
|
78
|
+
piuris = list(getattr(cls, 'piuris', ()) or ())
|
|
79
|
+
declares['piuris'] = piuris
|
|
80
|
+
builtin = cls in core_mod.MANIFEST
|
|
81
|
+
for piuri in piuris:
|
|
82
|
+
# A built-in speaks fleetmesh.org's protocols by definition; the
|
|
83
|
+
# origin rule is for plugins, which speak their own.
|
|
84
|
+
if not builtin and origin_mod.reserved(piuri, core_mod.validate.FLEETMESH_ORIGIN):
|
|
85
|
+
findings.append('claims the reserved-origin protocol %s; plugins speak their own origin' % piuri)
|
|
86
|
+
if piuris and not callable(getattr(cls, 'handle', None)):
|
|
87
|
+
findings.append('declares piuris but no handle(inbound)')
|
|
88
|
+
# 3. sidecars: manifests that lint, under their own origin
|
|
89
|
+
try:
|
|
90
|
+
declares['sidecars'] = [s.to_dict() for s in core_mod.plugin_sidecars(cls)]
|
|
91
|
+
except ValueError as e:
|
|
92
|
+
findings.append(str(e))
|
|
93
|
+
# 4. CLI verbs: none that a core command owns
|
|
94
|
+
parser = argparse.ArgumentParser(prog='meshnode')
|
|
95
|
+
sub = parser.add_subparsers(dest='cmd')
|
|
96
|
+
for p in PROVIDERS:
|
|
97
|
+
p.register(sub)
|
|
98
|
+
verbs = [v for v, _ in (getattr(cls, 'cli', None) or ())]
|
|
99
|
+
declares['cli'] = verbs
|
|
100
|
+
for verb in verbs:
|
|
101
|
+
if verb in sub.choices:
|
|
102
|
+
findings.append('command %r already exists in meshnode' % verb)
|
|
103
|
+
return {'ref': ref, 'name': getattr(cls, 'name', None), 'ok': not findings,
|
|
104
|
+
'findings': findings, 'declares': declares}
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def cmd_plugin_check(a: argparse.Namespace) -> int:
|
|
108
|
+
try:
|
|
109
|
+
r = check_plugin(a.ref)
|
|
110
|
+
except ValueError as e:
|
|
111
|
+
fail(str(e), NOT_IMPORTABLE)
|
|
112
|
+
if a.json:
|
|
113
|
+
print(json.dumps(r, indent=2))
|
|
114
|
+
else:
|
|
115
|
+
d = r['declares']
|
|
116
|
+
print('%s (%s%s): %s' % (r['ref'], r['name'], ', built-in' if d.get('builtin') else '', 'conformant' if r['ok'] else 'NOT conformant'))
|
|
117
|
+
if 'after' in d:
|
|
118
|
+
print(' ticks after %s' % d['after'])
|
|
119
|
+
print(' dialects %s' % (', '.join(d.get('piuris') or []) or '-'))
|
|
120
|
+
print(' sidecars %s' % (', '.join('%s (%s)' % (s['name'], s['capability']) for s in d.get('sidecars') or []) or '-'))
|
|
121
|
+
print(' cli verbs %s' % (', '.join(d.get('cli') or []) or '-'))
|
|
122
|
+
for f in r['findings']:
|
|
123
|
+
print(' FAIL %s' % f)
|
|
124
|
+
return 0 if r['ok'] else NOT_CONFORMANT
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
class PluginCommandProvider:
|
|
128
|
+
"""Registers plugin-check."""
|
|
129
|
+
|
|
130
|
+
@classmethod
|
|
131
|
+
def register(cls, sub: argparse._SubParsersAction) -> None:
|
|
132
|
+
p = sub.add_parser('plugin-check', help='hold a plugin class to the rules the node applies at startup, with no node (for plugin authors)')
|
|
133
|
+
p.add_argument('ref', help='package.module:Class, importable from here')
|
|
134
|
+
p.add_argument('--json', action='store_true', help='print the result as JSON')
|
|
135
|
+
p.set_defaults(fn=cmd_plugin_check)
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
"""Sidecars: what the node fronts (GRASP git, ncontainer OCI, magpie files,
|
|
2
|
+
or anything an operator brings a manifest for), whether each has passed its
|
|
3
|
+
self-tests, and a way to run those tests now. The daemon owns the answer -
|
|
4
|
+
it is the one that declared or withheld the capability on the descriptor.
|
|
5
|
+
|
|
6
|
+
`sidecar-manifest` is the offline half: write a starting manifest, lint one,
|
|
7
|
+
or run its checks against an endpoint with no node at all - what a developer
|
|
8
|
+
does before an operator ever configures it."""
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import argparse
|
|
12
|
+
import json
|
|
13
|
+
|
|
14
|
+
from ..planes.services.sidecars import manifest as manifest_mod
|
|
15
|
+
from .base import add_command, fail
|
|
16
|
+
|
|
17
|
+
TEMPLATE = {
|
|
18
|
+
'sidecar': manifest_mod.VERSION,
|
|
19
|
+
'name': 'NAME',
|
|
20
|
+
'capability': 'https://YOUR-ORIGIN/NAME/1.0',
|
|
21
|
+
'endpoint_tag': 'NAME-endpoint',
|
|
22
|
+
'checks': [
|
|
23
|
+
{'name': 'http', 'method': 'HEAD', 'path': '/'},
|
|
24
|
+
{'name': 'PROTOCOL-ROUTE', 'method': 'GET', 'path': '/THE-ROUTE-THE-SPEC-MANDATES',
|
|
25
|
+
'expect': [200, 404], 'content_type': 'application/REPLACE-OR-DELETE'},
|
|
26
|
+
],
|
|
27
|
+
'docs': 'https://YOUR-ORIGIN/docs/NAME',
|
|
28
|
+
# How a deployer runs it (ops/roles/meshnode reads this; the node does
|
|
29
|
+
# not). Delete the block if the operator always runs it by hand.
|
|
30
|
+
'run': {'image': 'ghcr.io/YOUR-ORG/NAME:TAG', 'port': 7864,
|
|
31
|
+
'env': {'NAME_LISTEN': '0.0.0.0:7864'}},
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _show(r: dict) -> None:
|
|
36
|
+
rows = {k: v for k, v in r.items() if isinstance(v, dict)}
|
|
37
|
+
if not any(v.get('enabled') for v in rows.values()):
|
|
38
|
+
print('No sidecars configured.')
|
|
39
|
+
return
|
|
40
|
+
for name, v in rows.items():
|
|
41
|
+
if not v.get('enabled'):
|
|
42
|
+
print('%-11s off' % name)
|
|
43
|
+
continue
|
|
44
|
+
state = 'VERIFIED' if v.get('verified') else 'not verified'
|
|
45
|
+
print('%-11s %-13s %s' % (name, state, v.get('endpoint', '')))
|
|
46
|
+
for c in v.get('checks') or []:
|
|
47
|
+
why = (' - ' + c['why']) if c.get('why') and not c['ok'] else ''
|
|
48
|
+
print(' %-5s %-34s %s%s' % ('ok' if c['ok'] else 'FAIL', c['name'],
|
|
49
|
+
c.get('status') if c.get('status') is not None else 'no answer', why))
|
|
50
|
+
if r.get('redeclared'):
|
|
51
|
+
print('descriptor redeclared')
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _get_sidecar_command(key: str):
|
|
55
|
+
from ..planes.services.sidecars.compartment import Sidecars
|
|
56
|
+
for c in Sidecars.COMMANDS:
|
|
57
|
+
if c.key == key:
|
|
58
|
+
return c
|
|
59
|
+
raise KeyError(f'Sidecar command {key!r} not found')
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
from .adapter import CommandDispatcher
|
|
63
|
+
|
|
64
|
+
SIDECARS_DISPATCHER = CommandDispatcher('sidecars', default_action='status')
|
|
65
|
+
SIDECARS_DISPATCHER.add_command('status', _get_sidecar_command('sidecars'), human=_show, timeout=10.0)
|
|
66
|
+
SIDECARS_DISPATCHER.add_command('test', _get_sidecar_command('sidecar_test'), human=_show, timeout=60.0)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def cmd_sidecars(a: argparse.Namespace) -> int:
|
|
70
|
+
return SIDECARS_DISPATCHER.dispatch(a)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def cmd_manifest(a: argparse.Namespace) -> int:
|
|
74
|
+
if a.init:
|
|
75
|
+
doc = json.loads(json.dumps(TEMPLATE).replace('NAME', a.init))
|
|
76
|
+
with open(a.file, 'x', encoding='utf-8') as f:
|
|
77
|
+
json.dump(doc, f, indent=2); f.write('\n')
|
|
78
|
+
print('wrote %s - replace every CAPITALISED placeholder, then: meshnode sidecar-manifest %s' % (a.file, a.file))
|
|
79
|
+
return 0
|
|
80
|
+
try:
|
|
81
|
+
# Linted as a third party's: the reserved origin is refused even for
|
|
82
|
+
# a file that happens to sit in the tree.
|
|
83
|
+
from ..planes.services.sidecars.compartment import RESERVED_ORIGIN
|
|
84
|
+
s = manifest_mod.load_file(a.file, origin=RESERVED_ORIGIN)
|
|
85
|
+
except manifest_mod.ManifestError as e:
|
|
86
|
+
fail('not a usable sidecar manifest: %s' % e)
|
|
87
|
+
if not a.endpoint:
|
|
88
|
+
print('%s: ok - %s declares %s with %d check(s): %s' % (
|
|
89
|
+
a.file, s.name, s.capability, len(s.checks), ', '.join(c.name for c in s.checks)))
|
|
90
|
+
return 0
|
|
91
|
+
from ..planes.services.sidecars import compartment as sidecars_mod
|
|
92
|
+
results = [sidecars_mod.run_check(sidecars_mod._http, a.endpoint, c) for c in s.checks]
|
|
93
|
+
ok = all(r.ok for r in results)
|
|
94
|
+
if a.json:
|
|
95
|
+
print(json.dumps({'name': s.name, 'endpoint': a.endpoint, 'verified': ok,
|
|
96
|
+
'checks': [r._asdict() for r in results]}, indent=2))
|
|
97
|
+
else:
|
|
98
|
+
print('%s at %s: %s' % (s.name, a.endpoint, 'VERIFIED' if ok else 'not verified'))
|
|
99
|
+
for r in results:
|
|
100
|
+
why = (' - ' + r.why) if r.why and not r.ok else ''
|
|
101
|
+
print(' %-5s %-34s %s%s' % ('ok' if r.ok else 'FAIL', r.name,
|
|
102
|
+
r.status if r.status is not None else 'no answer', why))
|
|
103
|
+
return 0 if ok else 1
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
class SidecarsCommandProvider:
|
|
107
|
+
"""Registers the sidecars commands."""
|
|
108
|
+
|
|
109
|
+
@classmethod
|
|
110
|
+
def register(cls, sub: argparse._SubParsersAction) -> None:
|
|
111
|
+
g = add_command(sub, 'sidecars', cmd_sidecars,
|
|
112
|
+
'fronted sidecars (grasp, ncontainer, magpie, ...): self-test results, or run the tests now', js=True)
|
|
113
|
+
SIDECARS_DISPATCHER.register_arguments(g, action_choices=('status', 'test'))
|
|
114
|
+
m = sub.add_parser('sidecar-manifest',
|
|
115
|
+
help='write, lint or try a sidecar manifest with no node (for sidecar authors)')
|
|
116
|
+
m.add_argument('file', help='the manifest JSON')
|
|
117
|
+
m.add_argument('--init', metavar='NAME', help='write a starting manifest for NAME to FILE and exit')
|
|
118
|
+
m.add_argument('--endpoint', help='also run the checks against this URL, exit 1 if any fails')
|
|
119
|
+
m.add_argument('--json', action='store_true', help='print the check results as JSON')
|
|
120
|
+
m.set_defaults(fn=cmd_manifest)
|
node/commands/sync.py
ADDED
|
@@ -0,0 +1,388 @@
|
|
|
1
|
+
"""Sync scheduling, shard coverage claims, capacity metering, and strain telemetry."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import argparse
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
import sys
|
|
8
|
+
import time
|
|
9
|
+
|
|
10
|
+
from node import app as app_mod
|
|
11
|
+
from node.app import socket as control
|
|
12
|
+
from .base import (
|
|
13
|
+
_ask,
|
|
14
|
+
_live,
|
|
15
|
+
_peer,
|
|
16
|
+
_status_file,
|
|
17
|
+
_table,
|
|
18
|
+
add_command,
|
|
19
|
+
emit,
|
|
20
|
+
fail,
|
|
21
|
+
load_json_arg,
|
|
22
|
+
status_health,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _strain(st: dict) -> None:
|
|
27
|
+
if not st or not st.get('kinds'):
|
|
28
|
+
print('nothing survived yet. This node has not been stressed, which is')
|
|
29
|
+
print('not the same as being strong: a tree raised without wind grows')
|
|
30
|
+
print('tall and slender and cannot stand.')
|
|
31
|
+
return
|
|
32
|
+
print('survived:')
|
|
33
|
+
for k, n in sorted(st['kinds'].items(), key=lambda kv: -kv[1]):
|
|
34
|
+
print(' %-12s %d' % (k, n))
|
|
35
|
+
print('\n%d directions, %.2f weighted, reserves at %.2fx'
|
|
36
|
+
% (st.get('directions', 0), st.get('weighted_directions', 0),
|
|
37
|
+
st.get('reserve_multiple', 1)))
|
|
38
|
+
print('one peer moves this node by at most one direction, however hard it pushes.')
|
|
39
|
+
if st.get('flag_tree'):
|
|
40
|
+
print('\n!! flag tree: %.0f%% of it is one direction, %s'
|
|
41
|
+
% (100 * st.get('dominant_share', 0),
|
|
42
|
+
' / '.join(x for x in st['flag_tree']['direction'] if x)[:40]))
|
|
43
|
+
print(' %s' % st['flag_tree']['note'])
|
|
44
|
+
print('\nnone of this is a finding, and none of it crosses the wire.')
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def cmd_strain(a: argparse.Namespace) -> int:
|
|
48
|
+
"""What this node has survived, and what it holds because of it."""
|
|
49
|
+
st = _live(a, 'strain')
|
|
50
|
+
if st is None:
|
|
51
|
+
st = status_health(a.dir).get('strain') or {}
|
|
52
|
+
return emit(a, st, _strain)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _subsystems(subs: dict) -> None:
|
|
56
|
+
if not subs:
|
|
57
|
+
print('the node has not run yet')
|
|
58
|
+
return
|
|
59
|
+
for name in sorted(subs):
|
|
60
|
+
st = dict(subs[name])
|
|
61
|
+
ref = st.pop('refusals', {}) or {}
|
|
62
|
+
print('%s' % name)
|
|
63
|
+
for k in sorted(st):
|
|
64
|
+
v = st[k]
|
|
65
|
+
if isinstance(v, (dict, list)) and not v:
|
|
66
|
+
continue
|
|
67
|
+
print(' %-22s %s' % (k, json.dumps(v) if isinstance(v, (dict, list)) else v))
|
|
68
|
+
if ref.get('total'):
|
|
69
|
+
print(' %-22s %d' % ('refused', ref['total']))
|
|
70
|
+
for why, n in sorted((ref.get('by_reason') or {}).items(), key=lambda kv: -kv[1]):
|
|
71
|
+
print(' %6d %s' % (n, why))
|
|
72
|
+
print()
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def cmd_subsystems(a: argparse.Namespace) -> int:
|
|
76
|
+
"""Every compartment of this node, side by side."""
|
|
77
|
+
subs = _live(a, 'subsystems') or status_health(a.dir).get('subsystems') or {}
|
|
78
|
+
return emit(a, subs, _subsystems)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _render_event(e: dict) -> str:
|
|
82
|
+
data = e.get('data') or {}
|
|
83
|
+
short = ' '.join('%s=%s' % (k, (v[:12] + '…') if isinstance(v, str) and len(v) > 24 else v) for k, v in data.items())
|
|
84
|
+
return '%s %-20s %s' % (time.strftime('%H:%M:%S', time.gmtime(e.get('ts') or 0)), e.get('topic'), short)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _events(out: dict) -> None:
|
|
88
|
+
print('seq %s, ring holds %s (oldest %s), %s subscriber(s); topics: %s'
|
|
89
|
+
% (out['seq'], out['ring'], out['oldest'], out['subscribers'], ' '.join(out['topics'])))
|
|
90
|
+
for e in out['events']:
|
|
91
|
+
print(' %6s %s' % (e['seq'], _render_event(e)))
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def cmd_events(a: argparse.Namespace) -> int:
|
|
95
|
+
"""What the node's bus carried lately, from memory; needs the daemon."""
|
|
96
|
+
out = _live(a, 'events', {'since': a.since, 'topics': a.topics or None, 'last': a.last})
|
|
97
|
+
if out is None:
|
|
98
|
+
fail('events needs a running node; is one up in %s?' % a.dir)
|
|
99
|
+
return emit(a, out, _events)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def cmd_subscribe(a: argparse.Namespace) -> int:
|
|
103
|
+
"""Follow the node's bus live: one line per event until interrupted."""
|
|
104
|
+
args = {}
|
|
105
|
+
if a.topics:
|
|
106
|
+
args['topics'] = a.topics
|
|
107
|
+
if a.since is not None:
|
|
108
|
+
args['since'] = a.since
|
|
109
|
+
try:
|
|
110
|
+
it = control.stream(a.dir, 'subscribe', timeout=None, **args)
|
|
111
|
+
ack = next(it)
|
|
112
|
+
if not a.json:
|
|
113
|
+
print('subscribed at seq %s%s' % (ack['seq'], (', replaying %d' % ack['replaying']) if ack.get('replaying') else ''),
|
|
114
|
+
file=sys.stderr)
|
|
115
|
+
for e in it:
|
|
116
|
+
print(json.dumps(e) if a.json else _render_event(e), flush=True)
|
|
117
|
+
except control.ControlError as e:
|
|
118
|
+
fail(str(e))
|
|
119
|
+
except KeyboardInterrupt:
|
|
120
|
+
return 0
|
|
121
|
+
return 0
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _refusals(rows: list) -> None:
|
|
125
|
+
if not rows:
|
|
126
|
+
print('this node has refused nothing it recorded.')
|
|
127
|
+
return
|
|
128
|
+
print('%-10s %-8s %-12s %-16s %s' % ('when', 'from', 'kind', 'subject', 'why'))
|
|
129
|
+
for r in rows:
|
|
130
|
+
print('%-10s %-8s %-12s %-16s %s' % (
|
|
131
|
+
time.strftime('%H:%M:%S', time.gmtime(r['at'])), r['subsystem'][:8],
|
|
132
|
+
r['kind'][:12], (r.get('subject') or '-')[:16], r['why']))
|
|
133
|
+
if r.get('detail'):
|
|
134
|
+
print('%-10s %s' % ('', json.dumps(r['detail'])[:100]))
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def cmd_refusals(a: argparse.Namespace) -> int:
|
|
138
|
+
"""Every no this node said, in one place."""
|
|
139
|
+
rows = _live(a, 'refusals', {'last': a.last})
|
|
140
|
+
if rows is None:
|
|
141
|
+
subs = status_health(a.dir).get('subsystems') or {}
|
|
142
|
+
rows = []
|
|
143
|
+
for name, sub in subs.items():
|
|
144
|
+
ref = (sub or {}).get('refusals') or {}
|
|
145
|
+
for r in ref.get('recent') or []:
|
|
146
|
+
row = dict(r)
|
|
147
|
+
row['subsystem'] = name
|
|
148
|
+
rows.append(row)
|
|
149
|
+
rows.sort(key=lambda r: r.get('at', 0))
|
|
150
|
+
if a.last:
|
|
151
|
+
rows = rows[-int(a.last):]
|
|
152
|
+
return emit(a, rows, _refusals)
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def _capacity(c: dict) -> None:
|
|
156
|
+
if not c:
|
|
157
|
+
print('the node has not run yet, so it has carried nothing')
|
|
158
|
+
return
|
|
159
|
+
if not c.get('declared'):
|
|
160
|
+
print('no aggregate ceiling is declared.')
|
|
161
|
+
print('Every grant bounds one peer and nothing bounds this node, so it will')
|
|
162
|
+
print('accept load until it fails. What it has carried in one window:')
|
|
163
|
+
for k, v in sorted((c.get('peak') or c.get('measured') or {}).items()):
|
|
164
|
+
print(' %-12s %s' % (k, v))
|
|
165
|
+
print('\nPut a total in config.json under "capacity" to turn the shed ladder on.')
|
|
166
|
+
print('The number is yours: a class declares connectivity and not capability,')
|
|
167
|
+
print('so nothing in the protocol can guess what this machine will carry.')
|
|
168
|
+
return
|
|
169
|
+
print('pressure %.0f%%%s' % (100 * (c.get('pressure') or 0),
|
|
170
|
+
(' on %s' % c['binding']) if c.get('binding') else ''))
|
|
171
|
+
print('%-12s %10s %10s %10s' % ('counter', 'declared', 'this window', 'peak'))
|
|
172
|
+
for k in sorted(set(list(c.get('total') or {}) + list(c.get('peak') or {}))):
|
|
173
|
+
print('%-12s %10s %10s %10s' % (k, (c.get('total') or {}).get(k, '-'),
|
|
174
|
+
(c.get('measured') or {}).get(k, 0),
|
|
175
|
+
(c.get('peak') or {}).get(k, 0)))
|
|
176
|
+
if c.get('held'):
|
|
177
|
+
print('\nshedding, in ladder order:')
|
|
178
|
+
for action in c['held']:
|
|
179
|
+
since = (c.get('since') or {}).get(action)
|
|
180
|
+
print(' %-16s%s' % (action, (' since %s UTC' % time.strftime('%H:%M', time.gmtime(since)))
|
|
181
|
+
if since else ''))
|
|
182
|
+
print('none of this is a finding about any peer.')
|
|
183
|
+
else:
|
|
184
|
+
print('\nnothing is being shed.')
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def cmd_capacity(a: argparse.Namespace) -> int:
|
|
188
|
+
"""The node's own ceiling: what it promised its peers against what the
|
|
189
|
+
machine was told it can do."""
|
|
190
|
+
c = _live(a, 'capacity')
|
|
191
|
+
if c is None:
|
|
192
|
+
c = status_health(a.dir).get('capacity') or {}
|
|
193
|
+
return emit(a, c, _capacity)
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def _scheduled(s: dict) -> None:
|
|
197
|
+
pulls = s.get('pulls') or []
|
|
198
|
+
if not pulls:
|
|
199
|
+
print('no pulls scheduled')
|
|
200
|
+
else:
|
|
201
|
+
print('%-16s %-16s %-12s %s' % ('peer', 'shard', 'every', 'last run'))
|
|
202
|
+
for p in pulls:
|
|
203
|
+
last = ('%ds ago' % (int(time.time()) - p['last'])) if p.get('last') else 'never'
|
|
204
|
+
print('%-16s %-16s %-12s %s' % (p['peer'], p['shard'], '%ds' % p.get('every', 900), last))
|
|
205
|
+
if s.get('throttled'):
|
|
206
|
+
print('\nthrottled (rate-limited by peer grant):')
|
|
207
|
+
for t in s['throttled']:
|
|
208
|
+
until = time.strftime('%H:%M:%S UTC', time.gmtime(t['until'])) if t.get('until') else 'unknown'
|
|
209
|
+
print(' %-16s %-16s %s (until %s)' % (t.get('peer', '')[:16], t.get('shard', ''), t.get('why', ''), until))
|
|
210
|
+
if s.get('held'):
|
|
211
|
+
print('\nheld (under local capacity ceiling):')
|
|
212
|
+
for h in s['held']:
|
|
213
|
+
print(' %s' % h)
|
|
214
|
+
if s.get('declined_until'):
|
|
215
|
+
print('\ndeclined (waiting out interval):')
|
|
216
|
+
for ps, until in s['declined_until'].items():
|
|
217
|
+
t_str = time.strftime('%H:%M:%S UTC', time.gmtime(until))
|
|
218
|
+
print(' %-32s until %s' % (ps, t_str))
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def cmd_scheduled(a: argparse.Namespace) -> int:
|
|
222
|
+
"""Every scheduled pull, when it last ran, and what is holding it back."""
|
|
223
|
+
s = _live(a, 'scheduled')
|
|
224
|
+
if s is None:
|
|
225
|
+
cfg = app_mod.load_config(a.dir)
|
|
226
|
+
st = _status_file(a.dir)
|
|
227
|
+
health = st.get('health') or {}
|
|
228
|
+
subs = health.get('subsystems') or {}
|
|
229
|
+
sched_sub = subs.get('scheduler') or {}
|
|
230
|
+
s = {
|
|
231
|
+
'pulls': [
|
|
232
|
+
{
|
|
233
|
+
'peer': (p.get('pubkey') or p.get('peer') or '')[:16],
|
|
234
|
+
'shard': p.get('shard', ''),
|
|
235
|
+
'every': p.get('pull_every', 900),
|
|
236
|
+
'last': None,
|
|
237
|
+
}
|
|
238
|
+
for p in cfg.get('peers') or []
|
|
239
|
+
],
|
|
240
|
+
'throttled': sched_sub.get('throttled') or health.get('throttled') or [],
|
|
241
|
+
'held': sched_sub.get('held') or [],
|
|
242
|
+
'declined_until': sched_sub.get('declined_until') or {},
|
|
243
|
+
}
|
|
244
|
+
return emit(a, s, _scheduled)
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def _coverage(cov: list) -> None:
|
|
248
|
+
if not cov:
|
|
249
|
+
print('no shards claimed')
|
|
250
|
+
return
|
|
251
|
+
_table(('shard', 'completeness', 'filter'),
|
|
252
|
+
[(c['shard'], c.get('completeness', 'best-effort'), json.dumps(c['filter'])) for c in cov])
|
|
253
|
+
print('\n`asserted` is a request: it is honoured only for a shard every event of which arrived at the relay face')
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
from ..config.parameter import Parameter
|
|
257
|
+
from .adapter import CommandDispatcher
|
|
258
|
+
|
|
259
|
+
PARAM_SHARD = Parameter(key='shard', name='shard', description='shard name', type=str, default='', positional=True, required=False, nargs='?')
|
|
260
|
+
PARAM_FILTER = Parameter(key='filter', name='filter', description='a nostr filter, JSON or @file (add)', type=str, default='')
|
|
261
|
+
PARAM_ASSERTED = Parameter(key='asserted', name='asserted', description='claim completeness (honoured only if every event arrived directly)', type=bool, default=False)
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def _coverage_list(a: argparse.Namespace) -> int:
|
|
265
|
+
cfg = app_mod.load_config(a.dir)
|
|
266
|
+
cov = list(cfg.get('coverage') or [])
|
|
267
|
+
return emit(a, cov, _coverage)
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def _coverage_remove(a: argparse.Namespace) -> int:
|
|
271
|
+
cfg = dict(app_mod.load_config(a.dir))
|
|
272
|
+
cov = list(cfg.get('coverage') or [])
|
|
273
|
+
if not a.shard:
|
|
274
|
+
fail('remove needs a SHARD')
|
|
275
|
+
kept = [c for c in cov if c['shard'] != a.shard]
|
|
276
|
+
if len(kept) == len(cov):
|
|
277
|
+
fail('%s is not claimed' % a.shard)
|
|
278
|
+
cfg['coverage'] = kept
|
|
279
|
+
app_mod.save_config(a.dir, cfg)
|
|
280
|
+
print('no longer claiming %s; the claim on the relays expires on its own' % a.shard)
|
|
281
|
+
return 0
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
def _coverage_add(a: argparse.Namespace) -> int:
|
|
285
|
+
cfg = dict(app_mod.load_config(a.dir))
|
|
286
|
+
cov = list(cfg.get('coverage') or [])
|
|
287
|
+
if not a.shard:
|
|
288
|
+
fail('add needs a SHARD')
|
|
289
|
+
if not a.filter:
|
|
290
|
+
fail('add needs --filter')
|
|
291
|
+
filt = load_json_arg(a.filter, '--filter')
|
|
292
|
+
entry = {'shard': a.shard, 'filter': filt, 'completeness': 'asserted' if a.asserted else 'best-effort'}
|
|
293
|
+
cfg['coverage'] = [c for c in cov if c['shard'] != a.shard] + [entry]
|
|
294
|
+
problems = app_mod.validate_config(cfg)
|
|
295
|
+
if problems:
|
|
296
|
+
fail('\n'.join(problems))
|
|
297
|
+
app_mod.save_config(a.dir, cfg)
|
|
298
|
+
print('claiming %s as %s on the next %s' % (a.shard, entry['completeness'], 'tick' if control.running(a.dir) else 'run'))
|
|
299
|
+
return 0
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
COVERAGE_DISPATCHER = CommandDispatcher('coverage', default_action='list')
|
|
303
|
+
COVERAGE_DISPATCHER.add_custom('list', _coverage_list)
|
|
304
|
+
COVERAGE_DISPATCHER.add_custom('add', _coverage_add, parameters=(PARAM_SHARD, PARAM_FILTER, PARAM_ASSERTED))
|
|
305
|
+
COVERAGE_DISPATCHER.add_custom('remove', _coverage_remove, parameters=(PARAM_SHARD,))
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
def cmd_coverage(a: argparse.Namespace) -> int:
|
|
309
|
+
return COVERAGE_DISPATCHER.dispatch(a)
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
def cmd_descriptor(a: argparse.Namespace) -> int:
|
|
313
|
+
dpath = os.path.join(a.dir, app_mod.NodeApp.DESCRIPTOR_FILE)
|
|
314
|
+
if not os.path.exists(dpath):
|
|
315
|
+
fail('no descriptor.json yet; it is mined on the first run')
|
|
316
|
+
with open(dpath, encoding='utf-8') as _fh:
|
|
317
|
+
d = json.load(_fh)
|
|
318
|
+
if a.republish:
|
|
319
|
+
r = _ask(a.dir, 'republish', timeout=60)
|
|
320
|
+
|
|
321
|
+
def human(r: dict) -> None:
|
|
322
|
+
for url, (okay, note) in (r.get('relays') or {}).items():
|
|
323
|
+
print(' %-8s %s %s' % ('sent' if okay else 'FAILED', url, note))
|
|
324
|
+
return emit(a, r, human)
|
|
325
|
+
# The descriptor is JSON whether or not --json was asked for.
|
|
326
|
+
print(json.dumps(d, indent=2))
|
|
327
|
+
return 0
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
def cmd_pull(a: argparse.Namespace) -> int:
|
|
331
|
+
pk = _peer(a.dir, a.peer)
|
|
332
|
+
filt = None
|
|
333
|
+
if a.filter:
|
|
334
|
+
filt = load_json_arg(a.filter, '--filter')
|
|
335
|
+
else:
|
|
336
|
+
for p in app_mod.load_config(a.dir).get('peers', []):
|
|
337
|
+
if p['pubkey'] == pk and p['shard'] == a.shard:
|
|
338
|
+
filt = p['filter']
|
|
339
|
+
if filt is None:
|
|
340
|
+
fail('no filter for shard %s with this peer; give --filter' % a.shard)
|
|
341
|
+
# The daemon's own pull deadline is sent as a request argument and the
|
|
342
|
+
# client waits a little longer than it, so the node's answer (done, or
|
|
343
|
+
# timed out by name) always arrives before this end gives up.
|
|
344
|
+
r = _ask(a.dir, 'pull', peer=pk, shard=a.shard, filter=filt, force=a.force,
|
|
345
|
+
timeout=a.timeout + 10, request={'timeout': a.timeout})
|
|
346
|
+
|
|
347
|
+
def human(r: dict) -> None:
|
|
348
|
+
c = r.get('counters') or {}
|
|
349
|
+
print('%s: %s events in, %s ranges, %s bytes in, dup ratio %s'
|
|
350
|
+
% (r.get('status'), c.get('events_in', 0), c.get('sync_ranges', 0), c.get('bytes_in', 0), r.get('dup_ratio')))
|
|
351
|
+
for sev, why in r.get('findings') or []:
|
|
352
|
+
print(' finding %s: %s' % (sev, why))
|
|
353
|
+
return emit(a, r, human)
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
class SyncCommandProvider:
|
|
357
|
+
"""Registers sync, shard coverage, capacity, strain, and pull commands."""
|
|
358
|
+
|
|
359
|
+
@classmethod
|
|
360
|
+
def register(cls, sub: argparse._SubParsersAction) -> None:
|
|
361
|
+
add_command(sub, 'strain', cmd_strain, "what this node has survived, by direction, and the reserves it sets", js=True)
|
|
362
|
+
add_command(sub, 'subsystems', cmd_subsystems, 'every compartment of this node, and what each has refused', js=True)
|
|
363
|
+
|
|
364
|
+
rf = add_command(sub, 'refusals', cmd_refusals, 'every no this node said, across every compartment', js=True)
|
|
365
|
+
rf.add_argument('--last', type=int, default=5, help='how many per compartment')
|
|
366
|
+
|
|
367
|
+
ev = add_command(sub, 'events', cmd_events, "the node's recent bus events, from memory", js=True)
|
|
368
|
+
ev.add_argument('--since', type=int, default=0, help='only events after this seq')
|
|
369
|
+
ev.add_argument('--topics', nargs='*', help='keep only these topics')
|
|
370
|
+
ev.add_argument('--last', type=int, default=50, help='at most this many, newest')
|
|
371
|
+
sb = add_command(sub, 'subscribe', cmd_subscribe, "follow the node's bus live, one line per event", js=True)
|
|
372
|
+
sb.add_argument('--since', type=int, help='replay what the node still holds after this seq first')
|
|
373
|
+
sb.add_argument('--topics', nargs='*', help='keep only these topics')
|
|
374
|
+
|
|
375
|
+
add_command(sub, 'capacity', cmd_capacity, "this node's own ceiling, the pressure on it, and what it is shedding", js=True)
|
|
376
|
+
add_command(sub, 'scheduled', cmd_scheduled, 'every scheduled pull, throttles and holds', js=True)
|
|
377
|
+
|
|
378
|
+
c = add_command(sub, 'coverage', cmd_coverage, 'the shards this node claims to hold, as kind 30803', js=True)
|
|
379
|
+
COVERAGE_DISPATCHER.register_arguments(c, action_choices=('list', 'add', 'remove'))
|
|
380
|
+
|
|
381
|
+
d = add_command(sub, 'descriptor', cmd_descriptor, 'the kind 11801 this node published', js=True)
|
|
382
|
+
d.add_argument('--republish', action='store_true', help='send it to the relays again (needs the node running)')
|
|
383
|
+
|
|
384
|
+
pl = add_command(sub, 'pull', cmd_pull, 'pull a shard from a peer now (needs the node running)', peer=True, js=True)
|
|
385
|
+
pl.add_argument('shard')
|
|
386
|
+
pl.add_argument('--filter', help='a nostr filter, JSON or @file; default: the one configured for this peer and shard')
|
|
387
|
+
pl.add_argument('--force', action='store_true', help='pull even when the grant does not cover a session now')
|
|
388
|
+
pl.add_argument('--timeout', type=float, default=60.0, help='seconds the node gives the session (default 60, max 600)')
|