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 thedevelopmentbranch ofluna2-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,groupornode) andtablerefid(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.
Catalog — get_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.
Couplings — couple_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.
Resolution — effective_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:
- If the route names a device, that interface is used — with
BOOTIFmapping to the node's provisioning interface. - Otherwise, if it has a next-hop, the interface whose own subnet contains that next-hop is
chosen (
_interface_for_nexthop). - 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 withto:,via:, andon-link: truewhere 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 noon-linkequivalent 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.