Let the Reserve button pick another address of the lease's subnet
The Reserve dialog fills in the lease's address but lets it be changed;
POST /leases/{ip}/reserve takes an optional ip, steward lease reserve
takes --ip. The address must be in the lease's subnet.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
c7a64773a7
commit
1bc9f9f63a
8 files changed
+49
-12
No files matched your search
@@ -114,7 +114,8 @@ func (s *Server) deleteLease(w http.ResponseWriter, r *http.Request, p *auth.Pri
|
||||
return nil
|
||||
}
|
||||
|
||||
// reserveLease makes a lease's address the client's reservation.
|
||||
// reserveLease makes a lease's address (or another in its subnet) the
|
||||
// client's reservation.
|
||||
func (s *Server) reserveLease(w http.ResponseWriter, r *http.Request, p *auth.Principal) error {
|
||||
ip, err := leaseIP(r)
|
||||
if err != nil {
|
||||
@@ -126,6 +127,8 @@ func (s *Server) reserveLease(w http.ResponseWriter, r *http.Request, p *auth.Pr
|
||||
// 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"`
|
||||
// IP reserves another address of the lease's subnet instead.
|
||||
IP string `json:"ip,omitempty"`
|
||||
}
|
||||
if err := decodeOptional(r, &in); err != nil {
|
||||
return err
|
||||
@@ -161,10 +164,25 @@ func (s *Server) reserveLease(w http.ResponseWriter, r *http.Request, p *auth.Pr
|
||||
} else if !validate.ValidHostname(host) {
|
||||
host = ""
|
||||
}
|
||||
want := ip
|
||||
if in.IP != "" {
|
||||
if want, err = netip.ParseAddr(strings.TrimSpace(in.IP)); err != nil || !want.Is4() {
|
||||
return badRequest("ip %q: not an IPv4 address", in.IP)
|
||||
}
|
||||
}
|
||||
var id int64
|
||||
return s.change(w, r, p, "reserve "+ip.String()+" for "+who, func(st *model.State, newID func() int64) error {
|
||||
return s.change(w, r, p, "reserve "+want.String()+" for "+who, func(st *model.State, newID func() int64) error {
|
||||
if want != ip {
|
||||
if sub := st.SubnetFor(ip); sub == nil || !sub.Prefix.Contains(want) {
|
||||
where := "the lease's subnet"
|
||||
if sub != nil {
|
||||
where = sub.Prefix.String()
|
||||
}
|
||||
return badRequest("%s isn't in %s", want, where)
|
||||
}
|
||||
}
|
||||
id = newID()
|
||||
st.Reservations = append(st.Reservations, &model.Reservation{ID: id, MAC: mac, ClientID: cid, IP: ip, Hostname: host, Options: []model.OptionValue{}, Comment: in.Comment})
|
||||
st.Reservations = append(st.Reservations, &model.Reservation{ID: id, MAC: mac, ClientID: cid, IP: want, Hostname: host, Options: []model.OptionValue{}, Comment: in.Comment})
|
||||
return nil
|
||||
}, func(st *model.State) any {
|
||||
if res := st.ReservationByID(id); res != nil {
|
||||
|
||||
@@ -483,12 +483,16 @@ func TestReserveByClientID(t *testing.T) {
|
||||
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 {
|
||||
var eb0 api.ErrorBody
|
||||
if code := e.call(e.token, "POST", "/leases/"+b.String()+"/reserve", map[string]any{"ip": "10.0.10.30"}, &eb0); code != 400 {
|
||||
t.Errorf("reserve outside the subnet: %d %+v", code, eb0)
|
||||
}
|
||||
if code := e.call(e.token, "POST", "/leases/"+b.String()+"/reserve", map[string]any{"ip": "10.0.20.200", "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" {
|
||||
if rv.MAC != "" || rv.ClientID != "ff02" || rv.IP.String() != "10.0.20.200" {
|
||||
t.Errorf("by client id: %+v", rv)
|
||||
}
|
||||
var eb api.ErrorBody
|
||||
|
||||
@@ -36,18 +36,21 @@ function stateBadge(st) {
|
||||
}
|
||||
|
||||
async function reserve(l, done) {
|
||||
const ip = input({ value: l.ip, class: "mono" });
|
||||
const host = input({ value: l.hostname || "", placeholder: "host name (optional)" });
|
||||
const by = select([["both", "MAC address or client ID (either one)"], ["mac", "MAC address only"], ["client_id", "client ID only"]], "both");
|
||||
const err = h("div");
|
||||
await modal("Reserve " + l.ip, h("div", null,
|
||||
h("p", null, (l.mac || "The client") + " keeps " + l.ip + " from now on."),
|
||||
h("div", { class: "form-grid" }, h("label", null, "Host name"), host,
|
||||
h("p", null, (l.mac || "The client") + " keeps this address from now on."),
|
||||
h("div", { class: "form-grid" }, h("label", null, "Address"), ip, h("label"),
|
||||
h("div", { class: "help" }, "Another address of the same subnet moves the client there when it next renews."),
|
||||
h("label", null, "Host name"), host,
|
||||
l.client_id && l.mac ? [h("label", null, "Know it by"), by, h("label"),
|
||||
h("div", { class: "help" }, "It sent the client ID ", h("span", { class: "mono" }, l.client_id), ". With either one, it gets the address whichever system it boots.")] : null), err), [
|
||||
{ label: "Cancel", value: null },
|
||||
{ label: "Reserve", kind: "primary", onclick: async () => {
|
||||
try {
|
||||
await change("POST", "/leases/" + l.ip + "/reserve", { hostname: host.value.trim(), by: l.client_id && l.mac ? by.value : undefined });
|
||||
await change("POST", "/leases/" + l.ip + "/reserve", { ip: ip.value.trim(), hostname: host.value.trim(), by: l.client_id && l.mac ? by.value : undefined });
|
||||
done();
|
||||
return true;
|
||||
} catch (e) {
|
||||
|
||||
Reference in new issue
Block a user