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 <noreply@anthropic.com>
This commit is contained in:
1 parent
482a9d5987
commit
dbb0d93dc7
2 files changed
+37
-9
No files matched your search
@@ -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 {
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
Reference in new issue
Block a user