luna2 daemon static routes

Scope — Internal design of the static-route subsystem in luna2-daemon: how the route catalog and its couplings are stored, how a node's effective routes are resolved, and how they are bound to interfaces and rendered into a node's network configuration. Written from the development branch of luna2-daemon; for developers and integrators, not end users. The operator-facing walkthrough is Static network routes.

Availability

Static routes are available from Luna 2.1u8 and higher.

1. Model: a catalog plus couplings

Routes are stored as a reusable catalog decoupled from the objects that use them, so a route body is written once and referenced many times. Two tables hold this:

  • route — the catalog. One row per named route: name (unique), destination, gateway, metric, device, comment.
  • routemap — the couplings. One row per (route, target) link: routeid, tableref (network, group or node) and tablerefid (the target's id).

A route never stores what it is attached to; a coupling never stores route detail. Editing a catalog row therefore changes every target the route is coupled to, with no duplication.

2. The Route base class

base/route.py owns the whole subsystem; COUPLE_TABLES = ['network', 'group', 'node'] is the single definition of what may carry a route. Its methods fall into three groups.

Catalogget_routes, update_route, delete_route. update_route carries only the edited fields and falls back to the stored row for the rest, validates through validate_route, and supports rename via newname. delete_route refuses while the route is still coupled (an in-use guard, as for os images): the caller must uncouple first.

Couplingscouple_route / decouple_route (stack or remove one link, idempotent), reconcile (make a target's couplings match a given name list — the inline -R/--routes path), copy_couplings (used on clone) and delete_couplings (used when a target is deleted). assigned_names and coupled_targets read the map back for show / list.

Resolutioneffective_for_node, resolve_for_node and the private binding helpers, covered in §4.

3. Coupling lifecycle

Couplings are kept in step with the objects that own them from the base layer:

Event Call File
Network / group / node changed with -R/--routes Route().reconcile(...) base/network.py, base/group.py, base/node.py
Group / node cloned Route().copy_couplings(...) base/group.py, base/node.py
Network / group / node deleted Route().delete_couplings(...) base/network.py, base/group.py, base/node.py

The route catalog itself is reached over HTTP through routes/config_route.py: GET /config/route[/<name>] to read, POST /config/route/<name> to create or update, GET /config/route/<name>/_delete to remove, and GET /config/route/<name>/<tableref>/<target>/_couple (and _decouple) to link and unlink. Each handler calls exactly one Route method, in keeping with the daemon's routes → base layering.

4. Resolving a node's effective routes

Resolution is a strict override, identical in spirit to how provision_interface is resolved: the most specific level that has any coupled routes wins outright, and lower levels are then ignored. effective_for_node walks the levels in order —

node  >  group  >  network base

— returning the first non-empty level's routes, deduplicated by (destination, metric). The network base is the set of routes coupled to any network the node's interfaces attach to; there is no merging across levels, so a node with its own routes does not also inherit its group's.

resolve_for_node is the boot-time entry point. base/boot.py calls it while assembling the node's boot data (Route().resolve_for_node(interfaces, nodeid, provision_interface)); it looks up the node's group and attached networks, calls effective_for_node, and distributes the result onto the already-built interface structures for template rendering. Two properties matter:

  • Additive. With no coupled routes the interfaces are left untouched, so a node that uses no static routes provisions exactly as before.
  • Fail-safe. The whole step is wrapped so that any error is logged and skipped rather than propagated — a fault in route resolution can never break node provisioning.

5. Binding a route to an interface

_bind_route decides which interface a route is written onto:

  1. If the route names a device, that interface is used — with BOOTIF mapping to the node's provisioning interface.
  2. Otherwise, if it has a next-hop, the interface whose own subnet contains that next-hop is chosen (_interface_for_nexthop).
  3. Failing both, it falls back to the provisioning interface.

IPv4 and IPv6 routes are separated into routes and routes_ipv6 on the interface by inspecting the destination. When a next-hop lies outside the chosen interface's own subnet (_nexthop_on_link is false), the entry is marked on_link so the kernel installs an off-link next-hop.

6. What is rendered

The bound routes / routes_ipv6 entries are rendered into the node's network configuration by the install template, per distribution:

  • Ubuntu / netplan — a routes: entry with to:, via:, and on-link: true where the next-hop is off-link.
  • RHEL family / NetworkManager — a route<n>= line in the interface keyfile. NetworkManager installs an off-link next-hop without an extra flag, so no on-link equivalent is emitted.

See Static network routes for the rendered examples and how to verify them on a booted node.

7. Boundary validation

validate_route is the single validation point, applied when a route is created or changed: the destination must be a valid CIDR network or host, at least one of {device, gateway} must be present, a gateway (when given) must be a valid IP, and a metric (when given) must be an integer. Validation lives at this boundary only; the resolution and binding code downstream trusts the stored rows.