- A subnet is a deny-list subnet (the default: everyone not denied) or
an allow-list subnet (only clients allowed or with a reservation);
the deny list counts in both. Entries are MAC addresses, prefixes
(aa:bb:cc:*) or client IDs. A refused client gets no answer (DENIED
in the activity log, with an Allow button) or a NAK on renewal.
steward access, the subnet's Access tab. Schema migration 7.
- The activity log keeps each message's header fields and options,
decoded into words (dhcp4.Describe): the parameter request list by
name, client IDs, lease times. ▸ on an Activity row, steward
activity show N / --details, GET /events/{id}.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
619 lines
19 KiB
Go
619 lines
19 KiB
Go
package main
|
|
|
|
import (
|
|
"fmt"
|
|
"io"
|
|
"net/url"
|
|
"strings"
|
|
|
|
"github.com/spf13/cobra"
|
|
|
|
"steward/internal/api"
|
|
"steward/internal/model"
|
|
)
|
|
|
|
// subnetPath is the API path of a subnet given by ID, name or prefix.
|
|
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 {
|
|
out = append(out, p.String())
|
|
}
|
|
if len(out) == 0 {
|
|
return "-"
|
|
}
|
|
return strings.Join(out, ", ")
|
|
}
|
|
|
|
func usageText(u api.Usage) string {
|
|
if u.PoolSize == 0 {
|
|
return "-"
|
|
}
|
|
return fmt.Sprintf("%d/%d (%d%%)", u.Leased, u.PoolSize, u.Leased*100/u.PoolSize)
|
|
}
|
|
|
|
func subnetCmd() *cobra.Command {
|
|
cmd := &cobra.Command{Use: "subnet", Aliases: []string{"subnets"}, Short: "Subnets: the networks Steward serves, each on its interface"}
|
|
cmd.AddCommand(&cobra.Command{
|
|
Use: "list", Short: "List the subnets with their interface, pools and use", Args: cobra.NoArgs,
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
c, err := newClient()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
var subs []api.Subnet
|
|
if err := c.Get(ctx(), "/subnets", nil, &subs); err != nil {
|
|
return err
|
|
}
|
|
return output(subs, func(w io.Writer) {
|
|
fmt.Fprintln(w, "ID\tNAME\tINTERFACE\tPREFIX\tPOOLS\tLEASED\tRESERVED\tLEASE TIME\tSTATE")
|
|
for _, s := range subs {
|
|
state := "on"
|
|
if s.Disabled {
|
|
state = "disabled"
|
|
}
|
|
fmt.Fprintf(w, "%d\t%s\t%s\t%s\t%s\t%s\t%d\t%s\t%s\n", s.ID, orDash(s.Name), s.Interface, s.Prefix, poolsText(s.Pools), usageText(s.Usage),
|
|
s.Usage.Reservations, fmtSeconds(s.LeaseTime), state)
|
|
}
|
|
})
|
|
},
|
|
})
|
|
cmd.AddCommand(&cobra.Command{
|
|
Use: "show SUBNET", Short: "Show a subnet (by ID, name or prefix)", Args: cobra.ExactArgs(1),
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
c, err := newClient()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
var s api.Subnet
|
|
if err := c.Get(ctx(), subnetPath(args[0]), nil, &s); err != nil {
|
|
return err
|
|
}
|
|
return output(s, func(w io.Writer) {
|
|
fmt.Fprintf(w, "ID:\t%d\n", s.ID)
|
|
fmt.Fprintf(w, "Name:\t%s\n", orDash(s.Name))
|
|
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, "Access:\t%s\n", accessText(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"
|
|
if s.LeaseTime > 0 {
|
|
lt = fmtSeconds(s.LeaseTime)
|
|
}
|
|
fmt.Fprintf(w, "Lease time:\t%s\n", lt)
|
|
fmt.Fprintln(w, "Options:\t"+optionsText(s.Options))
|
|
if s.Disabled {
|
|
fmt.Fprintln(w, "State:\tdisabled (no answers)")
|
|
}
|
|
if s.Comment != "" {
|
|
fmt.Fprintf(w, "Comment:\t%s\n", s.Comment)
|
|
}
|
|
})
|
|
},
|
|
})
|
|
|
|
var name, iface, leaseTime, comment, prefix, poolOrder, addrOrder string
|
|
var pools, opts, addPools, removePools, unsetOpts []string
|
|
add := &cobra.Command{
|
|
Use: "add PREFIX --interface IFACE",
|
|
Short: "Add a subnet",
|
|
Long: `Adds a subnet, served on one interface: only clients on that interface
|
|
get its addresses.
|
|
|
|
steward subnet add 10.0.10.0/24 --interface eth0 --name office \
|
|
--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. 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),
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
c, err := newClient()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
in := api.SubnetInput{Prefix: &args[0], Interface: &iface, Pools: &pools}
|
|
if pools == nil {
|
|
in.Pools = &[]string{}
|
|
}
|
|
if name != "" {
|
|
in.Name = &name
|
|
}
|
|
if comment != "" {
|
|
in.Comment = &comment
|
|
}
|
|
if leaseTime != "" {
|
|
n, err := parseSeconds(leaseTime)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
in.LeaseTime = &n
|
|
}
|
|
if poolOrder != "" {
|
|
in.PoolOrder = &poolOrder
|
|
}
|
|
if addrOrder != "" {
|
|
in.AddressOrder = &addrOrder
|
|
}
|
|
if len(opts) > 0 {
|
|
o, err := optionsArg(opts)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
in.Options = &o
|
|
}
|
|
var resp api.MutationResponse
|
|
if err := c.Do(ctx(), "POST", "/subnets", nil, in, &resp); err != nil {
|
|
return err
|
|
}
|
|
return report(&resp)
|
|
},
|
|
}
|
|
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, 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")
|
|
add.MarkFlagRequired("interface")
|
|
cmd.AddCommand(add)
|
|
|
|
update := &cobra.Command{
|
|
Use: "update SUBNET",
|
|
Short: "Change a subnet",
|
|
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 {
|
|
c, err := newClient()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
var cur api.Subnet
|
|
if err := c.Get(ctx(), subnetPath(args[0]), nil, &cur); err != nil {
|
|
return err
|
|
}
|
|
in := api.SubnetInput{}
|
|
fl := cmd.Flags()
|
|
if fl.Changed("name") {
|
|
in.Name = &name
|
|
}
|
|
if fl.Changed("interface") {
|
|
in.Interface = &iface
|
|
}
|
|
if fl.Changed("prefix") {
|
|
in.Prefix = &prefix
|
|
}
|
|
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" {
|
|
if n, err = parseSeconds(leaseTime); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
in.LeaseTime = &n
|
|
}
|
|
if fl.Changed("pool") || len(addPools) > 0 || len(removePools) > 0 {
|
|
list := []string{}
|
|
if !fl.Changed("pool") {
|
|
for _, p := range cur.Pools {
|
|
list = append(list, p.String())
|
|
}
|
|
} else {
|
|
list = append(list, pools...)
|
|
}
|
|
for _, r := range removePools {
|
|
rp, err := model.ParsePool(r)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
found := false
|
|
for i, p := range list {
|
|
if pp, _ := model.ParsePool(p); pp.SameRange(rp) {
|
|
list = append(list[:i], list[i+1:]...)
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
if !found {
|
|
return fmt.Errorf("the subnet has no pool %s (it has %s)", rp, poolsText(cur.Pools))
|
|
}
|
|
}
|
|
// 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 {
|
|
set, err := optionsArg(opts)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
merged, err := mergeOptions(cur.Options, set, unsetOpts)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
in.Options = &merged
|
|
}
|
|
var resp api.MutationResponse
|
|
if err := c.Do(ctx(), "PATCH", subnetPath(args[0]), nil, in, &resp); err != nil {
|
|
return err
|
|
}
|
|
return report(&resp)
|
|
},
|
|
}
|
|
f = update.Flags()
|
|
f.StringVar(&name, "name", "", "a new name (\"\" for none)")
|
|
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 (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")
|
|
f.StringArrayVar(&unsetOpts, "unset-option", nil, "remove an option (by name or number)")
|
|
f.StringVar(&comment, "comment", "", "a comment")
|
|
cmd.AddCommand(update)
|
|
|
|
for _, on := range []bool{true, false} {
|
|
on := on
|
|
verb := "disable"
|
|
short := "Stop answering a subnet's clients (its configuration and leases stay)"
|
|
if on {
|
|
verb, short = "enable", "Answer a disabled subnet's clients again"
|
|
}
|
|
cmd.AddCommand(&cobra.Command{
|
|
Use: verb + " SUBNET", Short: short, Args: cobra.ExactArgs(1),
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
c, err := newClient()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
disabled := !on
|
|
var resp api.MutationResponse
|
|
if err := c.Do(ctx(), "PATCH", subnetPath(args[0]), nil, api.SubnetInput{Disabled: &disabled}, &resp); err != nil {
|
|
return err
|
|
}
|
|
return report(&resp)
|
|
},
|
|
})
|
|
}
|
|
|
|
var yes bool
|
|
del := &cobra.Command{
|
|
Use: "delete SUBNET", Short: "Delete a subnet and its reservations", Args: cobra.ExactArgs(1),
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
c, err := newClient()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
var cur api.Subnet
|
|
if err := c.Get(ctx(), subnetPath(args[0]), nil, &cur); err != nil {
|
|
return err
|
|
}
|
|
msg := fmt.Sprintf("Delete subnet %s", cur.Label())
|
|
if cur.Usage.Reservations > 0 {
|
|
msg += fmt.Sprintf(" and its %d reservations", cur.Usage.Reservations)
|
|
}
|
|
if !confirm(msg+"?", yes) {
|
|
return fmt.Errorf("not deleted")
|
|
}
|
|
var resp api.MutationResponse
|
|
if err := c.Do(ctx(), "DELETE", subnetPath(args[0]), nil, nil, &resp); err != nil {
|
|
return err
|
|
}
|
|
return report(&resp)
|
|
},
|
|
}
|
|
del.Flags().BoolVarP(&yes, "yes", "y", false, "don't ask")
|
|
cmd.AddCommand(del)
|
|
return cmd
|
|
}
|
|
|
|
func reservationCmd() *cobra.Command {
|
|
cmd := &cobra.Command{Use: "reservation", Aliases: []string{"reservations", "res"}, Short: "Reservations: fixed addresses for clients"}
|
|
var subnet string
|
|
list := &cobra.Command{
|
|
Use: "list", Short: "List reservations", Args: cobra.NoArgs,
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
c, err := newClient()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
var rs []api.Reservation
|
|
if err := c.Get(ctx(), "/reservations", q("subnet", subnet), &rs); err != nil {
|
|
return err
|
|
}
|
|
return output(rs, func(w io.Writer) {
|
|
fmt.Fprintln(w, "ID\tIP\tMAC / CLIENT ID\tHOSTNAME\tSUBNET\tLEASE\tSTATE\tCOMMENT")
|
|
for _, r := range rs {
|
|
who := r.MAC
|
|
if r.ClientID != "" {
|
|
who = strings.TrimPrefix(r.MAC+" or id "+r.ClientID, " or ")
|
|
}
|
|
lease := "-"
|
|
if r.Lease != nil {
|
|
lease = "until " + r.Lease.End.Local().Format("Jan 2 15:04")
|
|
if r.Lease.LastSeen != nil {
|
|
lease += ", seen " + fmtWhen(*r.Lease.LastSeen)
|
|
}
|
|
}
|
|
state := "on"
|
|
if r.Disabled {
|
|
state = "disabled"
|
|
}
|
|
fmt.Fprintf(w, "%d\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n", r.ID, r.IP, who, orDash(r.Hostname), orDash(r.SubnetName), lease, state, r.Comment)
|
|
}
|
|
})
|
|
},
|
|
}
|
|
list.Flags().StringVar(&subnet, "subnet", "", "only this subnet's (ID, name or prefix)")
|
|
cmd.AddCommand(list)
|
|
cmd.AddCommand(&cobra.Command{
|
|
Use: "show RESERVATION", Short: "Show a reservation (by ID, address or MAC address)", Args: cobra.ExactArgs(1),
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
c, err := newClient()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
var r api.Reservation
|
|
if err := c.Get(ctx(), "/reservations/"+url.PathEscape(args[0]), nil, &r); err != nil {
|
|
return err
|
|
}
|
|
return output(r, func(w io.Writer) {
|
|
fmt.Fprintf(w, "ID:\t%d\n", r.ID)
|
|
fmt.Fprintf(w, "Address:\t%s (subnet %s)\n", r.IP, orDash(r.SubnetName))
|
|
if r.MAC != "" {
|
|
fmt.Fprintf(w, "MAC address:\t%s\n", r.MAC)
|
|
}
|
|
if r.ClientID != "" {
|
|
fmt.Fprintf(w, "Client ID:\t%s\n", r.ClientID)
|
|
}
|
|
fmt.Fprintf(w, "Host name:\t%s\n", orDash(r.Hostname))
|
|
fmt.Fprintln(w, "Options:\t"+optionsText(r.Options))
|
|
lt := "the subnet's"
|
|
if r.LeaseTime > 0 {
|
|
lt = fmtSeconds(r.LeaseTime)
|
|
}
|
|
fmt.Fprintf(w, "Lease time:\t%s\n", lt)
|
|
if r.Lease != nil {
|
|
fmt.Fprintf(w, "Lease:\tuntil %s, on %s\n", r.Lease.End.Local().Format("2006-01-02 15:04"), r.Lease.Interface)
|
|
if r.Lease.LastSeen != nil {
|
|
fmt.Fprintf(w, "Last seen:\t%s (answered a ping)\n", fmtWhen(*r.Lease.LastSeen))
|
|
}
|
|
} else {
|
|
fmt.Fprintln(w, "Lease:\tnone now")
|
|
}
|
|
if r.Disabled {
|
|
fmt.Fprintln(w, "State:\tdisabled (ignored: the client gets a pool address like any other)")
|
|
}
|
|
if r.Comment != "" {
|
|
fmt.Fprintf(w, "Comment:\t%s\n", r.Comment)
|
|
}
|
|
})
|
|
},
|
|
})
|
|
for _, on := range []bool{true, false} {
|
|
on := on
|
|
verb, short := "disable", "Ignore a reservation, keeping it (the client gets a pool address like any other)"
|
|
if on {
|
|
verb, short = "enable", "Use a disabled reservation again"
|
|
}
|
|
cmd.AddCommand(&cobra.Command{
|
|
Use: verb + " RESERVATION", Short: short, Args: cobra.ExactArgs(1),
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
c, err := newClient()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
disabled := !on
|
|
var resp api.MutationResponse
|
|
if err := c.Do(ctx(), "PATCH", "/reservations/"+url.PathEscape(args[0]), nil, api.ReservationInput{Disabled: &disabled}, &resp); err != nil {
|
|
return err
|
|
}
|
|
return report(&resp)
|
|
},
|
|
})
|
|
}
|
|
|
|
var hostname, clientID, comment, ip, mac, leaseTime string
|
|
var opts, unsetOpts []string
|
|
add := &cobra.Command{
|
|
Use: "add IP MAC",
|
|
Short: "Reserve an address for a client",
|
|
Long: `Reserves an address for a client, known by its MAC address, its client
|
|
identifier (option 61: give "-" for the MAC and --client-id), or both:
|
|
then either one gets it the address, as for a machine that boots two
|
|
systems. The address picks the subnet. An address inside a pool is fine:
|
|
it is never handed out to anyone else.
|
|
|
|
steward reservation add 10.0.10.20 aa:bb:cc:dd:ee:01 --hostname nas --lease-time 7d`,
|
|
Args: cobra.ExactArgs(2),
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
c, err := newClient()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
in := api.ReservationInput{IP: &args[0]}
|
|
if args[1] != "-" {
|
|
in.MAC = &args[1]
|
|
}
|
|
if clientID != "" {
|
|
in.ClientID = &clientID
|
|
}
|
|
if hostname != "" {
|
|
in.Hostname = &hostname
|
|
}
|
|
if comment != "" {
|
|
in.Comment = &comment
|
|
}
|
|
if leaseTime != "" {
|
|
n, err := parseSeconds(leaseTime)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
in.LeaseTime = &n
|
|
}
|
|
if len(opts) > 0 {
|
|
o, err := optionsArg(opts)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
in.Options = &o
|
|
}
|
|
var resp api.MutationResponse
|
|
if err := c.Do(ctx(), "POST", "/reservations", nil, in, &resp); err != nil {
|
|
return err
|
|
}
|
|
return report(&resp)
|
|
},
|
|
}
|
|
f := add.Flags()
|
|
f.StringVar(&hostname, "hostname", "", "the host name to send it")
|
|
f.StringVar(&clientID, "client-id", "", "the client identifier (hex): with a MAC address, either one matches")
|
|
f.StringVar(&leaseTime, "lease-time", "", "this client's lease time (12h, 7d; default: the subnet's)")
|
|
f.StringArrayVar(&opts, "option", nil, "an option NAME=VALUE for this client only")
|
|
f.StringVar(&comment, "comment", "", "a comment")
|
|
cmd.AddCommand(add)
|
|
|
|
update := &cobra.Command{
|
|
Use: "update RESERVATION", Short: "Change a reservation (only what is given changes)", Args: cobra.ExactArgs(1),
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
c, err := newClient()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
path := "/reservations/" + url.PathEscape(args[0])
|
|
var cur api.Reservation
|
|
if err := c.Get(ctx(), path, nil, &cur); err != nil {
|
|
return err
|
|
}
|
|
in := api.ReservationInput{}
|
|
fl := cmd.Flags()
|
|
if fl.Changed("ip") {
|
|
in.IP = &ip
|
|
}
|
|
if fl.Changed("mac") {
|
|
in.MAC = &mac
|
|
}
|
|
if fl.Changed("client-id") {
|
|
in.ClientID = &clientID
|
|
}
|
|
if fl.Changed("hostname") {
|
|
in.Hostname = &hostname
|
|
}
|
|
if fl.Changed("comment") {
|
|
in.Comment = &comment
|
|
}
|
|
if fl.Changed("lease-time") {
|
|
n := 0
|
|
if leaseTime != "default" {
|
|
if n, err = parseSeconds(leaseTime); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
in.LeaseTime = &n
|
|
}
|
|
if len(opts) > 0 || len(unsetOpts) > 0 {
|
|
set, err := optionsArg(opts)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
merged, err := mergeOptions(cur.Options, set, unsetOpts)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
in.Options = &merged
|
|
}
|
|
var resp api.MutationResponse
|
|
if err := c.Do(ctx(), "PATCH", path, nil, in, &resp); err != nil {
|
|
return err
|
|
}
|
|
return report(&resp)
|
|
},
|
|
}
|
|
f = update.Flags()
|
|
f.StringVar(&ip, "ip", "", "a new address")
|
|
f.StringVar(&mac, "mac", "", "a new MAC address (\"\" for none)")
|
|
f.StringVar(&clientID, "client-id", "", "a client identifier (\"\" for none)")
|
|
f.StringVar(&hostname, "hostname", "", "a new host name")
|
|
f.StringVar(&leaseTime, "lease-time", "", "lease time (12h, 7d, or \"default\": the subnet's)")
|
|
f.StringArrayVar(&opts, "option", nil, "set an option NAME=VALUE")
|
|
f.StringArrayVar(&unsetOpts, "unset-option", nil, "remove an option")
|
|
f.StringVar(&comment, "comment", "", "a comment")
|
|
cmd.AddCommand(update)
|
|
|
|
cmd.AddCommand(&cobra.Command{
|
|
Use: "delete RESERVATION", Short: "Delete a reservation", Args: cobra.ExactArgs(1),
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
c, err := newClient()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
var resp api.MutationResponse
|
|
if err := c.Do(ctx(), "DELETE", "/reservations/"+url.PathEscape(args[0]), nil, nil, &resp); err != nil {
|
|
return err
|
|
}
|
|
return report(&resp)
|
|
},
|
|
})
|
|
return cmd
|
|
}
|