package server import ( "fmt" "net/http" "sort" "strings" "time" "steward/internal/api" "steward/internal/auth" "steward/internal/engine" "steward/internal/kea" "steward/internal/model" ) // KeaImport is the body of POST /import/kea. type KeaImport struct { // Config is the kea-dhcp4.conf text; Leases the lease files' texts // (kea-leases4.csv, and kea-leases4.csv.2 if there is one, older // first). Config string `json:"config"` Leases []string `json:"leases,omitempty"` // Interfaces gives subnets' interfaces by prefix, for subnets Kea // doesn't tie to one. Interfaces map[string]string `json:"interfaces,omitempty"` // Replace replaces Steward's configuration when it already has // subnets or reservations. Replace bool `json:"replace,omitempty"` } // importKea: POST /import/kea makes Steward's configuration Kea's, and // adds Kea's current leases (?dry_run=1 shows what it would do). func (s *Server) importKea(w http.ResponseWriter, r *http.Request, p *auth.Principal) error { if err := require(p, auth.ResConfig, true); err != nil { return err } if err := require(p, auth.ResLeases, true); err != nil { return err } var in KeaImport if err := decodeJSON(r, &in); err != nil { return err } host, _ := s.ifaces.All() res, err := kea.ParseConfig([]byte(in.Config), kea.Options{HostInterfaces: host, Interfaces: in.Interfaces}) if err != nil { return badRequest("%v", err) } var missing []string for _, sn := range res.State.Subnets { if sn.Interface == "" { missing = append(missing, sn.Prefix.String()) } } if len(missing) > 0 { return badRequest("Kea doesn't say which interface serves %s, and no interface here has an address in it; give it: --interface %s=IFACE", strings.Join(missing, ", "), missing[0]) } cur := s.eng.State() if (len(cur.Subnets) > 0 || len(cur.Reservations) > 0) && !in.Replace { return conflict("Steward already has %d subnets and %d reservations; import with replace (--replace) to replace them with Kea's", len(cur.Subnets), len(cur.Reservations)) } // A subnet with the same prefix, or a reservation for the same address, // keeps its ID, so importing again shows only what differs. Kea has no // subnet names: Steward's stay. ids, err := s.store.AllocateIDs(r.Context(), len(res.State.Subnets)+len(res.State.Reservations)) if err != nil { return err } for i, sn := range res.State.Subnets { sn.ID = ids[i] for _, old := range cur.Subnets { if old.Prefix == sn.Prefix { sn.ID, sn.Name = old.ID, old.Name if sn.Comment == "" { sn.Comment = old.Comment } } } } for i, rv := range res.State.Reservations { rv.ID = ids[len(res.State.Subnets)+i] for _, old := range cur.Reservations { if old.IP == rv.IP { rv.ID = old.ID if rv.Comment == "" { rv.Comment = old.Comment } } } } // Steward's own settings that Kea has no word for stay. res.State.Settings.PingCheck = cur.Settings.PingCheck res.State.Settings.PingTimeout = cur.Settings.PingTimeout res.State.Settings.NotAuthoritative = cur.Settings.NotAuthoritative var leases []*model.Lease skippedLeases := 0 now := time.Now() for i, text := range in.Leases { ls, err := kea.ParseLeases(strings.NewReader(text), now) if err != nil { return badRequest("lease file %d: %v", i+1, err) } leases = append(leases, ls...) } // A later file's lease for an address replaces an earlier one's. byIP := map[string]*model.Lease{} for _, l := range leases { sn := res.State.SubnetFor(l.IP) if sn == nil { skippedLeases++ continue } l.SubnetID, l.Interface = sn.ID, sn.Interface byIP[l.IP.String()] = l } leases = leases[:0] for _, l := range byIP { leases = append(leases, l) } sort.Slice(leases, func(i, j int) bool { return leases[i].IP.Less(leases[j].IP) }) if skippedLeases > 0 { res.Notes = append(res.Notes, fmt.Sprintf("%d leases are for addresses outside the imported subnets and were left out", skippedLeases)) } result, err := s.eng.Replace(r.Context(), p, "import from Kea", engine.Options{DryRun: queryBool(r, "dry_run"), Via: via(r, p)}, res.State) if err != nil { s.writeErr(w, r, err, result) return nil } if result.Applied && len(leases) > 0 { if err := s.dhcp.ImportLeases(r.Context(), leases); err != nil { return fmt.Errorf("the configuration was imported, but saving the leases failed: %w", err) } s.eng.Audit(r.Context(), p, "lease.import", "kea", "success", fmt.Sprintf("%d leases", len(leases))) } out := api.ImportResult{MutationResponse: api.MutationResponse{Applied: result.Applied, DryRun: result.DryRun, HistoryID: result.HistoryID, Changes: result.Changes, Warnings: result.Warnings}, Leases: len(leases), Notes: res.Notes, Skipped: res.Skipped} if out.Changes == nil { out.Changes = []model.Change{} } if out.Notes == nil { out.Notes = []string{} } if out.Skipped == nil { out.Skipped = []string{} } writeJSON(w, http.StatusOK, out) return nil }