Server statistics, multi-view record commands, and rc.3 fixes

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>
This commit is contained in:
adamandClaude Opus 5.5 committed 2026-10-11 18:03:05 +01:00
1 parent b097e10bef
commit 7be0762f48
48 files changed
+1346 -128

No files matched your search

+77
View File
@@ -427,3 +427,80 @@ func classify(name, grammar string) (string, []string) {
}
return KindRaw, nil
}
// listParam matches an optional parameter before a statement's list, such
// as "[ port <integer> ]" in allow-transfer's grammar.
var listParam = regexp.MustCompile(`\[ ([a-z0-9-]+) `)
// WrapList adds the braces to a list value given without them, for
// statements that end in a list ("allow-transfer [ port <integer> ] {
// <address_match_element>; ... }"): "10.0.0.53" becomes "{ 10.0.0.53; }",
// "port 5353 10.0.0.53; key xfer" becomes "port 5353 { 10.0.0.53; key
// xfer; }". Elements are separated by ";", "," or new lines, or by spaces
// when they are all addresses. Values with a brace, and values of other
// statements, are returned as they are.
func (st *Statement) WrapList(v string) string {
g, _, _ := strings.Cut(st.Grammar, "\n")
open := strings.Index(g, " {")
if strings.Contains(v, "{") || open < 0 || !strings.HasSuffix(g, "; ... };") {
return v
}
params := map[string]bool{}
for _, m := range listParam.FindAllStringSubmatch(g[:open], -1) {
params[m[1]] = true
}
fields := strings.Fields(v)
i := 0
for i+1 < len(fields) && params[strings.ToLower(fields[i])] {
i += 2
}
head := strings.Join(fields[:i], " ")
rest := strings.Join(fields[i:], " ")
var elems []string
if !strings.ContainsAny(rest, ";,\n") {
elems = spaceElements(fields[i:])
} else {
for _, e := range strings.FieldsFunc(rest, func(r rune) bool { return r == ';' || r == ',' || r == '\n' }) {
if e = strings.TrimSpace(e); e != "" {
elems = append(elems, e)
}
}
}
if len(elems) == 0 {
return v
}
list := "{ " + strings.Join(elems, "; ") + "; }"
if head != "" {
return head + " " + list
}
return list
}
// spaceElements splits list elements separated by spaces: "key NAME"
// is one element, and "port N" and "tls NAME" belong to the address
// before them (forwarders { 192.0.2.1 port 53; }).
func spaceElements(fields []string) []string {
var out []string
for i := 0; i < len(fields); i++ {
f := fields[i]
switch strings.ToLower(f) {
case "key", "!key", "!":
if i+1 < len(fields) {
f += " " + fields[i+1]
i++
}
if f[0] == '!' && strings.HasPrefix(strings.ToLower(f), "! key") && i+1 < len(fields) {
f += " " + fields[i+1]
i++
}
case "port", "tls":
if len(out) > 0 && i+1 < len(fields) {
out[len(out)-1] += " " + f + " " + fields[i+1]
i++
continue
}
}
out = append(out, f)
}
return out
}
+25
View File
@@ -77,3 +77,28 @@ func TestCatalog(t *testing.T) {
t.Errorf("zone types = %s", types)
}
}
func TestWrapList(t *testing.T) {
c := Default()
for _, tc := range []struct{ scope, name, in, want string }{
{ScopeOptions, "allow-transfer", "10.0.10.53", "{ 10.0.10.53; }"},
{ScopeOptions, "allow-transfer", "localhost 10.0.10.53 key xfer ! key old !10.0.0.1", "{ localhost; 10.0.10.53; key xfer; ! key old; !10.0.0.1; }"},
{ScopeOptions, "forwarders", "1.1.1.1 port 853 tls cf 9.9.9.9", "{ 1.1.1.1 port 853 tls cf; 9.9.9.9; }"},
{ScopeOptions, "allow-transfer", "localhost; 10.0.10.53", "{ localhost; 10.0.10.53; }"},
{ScopeOptions, "allow-transfer", "port 5353 key xfer, !10.0.0.1", "port 5353 { key xfer; !10.0.0.1; }"},
{ScopeOptions, "allow-transfer", "{ any; }", "{ any; }"},
{ScopeOptions, "allow-query", "10.0.0.0/8 192.168.1.0/24", "{ 10.0.0.0/8; 192.168.1.0/24; }"},
{ScopeOptions, "forwarders", "1.1.1.1 9.9.9.9", "{ 1.1.1.1; 9.9.9.9; }"},
{ScopeOptions, "listen-on", "port 53 10.0.10.5", "port 53 { 10.0.10.5; }"},
{ScopeOptions, "recursion", "yes", "yes"},
{ScopeOptions, "directory", `"/var/cache/bind"`, `"/var/cache/bind"`},
} {
st, ok := c.Lookup(tc.scope, tc.name)
if !ok {
t.Fatalf("%s not found", tc.name)
}
if got := st.WrapList(tc.in); got != tc.want {
t.Errorf("%s %q = %q, want %q", tc.name, tc.in, got, tc.want)
}
}
}