internal/agent/run/wire_linux.go
Ref: Size: 6.4 KiB History
//go:build linux
package run
import (
"context"
"fmt"
"maps"
"slices"
"github.com/a73x/eitri/internal/agent/bootstrap"
"github.com/a73x/eitri/internal/agent/cloudhv"
"github.com/a73x/eitri/internal/agent/netenv"
"github.com/a73x/eitri/internal/agent/reconcile"
"github.com/a73x/eitri/internal/agent/serialpump"
"github.com/a73x/eitri/internal/agent/state"
)
// cloudhv.Provisioner is what serve() drives through the reconcile.Provisioner
// seam. Pin the method-set match at compile time: a drift on either side (a
// renamed Boot, a changed Capabilities signature) fails the build here rather
// than silently at the wiring call below.
var _ reconcile.Provisioner = (*cloudhv.Provisioner)(nil)
// platformProvisioner is what this host advertises to the server at join
// time — an opaque label the server stores and never interprets.
const platformProvisioner = "cloudhv"
// platform is this host's backend pair. It is a struct, not an interface:
// nothing consumes the bundle as a type, so the two seams stay independent — a
// different VMM is a change in this file alone. serve() is the contract's only
// consumer: a sibling wire_*.go must produce this same struct shape from a
// newPlatform with this same signature, and the fields must satisfy what
// serve() actually calls on them — Prov is reconcile.Provisioner; Pumps needs
// Ensure, StopAll, and the console Attach the sync client uses to service
// server-opened streams.
//
// Host networking does not appear here. It is per-VM state the provisioner
// attaches inside Boot, and host-wide setup that this file performs once at
// startup — neither is anything the platform-neutral serve() can sequence.
//
// Pumps is the console seam already wired to its lifecycle: the platform knows
// both how its console is exposed and which provisioner drives the pump, so it
// hands back a Manager rather than a raw source. One pump runs per running VM,
// started at Boot (cloudhv hook); CH runs in its own process group, so a pump
// surviving an agent restart reconnects to the still-listening serial socket
// rather than needing to be relaunched.
type platform struct {
Prov reconcile.Provisioner
Pumps *serialpump.Manager
// GuestCIDR reports the subnet this host's guests are on, asked once per
// report. Linux genuinely knows at startup — it builds the bridge — so this
// returns a constant; the poll shape costs it nothing and lets a platform
// whose OS owns the network answer late, or not at all.
GuestCIDR func() string
// HostNetworks are the named guest networks this host serves, sorted. A
// value rather than a poll, unlike GuestCIDR: the set is the agent's
// command line, so it changes only with a restart — which is why it rides
// in Hello and not in every report.
HostNetworks []string
}
// Per-platform because parseConfig is deliberately platform-neutral — every
// flag is accepted everywhere and ignored by hosts that do not run them — so a
// shared reader of cfg.BridgeCIDR would have a Mac propose a fabricated Linux
// subnet.
func proposeGuestCIDR(cfg Config) *string {
if cfg.BridgeCIDR == "" {
return nil
}
return &cfg.BridgeCIDR
}
// defaultStateDir is where a Linux agent keeps its state when nothing says
// otherwise. The unit runs as root and the agent creates the directory itself,
// so there is nothing for an operator to pre-create.
func defaultStateDir() (string, error) { return "/var/lib/eitri-agent", nil }
// lastResortGuestCIDR is used only when nothing else produced one: no persisted
// identity, no flag, and no suggestion from the control plane. It is a last
// resort and not "the default" — a host that reaches it is a host the fleet
// never advised, and two of them on one network would collide.
//
// It lives here, not in the resolver, because a fabricated subnet is only ever
// right on a platform that is about to CREATE the network it names. A host
// whose OS already owns the guest network must answer "" and go and look.
const lastResortGuestCIDR = "10.77.1.0/24"
// newPlatform builds the Linux backend: cloud-hypervisor over a bridge and tap,
// with the guest console on cloud-hypervisor's serial socket. A bare host that
// just joined has neither the hypervisor nor its UEFI firmware, so newPlatform
// installs them before anything can try to launch a VM.
func newPlatform(ctx context.Context, cfg Config, st *state.Store) (platform, error) {
bridgeCIDR := resolveGuestCIDR(st, cfg, "")
if bridgeCIDR == "" {
bridgeCIDR = lastResortGuestCIDR
persistGuestCIDR(st, bridgeCIDR)
}
net, err := netenv.New(hostRunner, bridgeCIDR, cfg.HostNetworks)
if err != nil {
return platform{}, fmt.Errorf("netenv init: %w", err)
}
// Before the NAT bridge and before any VM: a configured network whose
// bridge is missing is a host that cannot honor what it is about to
// advertise. Refuse to start, in the flag's own vocabulary.
if err := net.VerifyNetworks(); err != nil {
return platform{}, err
}
// Before anything is created: a subnet overlapping this host's own uplink
// would make its address local and stop it answering, and the only machine
// positioned to fix that is the one that just lost the route. Refusing here
// costs a startup error; not refusing costs the host.
if err := net.CheckUplinkCollision(); err != nil {
return platform{}, err
}
if err := net.EnsureBridge(ctx); err != nil {
return platform{}, fmt.Errorf("ensure bridge: %w", err)
}
// Must precede StartDHCP: the responder may not answer a surviving guest's
// renewal from an empty table.
replayReservations(st, net)
if err := net.StartDHCP(ctx); err != nil {
return platform{}, fmt.Errorf("start dhcp: %w", err)
}
// BootstrapDest maps the --ch-bin value (usually a bare $PATH name) to a
// real install path.
bs := &bootstrap.Bootstrapper{
CHPath: cloudhv.BootstrapDest(cfg.CHBin),
FirmwarePath: cfg.Firmware,
ManifestURL: cfg.BootstrapURL,
}
if err := bs.Ensure(ctx); err != nil {
return platform{}, fmt.Errorf("bootstrap runtime: %w", err)
}
prov := cloudhv.New(st, cfg.CHBin, cfg.Firmware, hostRunner, net)
pumps := serialpump.NewManager(cloudhv.ConsoleSource(st.SerialSocketPath), st.SerialLogPath)
prov.Pumps = pumps
return platform{Prov: prov, Pumps: pumps,
GuestCIDR: func() string { return bridgeCIDR },
// Sorted so every Hello from an unchanged host is byte-identical: map
// iteration order would make the advertisement churn for no reason.
HostNetworks: slices.Sorted(maps.Keys(cfg.HostNetworks))}, nil
}