From dbb0d93dc7975a93c71000f3d925751725bd279a Mon Sep 17 00:00:00 2001 From: Ilker Manap Date: Fri, 30 Oct 2026 15:13:05 +0100 Subject: [PATCH] Kea: reservations in memory when there is no hosts database Found against Kea 3.0.5 in the lab: reservation-add answers "Host database not available" on a server that keeps its configuration in a file. Then reservations go into the running configuration (operation-target memory) and config-write saves them; with a hosts database nothing changes. Co-Authored-By: Claude Opus 5.5 --- cmd/rollcall/dhcpres_kea.go | 28 ++++++++++++++++++++++------ cmd/rollcall/dhcpres_test.go | 18 +++++++++++++++--- 2 files changed, 37 insertions(+), 9 deletions(-) diff --git a/cmd/rollcall/dhcpres_kea.go b/cmd/rollcall/dhcpres_kea.go index 624abf5..05bba8c 100644 --- a/cmd/rollcall/dhcpres_kea.go +++ b/cmd/rollcall/dhcpres_kea.go @@ -2,8 +2,10 @@ package main // Kea: the control API (Kea 3.0's own HTTP control socket, or the Control // Agent before it) with the host_cmds hook. Subnet ids come from -// config-get; reservations carry user-context {"rollcall": }; -// config-write keeps reservations made in memory (no hosts database). +// config-get; reservations carry user-context {"rollcall": }. +// A server with a hosts database keeps them there; one without answers +// "Host database not available", and then they go into its running +// configuration (operation-target memory), which config-write saves. import ( "bytes" @@ -22,6 +24,7 @@ type keaProvider struct { hc *http.Client persist bool service *bool // send "service": ["dhcp4"] (a Control Agent wants it; Kea 3.0's own socket refuses it) + memory bool // no hosts database: reservations go into the running configuration } func (k *keaProvider) Name() string { return "kea" } @@ -193,7 +196,21 @@ func (k *keaProvider) Put(ctx context.Context, r dhcpRes) error { if r.Hostname != "" { res["hostname"] = r.Hostname } - _, err = k.ok(ctx, "reservation-add", map[string]any{"reservation": res}) + return k.change(ctx, "reservation-add", map[string]any{"reservation": res}) +} + +// change runs reservation-add/-del against the hosts database, or the +// configuration in memory when the server has no hosts database. +func (k *keaProvider) change(ctx context.Context, command string, args map[string]any) error { + if k.memory { + args["operation-target"] = "memory" + } + _, err := k.ok(ctx, command, args) + if err != nil && !k.memory && strings.Contains(err.Error(), "database not available") { + k.memory = true + args["operation-target"] = "memory" + _, err = k.ok(ctx, command, args) + } return err } @@ -202,12 +219,11 @@ func (k *keaProvider) Delete(ctx context.Context, r dhcpRes) error { if err != nil { return err } - _, err = k.ok(ctx, "reservation-del", map[string]any{"subnet-id": id, "identifier-type": "hw-address", "identifier": r.MAC}) - return err + return k.change(ctx, "reservation-del", map[string]any{"subnet-id": id, "identifier-type": "hw-address", "identifier": r.MAC}) } func (k *keaProvider) Done(ctx context.Context) error { - if !k.persist { + if !k.persist || !k.memory { // a hosts database keeps them by itself return nil } if _, err := k.ok(ctx, "config-write", map[string]any{}); err != nil { diff --git a/cmd/rollcall/dhcpres_test.go b/cmd/rollcall/dhcpres_test.go index 5ef1dfb..12250f7 100644 --- a/cmd/rollcall/dhcpres_test.go +++ b/cmd/rollcall/dhcpres_test.go @@ -43,6 +43,7 @@ type fakeKea struct { mu sync.Mutex agent bool noHooks bool + noHostDB bool // like Kea with a configuration file only: add/del need operation-target memory hosts []obj commands []string written int @@ -89,6 +90,13 @@ func (f *fakeKea) handler() http.Handler { return } answer(obj{"result": 0, "arguments": obj{"hosts": f.hosts}}) + case "reservation-add", "reservation-del": + if f.noHostDB && args["operation-target"] != "memory" { + answer(obj{"result": 1, "text": "Host database not available, cannot add host."}) + return + } + } + switch req.Command { case "reservation-add": res := args["reservation"].(map[string]any) for _, h := range f.hosts { @@ -122,7 +130,7 @@ func keaFor(t *testing.T, f *fakeKea) (*keaProvider, func()) { func TestKeaReservations(t *testing.T) { for _, agent := range []bool{false, true} { - f := &fakeKea{agent: agent, hosts: []obj{ + f := &fakeKea{agent: agent, noHostDB: agent, hosts: []obj{ {"hw-address": "52:54:00:aa:aa:aa", "ip-address": "10.214.0.9", "hostname": "printer"}, // someone else's {"hw-address": "52:54:00:00:00:09", "ip-address": "10.214.0.99", "user-context": obj{"rollcall": "m-old"}}, }} @@ -146,13 +154,17 @@ func TestKeaReservations(t *testing.T) { added = h } } - if dig(added, "user-context", "rollcall") != "m-1" || added["hostname"] != "fresh01" || added["subnet-id"] != float64(7) || f.written != 1 { + wantWrites := 0 // a hosts database keeps reservations by itself + if f.noHostDB { + wantWrites = 1 // in memory: config-write saves them + } + if dig(added, "user-context", "rollcall") != "m-1" || added["hostname"] != "fresh01" || added["subnet-id"] != float64(7) || f.written != wantWrites { t.Errorf("agent=%v added %v written %d", agent, added, f.written) } // a second pass changes nothing rep = dhcpResReport{} a.dhcpResWith(context.Background(), want, []string{"10.214.0.0/24"}, &rep) - if len(rep.Put)+len(rep.Deleted) != 0 || f.written != 1 { + if len(rep.Put)+len(rep.Deleted) != 0 || f.written != wantWrites { t.Errorf("agent=%v second pass %+v", agent, rep) } done()