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,461 @@
|
|
|
1
|
+
"""Sidecars: fronting off-the-shelf projects the node does not reimplement.
|
|
2
|
+
|
|
3
|
+
This compartment does not speak git smart-http, the OCI distribution
|
|
4
|
+
protocol or Magpie's media API itself - ngit-grasp, an ncontainer-compatible
|
|
5
|
+
gateway and magpie already do, and reimplementing any of them (Option B,
|
|
6
|
+
considered and rejected each time) buys nothing a sidecar does not already
|
|
7
|
+
give for less code and less to keep current with upstream. What this
|
|
8
|
+
compartment owns is the bridge:
|
|
9
|
+
|
|
10
|
+
* which capabilities this node declares, and the endpoint tags a leaf
|
|
11
|
+
reads to find the sidecar - all opt-in, all carried on the kind 11801
|
|
12
|
+
descriptor the same way mint/price already are (see
|
|
13
|
+
PaidFetch.descriptor_tags).
|
|
14
|
+
* whether the sidecar actually speaks its protocol. Every sidecar ships
|
|
15
|
+
its own self-tests (a git `info/refs`, an OCI `/v2/`, a magpie files
|
|
16
|
+
route): they run when the compartment starts, on the usual cadence after
|
|
17
|
+
that, and on demand through the `sidecar_test` verb. A capability is
|
|
18
|
+
declared ONLY while the tests pass - a config flag alone declares
|
|
19
|
+
nothing - and when the verified set changes the compartment rebuilds the
|
|
20
|
+
descriptor there and then, so a kind 11801 never advertises a protocol
|
|
21
|
+
nothing behind this node can answer. A failing sidecar is `trouble()`
|
|
22
|
+
the way every other mechanism here is.
|
|
23
|
+
* nothing about payment: PaidFetch.quote_session (GRASP, coarse per
|
|
24
|
+
clone/fetch session) and PaidFetch.quote (ncontainer and magpie, per blob -
|
|
25
|
+
a resolved OCI digest or a Magpie file sha256 is priced exactly like any
|
|
26
|
+
other Blossom fetch) are the payment surface, called from the reverse
|
|
27
|
+
proxy in front of each sidecar, not from here.
|
|
28
|
+
|
|
29
|
+
What a sidecar IS lives in a manifest (node/sidecar_manifest.py), not here:
|
|
30
|
+
the node ships three in node/planes/services/sidecars/manifests/ (grasp, ncontainer, magpie);
|
|
31
|
+
an operator adds a fourth with `sidecars.<name>.manifest`; a plugin may
|
|
32
|
+
declare more. This compartment reads the `sidecars` config block, resolves
|
|
33
|
+
each entry to a manifest, runs the manifest's checks and declares what
|
|
34
|
+
passes. Adding a sidecar is a JSON file - not a change to this one.
|
|
35
|
+
|
|
36
|
+
A node with no `sidecars` block behaves exactly as before: no capability
|
|
37
|
+
tag, no endpoint tag, nothing to test, nothing in `trouble()`.
|
|
38
|
+
"""
|
|
39
|
+
from __future__ import annotations
|
|
40
|
+
|
|
41
|
+
import urllib.parse
|
|
42
|
+
from typing import Any, Callable, Mapping, MutableMapping, NamedTuple, Optional
|
|
43
|
+
|
|
44
|
+
from node.transport import netfetch
|
|
45
|
+
from . import manifest as manifest_mod
|
|
46
|
+
from node import compartment as compartment_mod
|
|
47
|
+
from node.compartment import stores as st
|
|
48
|
+
from node.channels import Channel, INFO, NOTE
|
|
49
|
+
from node.metrics import COUNTER, Metric
|
|
50
|
+
from node.compartment import Command, Every, Need
|
|
51
|
+
from node.config.parameter import Parameter
|
|
52
|
+
from fleetmesh_core.wire import validate
|
|
53
|
+
from node.config import SidecarEntry, sidecars_of, SidecarsConfig
|
|
54
|
+
from .manifest import Check, ManifestError, Sidecar # noqa: F401 - the compartment's own vocabulary
|
|
55
|
+
|
|
56
|
+
# The origin whose capability identifiers the schema digest governs: only a
|
|
57
|
+
# shipped manifest may declare under it. Derived from the schema, never
|
|
58
|
+
# written down here (node/ names no host; see test_node's unplug scan).
|
|
59
|
+
RESERVED_ORIGIN = validate.FLEETMESH_ORIGIN.rstrip('/')
|
|
60
|
+
|
|
61
|
+
# The manifests the node ships, in descriptor order. Operator and plugin
|
|
62
|
+
# manifests are appended behind them, in config order.
|
|
63
|
+
BUILTINS: tuple[Sidecar, ...] = manifest_mod.builtins()
|
|
64
|
+
_BUILTIN_CAPS = {s.name: s.capability for s in BUILTINS}
|
|
65
|
+
CAP_GRASP = _BUILTIN_CAPS['grasp']
|
|
66
|
+
CAP_NCONTAINER = _BUILTIN_CAPS['ncontainer']
|
|
67
|
+
CAP_MAGPIE = _BUILTIN_CAPS['magpie']
|
|
68
|
+
SIDECARS = BUILTINS # the historical name; the live set is Sidecars.registry()
|
|
69
|
+
_BUILTIN_BY_NAME: dict[str, Sidecar] = {s.name: s for s in BUILTINS}
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
from .types import SidecarStatusReport
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def resolve_manifest(name: str, entry: Mapping[str, Any],
|
|
76
|
+
extra: Mapping[str, Sidecar] = {}) -> Sidecar:
|
|
77
|
+
"""The manifest a config entry stands for: inline or `@file` when given,
|
|
78
|
+
else a built-in or plugin-declared manifest of that name. A manifest's
|
|
79
|
+
`name` must match the entry key it is configured under - the key is the
|
|
80
|
+
systemd unit and the Caddy route, the manifest the protocol."""
|
|
81
|
+
raw = entry.get('manifest')
|
|
82
|
+
if raw is None:
|
|
83
|
+
s = _BUILTIN_BY_NAME.get(name) or extra.get(name)
|
|
84
|
+
if s is None:
|
|
85
|
+
raise ManifestError('sidecars.%s: not a built-in (%s) and no manifest given' % (
|
|
86
|
+
name, ', '.join(_BUILTIN_BY_NAME)))
|
|
87
|
+
return s
|
|
88
|
+
if isinstance(raw, str):
|
|
89
|
+
if not raw.startswith('@'):
|
|
90
|
+
raise ManifestError('sidecars.%s.manifest must be "@/path/to/manifest.json" or an object' % name)
|
|
91
|
+
s = manifest_mod.load_file(raw[1:], origin=RESERVED_ORIGIN)
|
|
92
|
+
else:
|
|
93
|
+
s = manifest_mod.parse(raw, origin=RESERVED_ORIGIN)
|
|
94
|
+
if s.name != name:
|
|
95
|
+
raise ManifestError('sidecars.%s: the manifest calls itself %r; the key and the name must agree' % (name, s.name))
|
|
96
|
+
return s
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
# How often a tick re-runs the checks - matching the cadence other
|
|
100
|
+
# compartments use for anything that costs a network round trip. The first
|
|
101
|
+
# run is at start(), not a cadence later.
|
|
102
|
+
_CHECK_EVERY_SECONDS = 300
|
|
103
|
+
_PROBE_TIMEOUT = 3.0
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
class ProbeResult(NamedTuple):
|
|
107
|
+
"""What one check said. `status` is the HTTP status when there was one."""
|
|
108
|
+
name: str
|
|
109
|
+
ok: bool
|
|
110
|
+
status: Optional[int]
|
|
111
|
+
why: str
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
Fetcher = Callable[[str, str], tuple[int, Mapping[str, str]]]
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _http(url: str, method: str, timeout: float = _PROBE_TIMEOUT) -> tuple[int, Mapping[str, str]]:
|
|
118
|
+
"""One request to the sidecar: (status, headers). Only http/https are
|
|
119
|
+
ever dialled - the URL is operator config, but urlopen would otherwise
|
|
120
|
+
honour file:// and ftp:// too - and it goes through netfetch so redirects
|
|
121
|
+
stay inside the same policy. allow_private=True because sidecars are
|
|
122
|
+
local by design. Raises on anything that is not an HTTP answer."""
|
|
123
|
+
parts = urllib.parse.urlsplit(url)
|
|
124
|
+
if parts.scheme not in ('http', 'https'):
|
|
125
|
+
raise ValueError('not an http(s) URL')
|
|
126
|
+
try:
|
|
127
|
+
_body, headers = netfetch.fetch(url, max_bytes=4096, timeout=timeout, allow_private=True, method=method)
|
|
128
|
+
return 200, headers
|
|
129
|
+
except netfetch.FetchHTTPError as e:
|
|
130
|
+
return e.code, e.headers or {}
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def _reachable(url: str, timeout: float = _PROBE_TIMEOUT) -> bool:
|
|
134
|
+
"""HEAD the sidecar. Reachable means it answered HTTP at all: a 4xx is a
|
|
135
|
+
live process that disliked HEAD, not an absent one."""
|
|
136
|
+
try:
|
|
137
|
+
status, _ = _http(url, 'HEAD', timeout)
|
|
138
|
+
return status < 500
|
|
139
|
+
except Exception:
|
|
140
|
+
return False
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def run_check(fetch: Fetcher, endpoint: str, check: Check) -> ProbeResult:
|
|
144
|
+
"""One check against one endpoint, never raising."""
|
|
145
|
+
url = endpoint.rstrip('/') + check.path
|
|
146
|
+
try:
|
|
147
|
+
status, headers = fetch(url, check.method)
|
|
148
|
+
except Exception as e:
|
|
149
|
+
return ProbeResult(check.name, False, None, '%s: %s' % (type(e).__name__, str(e)[:80] or 'no answer'))
|
|
150
|
+
if status not in check.expect:
|
|
151
|
+
return ProbeResult(check.name, False, status, 'HTTP %d, wanted one of %s' % (status, sorted(check.expect)))
|
|
152
|
+
if status == 200 and check.content_type:
|
|
153
|
+
ctype = next((v for k, v in headers.items() if k.lower() == 'content-type'), '')
|
|
154
|
+
if not str(ctype).lower().startswith(check.content_type):
|
|
155
|
+
return ProbeResult(check.name, False, status, 'content-type %r is not %s' % (ctype, check.content_type))
|
|
156
|
+
return ProbeResult(check.name, True, status, '')
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
class Sidecars(compartment_mod.Compartment):
|
|
160
|
+
"""Declares one capability per sidecar whose self-tests pass, and keeps
|
|
161
|
+
testing the sidecars behind them. See module docstring for what it does
|
|
162
|
+
not do."""
|
|
163
|
+
|
|
164
|
+
name = 'sidecars'
|
|
165
|
+
reads = ('sidecars',) + tuple(s.name for s in BUILTINS) # the old top-level keys, one release more
|
|
166
|
+
live = ('sidecars',) + tuple(s.name for s in BUILTINS)
|
|
167
|
+
config_cls = SidecarsConfig
|
|
168
|
+
STORES = (
|
|
169
|
+
st.Store(key='probes', description='recent sidecar probe results',
|
|
170
|
+
kind=st.RING, shape=dict, bound=256),
|
|
171
|
+
)
|
|
172
|
+
CHANNELS = (
|
|
173
|
+
Channel('sidecar_verified', 'Sidecar self-tests passed and capability declared', level=INFO, say='sidecar {name} verified and declared', tags=('sidecars', 'probe')),
|
|
174
|
+
Channel('sidecar_failed', 'Sidecar self-test failed and capability retracted', level=NOTE, say='sidecar {name} self-test failed: {why}', tags=('sidecars', 'probe')),
|
|
175
|
+
)
|
|
176
|
+
METRICS = (
|
|
177
|
+
Metric(name='sidecars_verified_total', description='Times sidecars verified.', kind=COUNTER, counts='sidecar_verified', tags=('sidecars', 'probe')),
|
|
178
|
+
Metric(name='sidecars_failed_total', description='Times sidecar probes failed.', kind=COUNTER, counts='sidecar_failed', tags=('sidecars', 'probe')),
|
|
179
|
+
)
|
|
180
|
+
NEEDS = (
|
|
181
|
+
Need('cfg', 'cfg', getter=True),
|
|
182
|
+
Need('descriptor', 'descriptor'),
|
|
183
|
+
Need('redescribe', 'redescribe'),
|
|
184
|
+
)
|
|
185
|
+
EVERY = (Every(seconds=_CHECK_EVERY_SECONDS, run='_check_endpoints'),)
|
|
186
|
+
COMMANDS = (
|
|
187
|
+
Command(
|
|
188
|
+
key='sidecars',
|
|
189
|
+
name='Configured Sidecars',
|
|
190
|
+
description='each configured sidecar, its endpoint, whether it is verified and its last self-test',
|
|
191
|
+
tags=('sidecars', 'status'),
|
|
192
|
+
permission='sidecars/sidecars',
|
|
193
|
+
run='list_sidecars',
|
|
194
|
+
reply='dict',
|
|
195
|
+
),
|
|
196
|
+
Command(
|
|
197
|
+
key='sidecar_test',
|
|
198
|
+
name='Sidecar Self-Test',
|
|
199
|
+
description='run the self-tests of one sidecar (or all) now and redeclare if the result changed',
|
|
200
|
+
tags=('sidecars', 'test'),
|
|
201
|
+
parameters=(
|
|
202
|
+
Parameter(key='name', name='name', description='test: only this sidecar (default all enabled)', type=str, default=None, required=False),
|
|
203
|
+
),
|
|
204
|
+
permission='sidecars/sidecar_test',
|
|
205
|
+
run='run_sidecar_test',
|
|
206
|
+
reply='{name: {verified, checks: [{name, ok, status, why}], at}}',
|
|
207
|
+
),
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
def __init__(self, cfg: Callable[[], Mapping[str, Any]], clock: Optional[Callable[[], int]] = None,
|
|
211
|
+
checker: Optional[Callable[[str], bool]] = None, node: Any = None,
|
|
212
|
+
fetch: Fetcher = _http,
|
|
213
|
+
descriptor: Optional[Callable[[], Optional[Mapping[str, Any]]]] = None,
|
|
214
|
+
redescribe: Optional[Callable[[], Any]] = None) -> None:
|
|
215
|
+
super().__init__(node, clock)
|
|
216
|
+
self._cfg = cfg
|
|
217
|
+
# `checker(url) -> bool` is the older, coarser injection: when given it
|
|
218
|
+
# stands in for every check of a sidecar. `fetch` is the real seam.
|
|
219
|
+
self._checker = checker
|
|
220
|
+
self._fetch = fetch
|
|
221
|
+
self._descriptor = descriptor or (lambda: None)
|
|
222
|
+
self._redescribe = redescribe
|
|
223
|
+
self._last_checked: dict[str, int] = {} # not a store: local configured sidecars probe timestamps
|
|
224
|
+
self._results: dict[str, list[ProbeResult]] = {} # not a store: local configured sidecars probe results
|
|
225
|
+
self._verified: dict[str, bool] = {} # not a store: local configured sidecars verified flags
|
|
226
|
+
self._declared: dict[str, Sidecar] = {} # not a store: plugin-declared manifests, by name
|
|
227
|
+
self._manifest_errors: dict[str, str] = {} # not a store: manifest errors for configured sidecars
|
|
228
|
+
self.redeclared = 0
|
|
229
|
+
self._closed = False
|
|
230
|
+
|
|
231
|
+
# -- config -----------------------------------------------------------
|
|
232
|
+
def _entries(self) -> dict[str, SidecarEntry]:
|
|
233
|
+
"""The `sidecars` block as it is now (old top-level keys folded in);
|
|
234
|
+
every sidecar reads live."""
|
|
235
|
+
out = {}
|
|
236
|
+
for name, e in sidecars_of(self._cfg()).items():
|
|
237
|
+
e = e or {}
|
|
238
|
+
out[name] = SidecarEntry(enabled=bool(e.get('enabled', False)), endpoint=e.get('endpoint'),
|
|
239
|
+
manifest=e.get('manifest'))
|
|
240
|
+
return out
|
|
241
|
+
|
|
242
|
+
def block(self, name: str) -> SidecarEntry:
|
|
243
|
+
return self._entries().get(name, SidecarEntry())
|
|
244
|
+
|
|
245
|
+
def enabled(self, name: str) -> bool:
|
|
246
|
+
return self.block(name).on
|
|
247
|
+
|
|
248
|
+
def registry(self) -> list[Sidecar]:
|
|
249
|
+
"""Every sidecar this node knows of, in descriptor order: the
|
|
250
|
+
built-ins, then plugin-declared ones, then any the operator named
|
|
251
|
+
only by manifest. A configured entry whose manifest does not resolve
|
|
252
|
+
is remembered as a manifest problem (trouble()) and skipped - a
|
|
253
|
+
bad manifest declares nothing, same as a failing check."""
|
|
254
|
+
self._manifest_errors = {} # not a store: reset on registry evaluation
|
|
255
|
+
out: list[Sidecar] = list(BUILTINS)
|
|
256
|
+
seen = {s.name for s in out}
|
|
257
|
+
for name, s in self._declared.items():
|
|
258
|
+
if name not in seen:
|
|
259
|
+
out.append(s); seen.add(name)
|
|
260
|
+
for name, e in self._entries().items():
|
|
261
|
+
if e.manifest is None and name in seen:
|
|
262
|
+
continue
|
|
263
|
+
try:
|
|
264
|
+
s = resolve_manifest(name, {'manifest': e.manifest}, self._declared)
|
|
265
|
+
except (ManifestError, OSError) as err:
|
|
266
|
+
self._manifest_errors[name] = str(err)
|
|
267
|
+
continue
|
|
268
|
+
if name in seen: # operator manifest overrides a plugin's, never a built-in
|
|
269
|
+
if name in _BUILTIN_BY_NAME:
|
|
270
|
+
self._manifest_errors[name] = 'sidecars.%s: %s is a built-in; its manifest is not overridable' % (name, name)
|
|
271
|
+
continue
|
|
272
|
+
out = [s if x.name == name else x for x in out]
|
|
273
|
+
else:
|
|
274
|
+
out.append(s); seen.add(name)
|
|
275
|
+
return out
|
|
276
|
+
|
|
277
|
+
def manifest(self, name: str) -> Sidecar:
|
|
278
|
+
for s in self.registry():
|
|
279
|
+
if s.name == name:
|
|
280
|
+
return s
|
|
281
|
+
raise KeyError('no sidecar named %r (have: %s)' % (name, ', '.join(x.name for x in self.registry())))
|
|
282
|
+
|
|
283
|
+
def declare(self, sidecar: Sidecar) -> None:
|
|
284
|
+
"""A plugin (or test) adds a manifest the node did not ship. Refused
|
|
285
|
+
under the reserved origin, like any other third-party manifest."""
|
|
286
|
+
manifest_mod.parse(sidecar.to_dict(), origin=RESERVED_ORIGIN)
|
|
287
|
+
self._declared[sidecar.name] = sidecar
|
|
288
|
+
|
|
289
|
+
# Readable names for the individual sidecars; enabled(name) is the
|
|
290
|
+
# generic form and what the table-driven paths use.
|
|
291
|
+
def grasp_enabled(self) -> bool:
|
|
292
|
+
return self.enabled('grasp')
|
|
293
|
+
|
|
294
|
+
def ncontainer_enabled(self) -> bool:
|
|
295
|
+
return self.enabled('ncontainer')
|
|
296
|
+
|
|
297
|
+
def magpie_enabled(self) -> bool:
|
|
298
|
+
return self.enabled('magpie')
|
|
299
|
+
|
|
300
|
+
def _endpoints(self) -> dict[str, str]:
|
|
301
|
+
"""Enabled entries whose manifest resolved. One with a manifest
|
|
302
|
+
problem is not an endpoint: it is neither tested nor declared."""
|
|
303
|
+
known = {s.name for s in self.registry()} - set(self._manifest_errors)
|
|
304
|
+
return {n: str(e.endpoint) for n, e in self._entries().items() if e.on and n in known}
|
|
305
|
+
|
|
306
|
+
def verified(self, name: str) -> bool:
|
|
307
|
+
"""Enabled now AND its last self-test passed."""
|
|
308
|
+
return self.enabled(name) and self._verified.get(name, False)
|
|
309
|
+
|
|
310
|
+
# -- what the descriptor says ------------------------------------------
|
|
311
|
+
def capability_protocols(self) -> list[str]:
|
|
312
|
+
"""Extra `c` protocol identifiers this node's descriptor should carry,
|
|
313
|
+
alongside the required ones `descriptor.build` always adds. Only the
|
|
314
|
+
sidecars whose self-tests pass: enabling one declares nothing until
|
|
315
|
+
it answers."""
|
|
316
|
+
return [s.capability for s in self.registry() if self.verified(s.name)]
|
|
317
|
+
|
|
318
|
+
def descriptor_tags(self) -> list[list[str]]:
|
|
319
|
+
"""<name>-endpoint, only for a verified sidecar - see
|
|
320
|
+
schema/kinds.json's note on kind 11801."""
|
|
321
|
+
eps = self._endpoints()
|
|
322
|
+
return [[s.tag, eps[s.name]] for s in self.registry() if s.name in eps and self.verified(s.name)]
|
|
323
|
+
|
|
324
|
+
def _declared_on_descriptor(self) -> set[str]:
|
|
325
|
+
desc = self._descriptor()
|
|
326
|
+
if not desc:
|
|
327
|
+
return set()
|
|
328
|
+
caps = {t[1] for t in desc.get('tags', []) if len(t) > 1 and t[0] == 'c'}
|
|
329
|
+
return {s.name for s in self.registry() if s.capability in caps}
|
|
330
|
+
|
|
331
|
+
# -- the self-tests ------------------------------------------------------
|
|
332
|
+
def test(self, name: str, now: Optional[int] = None) -> list[ProbeResult]:
|
|
333
|
+
"""Run one sidecar's checks now. Records the verdict; does not redeclare."""
|
|
334
|
+
s = self.manifest(name)
|
|
335
|
+
at = now if now is not None else self.clock()
|
|
336
|
+
url = self._endpoints().get(name)
|
|
337
|
+
if url is None:
|
|
338
|
+
results = [ProbeResult('enabled', False, None, 'not enabled')]
|
|
339
|
+
elif self._checker is not None:
|
|
340
|
+
up = self._checker(url)
|
|
341
|
+
results = [ProbeResult(c.name, up, 200 if up else None, '' if up else 'no answer') for c in s.checks]
|
|
342
|
+
else:
|
|
343
|
+
results = [run_check(self._fetch, url, c) for c in s.checks]
|
|
344
|
+
self._last_checked[name] = at
|
|
345
|
+
self._results[name] = results
|
|
346
|
+
self._verified[name] = url is not None and all(r.ok for r in results)
|
|
347
|
+
for r in results:
|
|
348
|
+
if not r.ok:
|
|
349
|
+
self.refusals.refuse('sidecar', 'self-test %s failed' % r.name, subject=name,
|
|
350
|
+
detail={'endpoint': url or '', 'status': r.status, 'why': r.why}, at=at)
|
|
351
|
+
return results
|
|
352
|
+
|
|
353
|
+
def test_all(self, now: Optional[int] = None, only: Optional[str] = None) -> dict[str, list[ProbeResult]]:
|
|
354
|
+
names = [only] if only else list(self._endpoints())
|
|
355
|
+
return {n: self.test(n, now) for n in names}
|
|
356
|
+
|
|
357
|
+
def redeclare_if_changed(self) -> bool:
|
|
358
|
+
"""Rebuild the descriptor when what should be declared differs from
|
|
359
|
+
what the current one says. True when it was rebuilt."""
|
|
360
|
+
want = {s.name for s in self.registry() if self.verified(s.name)}
|
|
361
|
+
if want == self._declared_on_descriptor():
|
|
362
|
+
return False
|
|
363
|
+
if self._redescribe is None:
|
|
364
|
+
return False
|
|
365
|
+
self._redescribe()
|
|
366
|
+
self.redeclared += 1
|
|
367
|
+
return True
|
|
368
|
+
|
|
369
|
+
# -- lifecycle -----------------------------------------------------------
|
|
370
|
+
def start(self) -> None:
|
|
371
|
+
"""Test every enabled sidecar before anything is declared, and bring
|
|
372
|
+
the descriptor into agreement at once rather than a cadence later."""
|
|
373
|
+
if self._endpoints():
|
|
374
|
+
self.test_all(self.clock())
|
|
375
|
+
self._every_last['_check_endpoints'] = self.clock()
|
|
376
|
+
self.redeclare_if_changed()
|
|
377
|
+
|
|
378
|
+
def _check_endpoints(self, now: Optional[int] = None) -> list[tuple[Any, ...]]:
|
|
379
|
+
now = now if now is not None else self.clock()
|
|
380
|
+
out: list[tuple[Any, ...]] = []
|
|
381
|
+
changed = False
|
|
382
|
+
for name in self._endpoints():
|
|
383
|
+
was = self._verified.get(name)
|
|
384
|
+
self.test(name, now)
|
|
385
|
+
is_ok = self._verified[name]
|
|
386
|
+
if was is not None and was != is_ok:
|
|
387
|
+
out.append(('sidecar', name, 'verified' if is_ok else 'failing'))
|
|
388
|
+
changed = changed or (was != is_ok)
|
|
389
|
+
if changed and self.redeclare_if_changed():
|
|
390
|
+
out.append(('sidecar', 'descriptor', 'redeclared'))
|
|
391
|
+
return out
|
|
392
|
+
|
|
393
|
+
_tick_runs_every = True
|
|
394
|
+
|
|
395
|
+
def tick(self, now: int) -> list[tuple[Any, ...]]:
|
|
396
|
+
out: list[tuple[Any, ...]] = []
|
|
397
|
+
if any(name not in self._last_checked for name in self._endpoints()):
|
|
398
|
+
self._every_last['_check_endpoints'] = now
|
|
399
|
+
out += self._check_endpoints(now)
|
|
400
|
+
else:
|
|
401
|
+
out += self.run_every(now)
|
|
402
|
+
changed = False
|
|
403
|
+
# A sidecar removed from the config stops being verified without a probe.
|
|
404
|
+
for name in [n for n in self._verified if n not in self._endpoints()]:
|
|
405
|
+
del self._verified[name]
|
|
406
|
+
changed = True
|
|
407
|
+
if changed and self.redeclare_if_changed():
|
|
408
|
+
out.append(('sidecar', 'descriptor', 'redeclared'))
|
|
409
|
+
return out
|
|
410
|
+
|
|
411
|
+
def _report(self, name: str) -> SidecarStatusReport:
|
|
412
|
+
return {'verified': self.verified(name), 'last_checked': self._last_checked.get(name),
|
|
413
|
+
'checks': [r._asdict() for r in self._results.get(name, [])]}
|
|
414
|
+
|
|
415
|
+
def state(self) -> Mapping[str, Any]:
|
|
416
|
+
if self._closed:
|
|
417
|
+
return {'stopped': True}
|
|
418
|
+
out: MutableMapping[str, Any] = {}
|
|
419
|
+
entries = self._entries()
|
|
420
|
+
for s in self.registry():
|
|
421
|
+
if s.name in entries:
|
|
422
|
+
out[s.name] = {'enabled': self.enabled(s.name), 'endpoint': self.block(s.name).endpoint,
|
|
423
|
+
'capability': s.capability, 'builtin': s.builtin,
|
|
424
|
+
'reachable': self._verified.get(s.name), **self._report(s.name)}
|
|
425
|
+
else:
|
|
426
|
+
out[s.name] = {'enabled': False, 'capability': s.capability, 'builtin': s.builtin}
|
|
427
|
+
for name, why in self._manifest_errors.items():
|
|
428
|
+
out[name] = {'enabled': entries[name].enabled, 'manifest_error': why}
|
|
429
|
+
out['redeclared'] = self.redeclared
|
|
430
|
+
return out
|
|
431
|
+
|
|
432
|
+
def trouble(self) -> list[str]:
|
|
433
|
+
out = []
|
|
434
|
+
eps = self._endpoints()
|
|
435
|
+
self.registry()
|
|
436
|
+
for name, why in self._manifest_errors.items():
|
|
437
|
+
out.append('%s sidecar has no usable manifest: %s' % (name, why))
|
|
438
|
+
for name, url in eps.items():
|
|
439
|
+
if self._verified.get(name) is False:
|
|
440
|
+
failed = [r for r in self._results.get(name, []) if not r.ok]
|
|
441
|
+
out.append('%s sidecar is configured at %s but not verified: %s' % (
|
|
442
|
+
name, url, '; '.join('%s (%s)' % (r.name, r.why) for r in failed) or 'untested'))
|
|
443
|
+
return out
|
|
444
|
+
|
|
445
|
+
def list_sidecars(self) -> Mapping[str, Any]:
|
|
446
|
+
return {n: {'endpoint': url, **self._report(n)} for n, url in self._endpoints().items()}
|
|
447
|
+
|
|
448
|
+
def run_sidecar_test(self, args: Mapping[str, Any]) -> Mapping[str, Any]:
|
|
449
|
+
only = args.get('name')
|
|
450
|
+
if only is not None:
|
|
451
|
+
self.manifest(only) # KeyError names the ones there are
|
|
452
|
+
if only is not None and not self.enabled(only):
|
|
453
|
+
raise KeyError('%s is not enabled' % only)
|
|
454
|
+
now = self.clock()
|
|
455
|
+
results = self.test_all(now, only)
|
|
456
|
+
redeclared = self.redeclare_if_changed()
|
|
457
|
+
return {n: {'verified': all(r.ok for r in rs), 'checks': [r._asdict() for r in rs], 'at': now}
|
|
458
|
+
for n, rs in results.items()} | {'redeclared': redeclared}
|
|
459
|
+
|
|
460
|
+
def stop(self) -> Any:
|
|
461
|
+
self._closed = True
|