Reservation lease time; reservations by MAC address or client ID

- A reservation can have its own lease time (--lease-time, the
  reservation dialog), over the subnet's. Schema migration 6.
- A reservation with a MAC address and a client ID matches either one
  (a dual-boot machine sends two client IDs from one MAC); before, it
  was found by MAC and then refused. A lease of the reserved address by
  the client's other identity counts as its own.
- The Leases page shows a client's client ID; reserving a lease uses
  both, or one with --by / the dialog's choice.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
abrown38andClaude Opus 5.5 committed 2026-10-18 20:26:24 +01:00
1 parent abb2ee0fd7
commit 4fd72998ec
20 files changed
+252 -65

No files matched your search

+42
View File
@@ -434,3 +434,45 @@ func TestAddressOrderAPI(t *testing.T) {
t.Errorf("bad pool order: %d", code)
}
}
func TestReserveByClientID(t *testing.T) {
e := newEnv(t)
e.call(e.token, "POST", "/subnets", api.SubnetInput{Prefix: ptr("10.0.20.0/24"), Interface: ptr("eth0.20"), Pools: &[]string{"10.0.20.100-10.0.20.110"}}, nil)
get := func(id byte) netip.Addr {
m := net.HardwareAddr{0xaa, 0, 0, 0, 0, id}
disc := dhcp4.NewRequest(dhcp4.Discover, m, 1)
disc.Set(dhcp4.OptClientID, []byte{0xff, id})
off := e.dhcp.Handle(context.Background(), "eth0.20", disc)
req := dhcp4.NewRequest(dhcp4.Request, m, 2)
req.Set(dhcp4.OptClientID, []byte{0xff, id})
req.Set(dhcp4.OptServerID, dhcp4.IP(netip.MustParseAddr("10.0.20.1")))
req.Set(dhcp4.OptRequestedIP, dhcp4.IP(off.Packet.YIAddr))
e.dhcp.Handle(context.Background(), "eth0.20", req)
return off.Packet.YIAddr
}
a, b := get(1), get(2)
var mr api.MutationResponse
if code := e.call(e.token, "POST", "/leases/"+a.String()+"/reserve", map[string]any{}, &mr); code != 201 {
t.Fatalf("reserve: %d", code)
}
var rv api.Reservation
json.Unmarshal(mr.Object, &rv)
if rv.MAC != "aa:00:00:00:00:01" || rv.ClientID != "ff01" {
t.Errorf("both: %+v", rv)
}
if code := e.call(e.token, "POST", "/leases/"+b.String()+"/reserve", map[string]any{"by": "client_id"}, &mr); code != 201 {
t.Fatalf("reserve by id: %d", code)
}
rv = api.Reservation{}
json.Unmarshal(mr.Object, &rv)
if rv.MAC != "" || rv.ClientID != "ff02" {
t.Errorf("by client id: %+v", rv)
}
var eb api.ErrorBody
if code := e.call(e.token, "PATCH", "/reservations/"+itoa(rv.ID), api.ReservationInput{LeaseTime: ptr(10)}, &eb); code != 422 || eb.Error.Problems[0].Code != "lease_time_range" {
t.Errorf("short lease time: %d %+v", code, eb)
}
if code := e.call(e.token, "PATCH", "/reservations/"+itoa(rv.ID), api.ReservationInput{LeaseTime: ptr(7 * 86400)}, nil); code != 200 || e.eng.State().ReservationByID(rv.ID).LeaseTime != 7*86400 {
t.Errorf("lease time: %d", code)
}
}