Added:
- Server statistics: named's memory and CPU (from /proc), cache size,
sockets and client counts against their limits (statistics channel,
rndc status), host memory and load, and free disk space. Memory and
CPU are kept over time as gauges (sum and sample count, averaged in
timelines). Statistics page Server section, jarl stats server,
GET /stats/server.
- jarl record add|set|delete|disable|enable --view a,b | all: the same
change in each view, applied as one changeset.
- Blocking can be paused until resumed (jarl block pause indefinitely,
"Until I resume", {"indefinitely": true}).
Changed:
- List options (allow-transfer, forwarders, ...) get their braces added
when given without them.
- The dashboard lists named-checkconf's warnings about the running
configuration; the CLI prints them after an apply.
- Stats page: tile and card rows fill their width, and the queries and
failed answers charts share margins so their time axes line up.
- Dashboard: options have their own tile.
- 24-hour times in the web UI.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
102 lines
2.6 KiB
Go
102 lines
2.6 KiB
Go
package main
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"strings"
|
|
|
|
"jarl/internal/api"
|
|
"jarl/internal/client"
|
|
)
|
|
|
|
// zoneViews expands a --view value for a record command: "" or one view
|
|
// as it is, "a,b" to those views, and "all" to every view that has the
|
|
// zone.
|
|
func zoneViews(c *client.Client, zone, view string) ([]string, error) {
|
|
if view != "all" && !strings.Contains(view, ",") {
|
|
return []string{view}, nil
|
|
}
|
|
if view != "all" {
|
|
var out []string
|
|
for _, v := range strings.Split(view, ",") {
|
|
if v = strings.TrimSpace(v); v != "" {
|
|
out = append(out, v)
|
|
}
|
|
}
|
|
return out, nil
|
|
}
|
|
var zones []api.Zone
|
|
if err := c.Get(ctx(), "/zones", nil, &zones); err != nil {
|
|
return nil, err
|
|
}
|
|
want := strings.ToLower(strings.TrimSuffix(zone, "."))
|
|
var out []string
|
|
for _, z := range zones {
|
|
if strings.ToLower(strings.TrimSuffix(z.Display, ".")) == want && z.View != "" {
|
|
out = append(out, z.View)
|
|
}
|
|
}
|
|
if len(out) == 0 {
|
|
return nil, fmt.Errorf("zone %s is in no view", zone)
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
// perView runs a record command once per view. With several views, the
|
|
// changes are staged into one changeset and applied together, so either
|
|
// every view gets the change or none does (with --changeset, they are
|
|
// staged there).
|
|
//
|
|
// do returns the server's response, or nil when there was nothing to do.
|
|
func perView(c *client.Client, views []string, description string, do func(c *client.Client, view string) (*api.MutationResponse, error)) error {
|
|
if len(views) == 1 || c.Changeset > 0 {
|
|
for _, v := range views {
|
|
resp, err := do(c, v)
|
|
if err != nil {
|
|
if len(views) > 1 {
|
|
err = fmt.Errorf("view %s: %w", v, err)
|
|
}
|
|
return err
|
|
}
|
|
if resp != nil {
|
|
if err := report(resp); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
var cs api.Changeset
|
|
if err := c.Do(ctx(), "POST", "/changesets", nil, map[string]any{"description": description + " (views " + strings.Join(views, ", ") + ")"}, &cs); err != nil {
|
|
return err
|
|
}
|
|
discard := func() { c.Do(ctx(), "POST", fmt.Sprintf("/changesets/%d/discard", cs.ID), nil, nil, nil) }
|
|
staging := *c
|
|
staging.Changeset, staging.DryRun = cs.ID, false
|
|
staged := 0
|
|
for _, v := range views {
|
|
resp, err := do(&staging, v)
|
|
if err != nil {
|
|
discard()
|
|
fmt.Fprintf(os.Stderr, "view %s failed, so nothing was changed in any view\n", v)
|
|
return err
|
|
}
|
|
if resp != nil {
|
|
staged++
|
|
}
|
|
}
|
|
if staged == 0 {
|
|
discard()
|
|
return nil
|
|
}
|
|
var resp api.MutationResponse
|
|
err := c.Do(ctx(), "POST", fmt.Sprintf("/changesets/%d/apply", cs.ID), nil, map[string]any{"force": c.Force, "dry_run": c.DryRun}, &resp)
|
|
if err != nil || c.DryRun {
|
|
discard()
|
|
}
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return report(&resp)
|
|
}
|