Kea: reservations in memory when there is no hosts database
CI / go (push) Successful in 41s
CI / agent (push) Successful in 2s
CI / site (push) Successful in 2s

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 <noreply@anthropic.com>
This commit is contained in:
Ilker ManapandClaude Opus 5.5 committed 2026-10-30 15:13:05 +01:00
1 parent 482a9d5987
commit dbb0d93dc7
2 files changed
+37 -9

No files matched your search

+22 -6
View File
@@ -2,8 +2,10 @@ package main
// Kea: the control API (Kea 3.0's own HTTP control socket, or the Control // 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 // Agent before it) with the host_cmds hook. Subnet ids come from
// config-get; reservations carry user-context {"rollcall": <machine>}; // config-get; reservations carry user-context {"rollcall": <machine>}.
// config-write keeps reservations made in memory (no hosts database). // 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 ( import (
"bytes" "bytes"
@@ -22,6 +24,7 @@ type keaProvider struct {
hc *http.Client hc *http.Client
persist bool persist bool
service *bool // send "service": ["dhcp4"] (a Control Agent wants it; Kea 3.0's own socket refuses it) 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" } func (k *keaProvider) Name() string { return "kea" }
@@ -193,7 +196,21 @@ func (k *keaProvider) Put(ctx context.Context, r dhcpRes) error {
if r.Hostname != "" { if r.Hostname != "" {
res["hostname"] = 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 return err
} }
@@ -202,12 +219,11 @@ func (k *keaProvider) Delete(ctx context.Context, r dhcpRes) error {
if err != nil { if err != nil {
return err return err
} }
_, err = k.ok(ctx, "reservation-del", map[string]any{"subnet-id": id, "identifier-type": "hw-address", "identifier": r.MAC}) return k.change(ctx, "reservation-del", map[string]any{"subnet-id": id, "identifier-type": "hw-address", "identifier": r.MAC})
return err
} }
func (k *keaProvider) Done(ctx context.Context) error { 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 return nil
} }
if _, err := k.ok(ctx, "config-write", map[string]any{}); err != nil { if _, err := k.ok(ctx, "config-write", map[string]any{}); err != nil {
+15 -3
View File
@@ -43,6 +43,7 @@ type fakeKea struct {
mu sync.Mutex mu sync.Mutex
agent bool agent bool
noHooks bool noHooks bool
noHostDB bool // like Kea with a configuration file only: add/del need operation-target memory
hosts []obj hosts []obj
commands []string commands []string
written int written int
@@ -89,6 +90,13 @@ func (f *fakeKea) handler() http.Handler {
return return
} }
answer(obj{"result": 0, "arguments": obj{"hosts": f.hosts}}) 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": case "reservation-add":
res := args["reservation"].(map[string]any) res := args["reservation"].(map[string]any)
for _, h := range f.hosts { for _, h := range f.hosts {
@@ -122,7 +130,7 @@ func keaFor(t *testing.T, f *fakeKea) (*keaProvider, func()) {
func TestKeaReservations(t *testing.T) { func TestKeaReservations(t *testing.T) {
for _, agent := range []bool{false, true} { 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: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"}}, {"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 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) t.Errorf("agent=%v added %v written %d", agent, added, f.written)
} }
// a second pass changes nothing // a second pass changes nothing
rep = dhcpResReport{} rep = dhcpResReport{}
a.dhcpResWith(context.Background(), want, []string{"10.214.0.0/24"}, &rep) 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) t.Errorf("agent=%v second pass %+v", agent, rep)
} }
done() done()