Address order: sequential, reverse or random, per subnet and pool

- Which never-used pool address a new client gets: sequential (the
  lowest, as before), reverse or random, in the settings, a subnet or
  a single pool (FIRST-LAST:ORDER). When no never-used address is left,
  the one free the longest, whatever the order; returning clients keep
  theirs.
- A subnet's pools are used in the order listed, or one at random
  weighted by its never-used addresses. Pools now keep the order they
  are given in (no more sorting by address).
- CLI --address-order and --pool-order; --add-pool of an existing range
  changes its order in place. Web UI: per-pool order and a move-up
  button, pool and address order on the subnet and settings pages.
- Kea's allocator: random is imported. Schema migration 5.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
abrown38andClaude Opus 5.5 committed 2026-10-18 18:13:25 +01:00
1 parent 29fc991ce1
commit 8ee430276c
29 files changed
+653 -79

No files matched your search

+6 -1
View File
@@ -249,10 +249,11 @@ func settingsCmd() *cobra.Command {
auth = "no (such requests get no answer)"
}
fmt.Fprintf(w, "Authoritative:\t%s\n", auth)
fmt.Fprintf(w, "Address order:\t%s (subnets and pools can set their own)\n", s.EffectiveAddressOrder())
})
},
})
var leaseTime, ping, pingTimeout, hold, authoritative string
var leaseTime, ping, pingTimeout, hold, authoritative, addrOrder string
var opts, unsetOpts []string
set := &cobra.Command{
Use: "set",
@@ -316,6 +317,9 @@ reservation's own option of the same name wins.
not := !*a
in.NotAuthoritative = &not
}
if fl.Changed("address-order") {
in.AddressOrder = &addrOrder
}
if len(opts) > 0 || len(unsetOpts) > 0 {
var cur model.Settings
if err := c.Get(ctx(), "/settings", nil, &cur); err != nil {
@@ -344,6 +348,7 @@ reservation's own option of the same name wins.
f.StringVar(&pingTimeout, "ping-timeout", "", "how long to wait for a ping's answer (500ms)")
f.StringVar(&hold, "decline-hold", "", "how long an address in use by someone else is held back (24h)")
f.StringVar(&authoritative, "authoritative", "", "refuse requests for addresses that don't belong on the interface: on or off")
f.StringVar(&addrOrder, "address-order", "", "which never-used pool address a new client gets: sequential (the lowest), reverse (the highest) or random")
f.StringArrayVar(&opts, "option", nil, "set an option NAME=VALUE for every subnet")
f.StringArrayVar(&unsetOpts, "unset-option", nil, "remove an option")
cmd.AddCommand(set)
+60 -9
View File
@@ -17,6 +17,20 @@ func subnetPath(ref string) string {
return "/subnets/" + url.PathEscape(strings.Replace(ref, "/", "_", 1))
}
// orderText says how new addresses are chosen: "pools listed, addresses
// the settings' order".
func orderText(s *model.Subnet) string {
pools := "pools in the order listed"
if s.PoolOrder == model.PoolsRandom {
pools = "a pool at random"
}
addrs := "the settings' order"
if s.AddressOrder != "" {
addrs = s.AddressOrder
}
return pools + ", addresses " + addrs + " (unless a pool has its own)"
}
func poolsText(ps []model.Pool) string {
var out []string
for _, p := range ps {
@@ -78,6 +92,7 @@ func subnetCmd() *cobra.Command {
fmt.Fprintf(w, "Interface:\t%s\n", s.Interface)
fmt.Fprintf(w, "Prefix:\t%s (mask %s)\n", s.Prefix, model.Mask(s.Prefix))
fmt.Fprintf(w, "Pools:\t%s\n", poolsText(s.Pools))
fmt.Fprintf(w, "Order:\t%s\n", orderText(s.Subnet))
fmt.Fprintf(w, "Leased:\t%s, %d declined\n", usageText(s.Usage), s.Usage.Declined)
fmt.Fprintf(w, "Reservations:\t%d (%d with a lease now)\n", s.Usage.Reservations, s.Usage.ReservationsLeased)
lt := "the default"
@@ -96,7 +111,7 @@ func subnetCmd() *cobra.Command {
},
})
var name, iface, leaseTime, comment, prefix string
var name, iface, leaseTime, comment, prefix, poolOrder, addrOrder string
var pools, opts, addPools, removePools, unsetOpts []string
add := &cobra.Command{
Use: "add PREFIX --interface IFACE",
@@ -108,7 +123,11 @@ get its addresses.
--pool 10.0.10.100-10.0.10.199 \
--option routers=10.0.10.1 --option domain-name-servers=10.0.10.2
Pools are FIRST-LAST or a prefix; give --pool once per pool. Options are
Pools are FIRST-LAST or a prefix; give --pool once per pool. A new client
gets a never-used address: from the pools in the order given (or a pool at
random: --pool-order random), the lowest first (or --address-order reverse
or random). A pool can have its own order: --pool 10.0.10.200-10.0.10.250:random.
Options are
NAME=VALUE ('steward option list' has the names); a number works for any
other option, with its type for ones Steward doesn't know: 224:string=hi.`,
Args: cobra.ExactArgs(1),
@@ -134,6 +153,12 @@ other option, with its type for ones Steward doesn't know: 224:string=hi.`,
}
in.LeaseTime = &n
}
if poolOrder != "" {
in.PoolOrder = &poolOrder
}
if addrOrder != "" {
in.AddressOrder = &addrOrder
}
if len(opts) > 0 {
o, err := optionsArg(opts)
if err != nil {
@@ -151,7 +176,9 @@ other option, with its type for ones Steward doesn't know: 224:string=hi.`,
f := add.Flags()
f.StringVar(&iface, "interface", "", "the network interface it is served on (required)")
f.StringVar(&name, "name", "", "a name for it")
f.StringArrayVar(&pools, "pool", nil, "a pool: FIRST-LAST or a prefix (repeat for several)")
f.StringArrayVar(&pools, "pool", nil, "a pool: FIRST-LAST or a prefix, with :ORDER for its own address order (repeat for several)")
f.StringVar(&poolOrder, "pool-order", "", "which pool a new address comes from: listed (in the order given, the default) or random")
f.StringVar(&addrOrder, "address-order", "", "which address of a pool: sequential, reverse or random (default: the settings')")
f.StringVar(&leaseTime, "lease-time", "", "lease time (12h, 1d; default: the settings')")
f.StringArrayVar(&opts, "option", nil, "an option NAME=VALUE (repeat for several)")
f.StringVar(&comment, "comment", "", "a comment")
@@ -161,11 +188,13 @@ other option, with its type for ones Steward doesn't know: 224:string=hi.`,
update := &cobra.Command{
Use: "update SUBNET",
Short: "Change a subnet",
Long: `Changes a subnet. Only what is given changes. --pool replaces all pools;
--add-pool and --remove-pool change one. --option sets an option (keeping
the others), --unset-option removes one.
Long: `Changes a subnet. Only what is given changes. --pool replaces all pools,
in the order given; --add-pool adds one at the end, or changes the order
of a pool it already has; --remove-pool removes one. --option sets an
option (keeping the others), --unset-option removes one.
steward subnet update office --add-pool 10.0.10.200-10.0.10.220
steward subnet update office --address-order reverse --add-pool 10.0.10.200-10.0.10.220:random
steward subnet update office --option ntp-servers=10.0.10.2 --unset-option time-offset`,
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
@@ -191,6 +220,12 @@ the others), --unset-option removes one.
if fl.Changed("comment") {
in.Comment = &comment
}
if fl.Changed("pool-order") {
in.PoolOrder = &poolOrder
}
if fl.Changed("address-order") {
in.AddressOrder = &addrOrder
}
if fl.Changed("lease-time") {
n := 0
if leaseTime != "default" {
@@ -216,7 +251,7 @@ the others), --unset-option removes one.
}
found := false
for i, p := range list {
if pp, _ := model.ParsePool(p); pp == rp {
if pp, _ := model.ParsePool(p); pp.SameRange(rp) {
list = append(list[:i], list[i+1:]...)
found = true
break
@@ -226,7 +261,21 @@ the others), --unset-option removes one.
return fmt.Errorf("the subnet has no pool %s (it has %s)", rp, poolsText(cur.Pools))
}
}
list = append(list, addPools...)
// A pool it has already changes in place (its order).
adding:
for _, a := range addPools {
ap, err := model.ParsePool(a)
if err != nil {
return err
}
for i, p := range list {
if pp, _ := model.ParsePool(p); pp.SameRange(ap) {
list[i] = a
continue adding
}
}
list = append(list, a)
}
in.Pools = &list
}
if len(opts) > 0 || len(unsetOpts) > 0 {
@@ -252,7 +301,9 @@ the others), --unset-option removes one.
f.StringVar(&iface, "interface", "", "a new interface")
f.StringVar(&prefix, "prefix", "", "a new prefix")
f.StringArrayVar(&pools, "pool", nil, "replace the pools (repeat for several)")
f.StringArrayVar(&addPools, "add-pool", nil, "add a pool")
f.StringArrayVar(&addPools, "add-pool", nil, "add a pool (FIRST-LAST[:ORDER]), or set the order of one it has")
f.StringVar(&poolOrder, "pool-order", "", "which pool a new address comes from: listed or random")
f.StringVar(&addrOrder, "address-order", "", "which address of a pool: sequential, reverse, random or default (the settings')")
f.StringArrayVar(&removePools, "remove-pool", nil, "remove a pool")
f.StringVar(&leaseTime, "lease-time", "", "lease time (12h, 1d, or \"default\")")
f.StringArrayVar(&opts, "option", nil, "set an option NAME=VALUE")