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
// Agent before it) with the host_cmds hook. Subnet ids come from
// config-get; reservations carry user-context {"rollcall": <machine>};
// config-write keeps reservations made in memory (no hosts database).
// config-get; reservations carry user-context {"rollcall": <machine>}.
// 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 {
+15 -3
View File
@@ -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()