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:
1 parent
abb2ee0fd7
commit
4fd72998ec
20 files changed
+252
-65
No files matched your search
@@ -440,6 +440,9 @@ func applyReservationInput(res *model.Reservation, in *api.ReservationInput) err
|
||||
}
|
||||
res.Options = opts
|
||||
}
|
||||
if in.LeaseTime != nil {
|
||||
res.LeaseTime = *in.LeaseTime
|
||||
}
|
||||
if in.Disabled != nil {
|
||||
res.Disabled = *in.Disabled
|
||||
}
|
||||
|
||||
@@ -122,14 +122,38 @@ func (s *Server) reserveLease(w http.ResponseWriter, r *http.Request, p *auth.Pr
|
||||
var in struct {
|
||||
Hostname *string `json:"hostname,omitempty"`
|
||||
Comment string `json:"comment,omitempty"`
|
||||
// By is how the reservation knows the client: mac, client_id or
|
||||
// both (the default when the client sent a client ID).
|
||||
By string `json:"by,omitempty"`
|
||||
}
|
||||
if err := decodeOptional(r, &in); err != nil {
|
||||
return err
|
||||
}
|
||||
l := s.dhcp.Lease(ip)
|
||||
if l == nil || l.MAC == "" {
|
||||
if l == nil || l.MAC == "" && l.ClientID == "" {
|
||||
return notFound("no lease for %s", ip)
|
||||
}
|
||||
mac, cid := l.MAC, l.ClientID
|
||||
switch strings.ReplaceAll(in.By, "-", "_") {
|
||||
case "", "both":
|
||||
case "mac":
|
||||
cid = ""
|
||||
case "client_id":
|
||||
mac = ""
|
||||
default:
|
||||
return badRequest("by %q: use mac, client_id or both", in.By)
|
||||
}
|
||||
if mac == "" && cid == "" {
|
||||
what := "client ID (option 61); reserve it by mac"
|
||||
if in.By == "mac" {
|
||||
what = "MAC address; reserve it by client_id"
|
||||
}
|
||||
return badRequest("the client of %s sent no %s", ip, what)
|
||||
}
|
||||
who := mac
|
||||
if cid != "" {
|
||||
who = strings.TrimSpace(mac + " client id " + cid)
|
||||
}
|
||||
host := l.Hostname
|
||||
if in.Hostname != nil {
|
||||
host = *in.Hostname
|
||||
@@ -137,9 +161,9 @@ func (s *Server) reserveLease(w http.ResponseWriter, r *http.Request, p *auth.Pr
|
||||
host = ""
|
||||
}
|
||||
var id int64
|
||||
return s.change(w, r, p, "reserve "+ip.String()+" for "+l.MAC, func(st *model.State, newID func() int64) error {
|
||||
return s.change(w, r, p, "reserve "+ip.String()+" for "+who, func(st *model.State, newID func() int64) error {
|
||||
id = newID()
|
||||
st.Reservations = append(st.Reservations, &model.Reservation{ID: id, MAC: l.MAC, IP: ip, Hostname: host, Options: []model.OptionValue{}, Comment: in.Comment})
|
||||
st.Reservations = append(st.Reservations, &model.Reservation{ID: id, MAC: mac, ClientID: cid, IP: ip, Hostname: host, Options: []model.OptionValue{}, Comment: in.Comment})
|
||||
return nil
|
||||
}, func(st *model.State) any {
|
||||
if res := st.ReservationByID(id); res != nil {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user