Reverse zones: list other views' addresses, clearer wording
The zone page and jarl zone show list the addresses only other views' records use (ptr_other_views_list), with the name each would get and the view it comes from, and say plainly that they aren't missing here. The page's button reads 'Add PTR records for them in view X'. zone show lists the missing PTRs too. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
a93373687e
commit
2fa9828bcf
11 files changed
+101
-22
No files matched your search
+3
-1
@@ -52,7 +52,9 @@ All notable changes to Jarl are documented here. The format follows
|
||||
(`fill_other_views`, `?other_views=1`, a checkbox in the New reverse
|
||||
zone dialog), so one view's host names don't leak into another. When
|
||||
there is nothing else to fill, Jarl says how many there are and how to
|
||||
add them.
|
||||
add them. The page and `jarl zone show` list those addresses with the
|
||||
name each would get, and the view it comes from
|
||||
(`ptr_other_views_list`); `jarl zone show` lists the missing ones too.
|
||||
|
||||
- Records, zones and validation findings sort numeric labels by value, so
|
||||
`2.10.0.10.in-addr.arpa` comes before `101.10.0.10.in-addr.arpa` in the
|
||||
|
||||
+42
-1
@@ -8,6 +8,7 @@ import (
|
||||
"net/netip"
|
||||
"net/url"
|
||||
"os"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
@@ -100,9 +101,16 @@ func zoneCmd() *cobra.Command {
|
||||
fmt.Fprintf(w, "Network:\t%s\n", z.Prefix)
|
||||
if z.PTRMissing > 0 {
|
||||
fmt.Fprintf(w, "Missing PTRs:\t%d (addresses the view's records use here; add them: jarl zone fill-ptr %s)\n", z.PTRMissing, args[0])
|
||||
printPTRFills(w, z.PTRMissingList, z.PTRMissing)
|
||||
}
|
||||
if z.PTROtherViews > 0 {
|
||||
fmt.Fprintf(w, "Other views:\t%d addresses here only other views' records use, without a PTR record (to name them from there: jarl zone fill-ptr %s --other-views)\n", z.PTROtherViews, args[0])
|
||||
what := "addresses"
|
||||
if z.PTROtherViews == 1 {
|
||||
what = "address"
|
||||
}
|
||||
fmt.Fprintf(w, "Other views:\t%d %s with A or AAAA records only in %s, so not missing here (add PTRs for them in this view too: jarl zone fill-ptr %s --other-views)\n",
|
||||
z.PTROtherViews, what, fromViews(z.PTROtherViewsList), args[0])
|
||||
printPTRFills(w, z.PTROtherViewsList, z.PTROtherViews)
|
||||
}
|
||||
}
|
||||
if len(z.LinkedWith) > 0 {
|
||||
@@ -1174,6 +1182,39 @@ func terminalWidth() int {
|
||||
return width
|
||||
}
|
||||
|
||||
// printPTRFills lists PTR records a reverse zone lacks under zone show's
|
||||
// line about them: address -> name, and the view the name is from.
|
||||
func printPTRFills(w io.Writer, fills []api.PTRFill, total int) {
|
||||
for _, f := range fills {
|
||||
from := ""
|
||||
if f.From != "" {
|
||||
from = " (view " + f.From + ")"
|
||||
}
|
||||
fmt.Fprintf(w, "\t %s -> %s%s\n", f.Address, dnsname.Display(f.Target), from)
|
||||
}
|
||||
if total > len(fills) {
|
||||
fmt.Fprintf(w, "\t and %d more\n", total-len(fills))
|
||||
}
|
||||
}
|
||||
|
||||
// fromViews names the views PTR fills take their names from: "view
|
||||
// internal", "views internal and lab".
|
||||
func fromViews(fills []api.PTRFill) string {
|
||||
var vs []string
|
||||
for _, f := range fills {
|
||||
if f.From != "" && !slices.Contains(vs, f.From) {
|
||||
vs = append(vs, f.From)
|
||||
}
|
||||
}
|
||||
switch len(vs) {
|
||||
case 0:
|
||||
return "other views"
|
||||
case 1:
|
||||
return "view " + vs[0]
|
||||
}
|
||||
return "views " + strings.Join(vs[:len(vs)-1], ", ") + " and " + vs[len(vs)-1]
|
||||
}
|
||||
|
||||
func zoneFillPTRCmd() *cobra.Command {
|
||||
var view string
|
||||
var otherViews bool
|
||||
|
||||
+4
-1
@@ -215,7 +215,10 @@ A reverse zone created without a view goes to the private view whose
|
||||
records use its addresses. Zones come back with `prefix` (reverse zones:
|
||||
the network), `ptr_missing` (addresses in it that the view's A and AAAA
|
||||
records use, without a PTR record) and `ptr_other_views` (addresses in it
|
||||
without a PTR record that only other views' records use; not missing).
|
||||
without a PTR record that only other views' records use; not missing),
|
||||
with the first 25 of each in `ptr_missing_list` and
|
||||
`ptr_other_views_list` (`address`, `name`, `target`, and `from`, the view
|
||||
the name comes from).
|
||||
|
||||
`/serials` and `/delegation` send DNS queries from the server, and only
|
||||
when called ([operations.md](operations.md#checking-name-servers-and-the-delegation)).
|
||||
|
||||
+13
-3
@@ -147,9 +147,19 @@ zone show` shows how many as `Missing PTRs`; the API lists them in
|
||||
`ptr_missing_list`. Only the zone's own view's records count: an address
|
||||
that only another view's records use isn't missing (say internal's
|
||||
`claude.adbraun.lol` and the vps view's copy of `10.0.10.in-addr.arpa`).
|
||||
Those are shown apart, as `Other views` in `jarl zone show`
|
||||
(`ptr_other_views` in the API) and a quiet note with a **Name them from
|
||||
there** button on the page; filling never adds them unasked.
|
||||
Those are listed apart, with the name each would get: `Other views` in
|
||||
`jarl zone show`, `ptr_other_views` and `ptr_other_views_list` in the
|
||||
API, and a quiet note on the page with an **Add PTR records for them in
|
||||
view vps** button. Filling never adds them unasked. `jarl zone show`
|
||||
lists the missing ones too:
|
||||
|
||||
```
|
||||
Missing PTRs: 1 (addresses the view's records use here; add them: jarl zone fill-ptr 10.0.10.0/24)
|
||||
10.0.10.7 -> printer.adbraun.lol
|
||||
Other views: 2 addresses with A or AAAA records only in view internal, so not missing here (add PTRs for them in this view too: jarl zone fill-ptr 10.0.10.0/24 --other-views)
|
||||
10.0.10.26 -> gomft.adbraun.lol (view internal)
|
||||
10.0.10.101 -> claude.adbraun.lol (view internal)
|
||||
```
|
||||
|
||||
## Keeping PTR records in step
|
||||
|
||||
|
||||
@@ -50,8 +50,10 @@ type Zone struct {
|
||||
PTRMissingList []PTRFill `json:"ptr_missing_list,omitempty"`
|
||||
// PTROtherViews counts the addresses in it, without a PTR record,
|
||||
// that only other views' records use: not missing, but fill-ptr with
|
||||
// other_views=1 can name them from there.
|
||||
PTROtherViews int `json:"ptr_other_views,omitempty"`
|
||||
// other_views=1 adds them, naming them from there. PTROtherViewsList
|
||||
// are the first 25 (From says which view).
|
||||
PTROtherViews int `json:"ptr_other_views,omitempty"`
|
||||
PTROtherViewsList []PTRFill `json:"ptr_other_views_list,omitempty"`
|
||||
// Pending is set when listing with a changeset: add, change or delete.
|
||||
Pending string `json:"pending,omitempty"`
|
||||
}
|
||||
|
||||
@@ -489,7 +489,7 @@ func (x *expander) zoneCreate(req Request) ([]model.Op, error) {
|
||||
x.warnings = append(x.warnings, fmt.Sprintf("no A or AAAA record of %s has an address in %s, so there was nothing to fill it with", viewOrServer(x.st, vid), dnsname.Display(name)))
|
||||
}
|
||||
if !fillOther {
|
||||
if h := otherViewsHint(x.st, vid, name, moved, len(fills), "to name them from there, fill it with other views (jarl zone fill-ptr ZONE --other-views)"); h != "" {
|
||||
if h := otherViewsHint(x.st, vid, name, moved, len(fills), "to add PTR records for them in this view too: jarl zone fill-ptr ZONE --other-views"); h != "" {
|
||||
x.warnings = append(x.warnings, h)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -207,11 +207,11 @@ func otherViewsHint(st *model.State, viewID int64, zoneName string, existing []*
|
||||
if n <= 0 {
|
||||
return ""
|
||||
}
|
||||
what := "addresses"
|
||||
what, has, they := "addresses", "have", "they are"
|
||||
if n == 1 {
|
||||
what = "address"
|
||||
what, has, they = "address", "has", "it is"
|
||||
}
|
||||
return fmt.Sprintf("%d %s in %s only other views' records use; %s", n, what, dnsname.Display(zoneName), how)
|
||||
return fmt.Sprintf("%d %s in %s %s A or AAAA records only in other views, so %s not missing here; %s", n, what, dnsname.Display(zoneName), has, they, how)
|
||||
}
|
||||
|
||||
// zoneFillPTR expands zone.fill_ptr: the PTR records a reverse zone lacks.
|
||||
@@ -238,7 +238,7 @@ func (x *expander) zoneFillPTR(req Request) ([]model.Op, error) {
|
||||
if len(fills) == 0 {
|
||||
msg := fmt.Sprintf("nothing to add: every address of %s's A and AAAA records in %s has a PTR record", viewOrServer(x.st, z.ViewID), zoneDesc(x.st, z))
|
||||
if !in.OtherViews {
|
||||
if h := otherViewsHint(x.st, z.ViewID, z.Name, x.st.ZoneRecords(z.ID), 0, "to name them from there, fill with other views (--other-views)"); h != "" {
|
||||
if h := otherViewsHint(x.st, z.ViewID, z.Name, x.st.ZoneRecords(z.ID), 0, "to add PTR records for them in this view too: --other-views"); h != "" {
|
||||
msg += "; " + h
|
||||
}
|
||||
}
|
||||
|
||||
@@ -110,6 +110,8 @@ func TestPTROtherViews(t *testing.T) {
|
||||
}
|
||||
if z := e.do("GET", "/zones/10.0.10.in-addr.arpa?view=vps", e.token, nil, 200); z["ptr_missing"] != nil || z["ptr_other_views"] != float64(1) {
|
||||
t.Errorf("vps: ptr_missing %v, ptr_other_views %v", z["ptr_missing"], z["ptr_other_views"])
|
||||
} else if l, _ := z["ptr_other_views_list"].([]any); len(l) != 1 || l[0].(map[string]any)["address"] != "10.0.10.101" || l[0].(map[string]any)["target"] != "claude.adbraun.lol." || l[0].(map[string]any)["from"] != "internal" {
|
||||
t.Errorf("vps: ptr_other_views_list %v", z["ptr_other_views_list"])
|
||||
}
|
||||
e.do("POST", "/zones/10.0.10.in-addr.arpa/fill-ptr?view=vps", e.token, nil, 400)
|
||||
e.do("POST", "/zones/10.0.10.in-addr.arpa/fill-ptr?view=vps&other_views=1", e.token, nil, 201)
|
||||
|
||||
@@ -43,12 +43,19 @@ func (s *Server) presentZone(st *model.State, z *model.Zone, counts map[int64]re
|
||||
if z.Type == model.ZonePrimary {
|
||||
fills := engine.PTRFills(st, z.ViewID, z.Name, st.ZoneRecords(z.ID), false)
|
||||
out.PTRMissing = len(fills)
|
||||
if z.ViewID != 0 {
|
||||
out.PTROtherViews = len(engine.PTRFills(st, z.ViewID, z.Name, st.ZoneRecords(z.ID), true)) - len(fills)
|
||||
}
|
||||
for _, f := range fills[:min(len(fills), 25)] {
|
||||
out.PTRMissingList = append(out.PTRMissingList, api.PTRFill(f))
|
||||
}
|
||||
if z.ViewID != 0 {
|
||||
for _, f := range engine.PTRFills(st, z.ViewID, z.Name, st.ZoneRecords(z.ID), true) {
|
||||
if f.From == "" {
|
||||
continue
|
||||
}
|
||||
if out.PTROtherViews++; out.PTROtherViews <= 25 {
|
||||
out.PTROtherViewsList = append(out.PTROtherViewsList, api.PTRFill(f))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, o := range st.LinkedWith(z) {
|
||||
|
||||
@@ -73,9 +73,20 @@ export async function render(el, params, query) {
|
||||
f.from ? h("span", { class: "muted" }, " (from view " + f.from + ")") : null,
|
||||
f.others && f.others.length ? h("span", { class: "muted" }, " (also " + f.others.map(display).join(", ") + ")") : null)),
|
||||
zone.ptr_missing > (zone.ptr_missing_list || []).length ? h("li", { class: "muted" }, "and " + (zone.ptr_missing - zone.ptr_missing_list.length) + " more") : null))
|
||||
: zone.ptr_other_views && can("zones", true) ? h("div", { class: "muted small" },
|
||||
plural(zone.ptr_other_views, "address", "addresses") + " in " + zone.prefix + " only other views' records use, without a PTR record here. ",
|
||||
h("button", { class: "small", title: "Adds PTR records naming them from the other views' records, publishing those names in view " + zone.view, onclick: () => fillPTR(zone, true) }, "Name them from there")) : null;
|
||||
: null;
|
||||
// Addresses only other views have records for: not missing here, but
|
||||
// they can be added (publishing those views' names in this one).
|
||||
const others = zone.ptr_other_views_list || [];
|
||||
const fromViews = [...new Set(others.map((f) => f.from))];
|
||||
const otherNote = !zone.disabled && zone.ptr_other_views && can("zones", true) ? h("div", { class: "muted small" },
|
||||
plural(zone.ptr_other_views, "address", "addresses") + " in " + zone.prefix + " " + (zone.ptr_other_views === 1 ? "has" : "have") + " A or AAAA records only in view " + fromViews.join(", ") +
|
||||
", not in view " + zone.view + ", so " + (zone.ptr_other_views === 1 ? "it isn't" : "they aren't") + " counted as missing here. ",
|
||||
h("button", { class: "small", title: "Clients of view " + zone.view + " will then get these names, which come from view " + fromViews.join(", "), onclick: () => fillPTR(zone, true) },
|
||||
"Add PTR records for " + (zone.ptr_other_views === 1 ? "it" : "them") + " in view " + zone.view),
|
||||
h("ul", { class: "small ptr-missing" }, others.map((f) => h("li", null,
|
||||
h("span", { class: "mono" }, f.address), " → ", h("span", { class: "mono" }, display(f.target)),
|
||||
fromViews.length > 1 ? h("span", { class: "muted" }, " (view " + f.from + ")") : null)),
|
||||
zone.ptr_other_views > others.length ? h("li", { class: "muted" }, "and " + (zone.ptr_other_views - others.length) + " more") : null)) : null;
|
||||
const tabBar = h("div", { class: "tabs" }, tabs.map(([k, label]) => h("a", { href: "#/zones/" + id + "/" + k, class: k === tab ? "active" : "" }, label)));
|
||||
// The header and tabs show at once. Below them, switching tabs keeps the
|
||||
// previous tab's content (faded) until the new one is loaded, rather
|
||||
@@ -86,7 +97,7 @@ export async function render(el, params, query) {
|
||||
holder.classList.add("stale");
|
||||
body.className = "zone-body";
|
||||
body.dataset.zone = id;
|
||||
mount(el, head, banner, tabBar, holder);
|
||||
mount(el, head, banner, otherNote, tabBar, holder);
|
||||
if (tab === "records") await recordsTab(body, zone, query);
|
||||
else if (tab === "settings") await settingsTab(body, zone);
|
||||
else if (tab === "options") await optionsEditor(body, { scope: "zone", scope_id: zone.id, catalogScope: "zone:" + zone.type, title: "Zone options" });
|
||||
|
||||
+3
-2
@@ -706,10 +706,11 @@ contains "nothing to add"
|
||||
# The same network in another view: internal's hosts aren't missing there,
|
||||
# and are named from internal only when asked.
|
||||
run "the network in external too, filled" 0 $J zone create 10.99.1.0/24 --view external --fill
|
||||
contains "only other views' records use"
|
||||
contains "A or AAAA records only in other views"
|
||||
run "nothing missing there" 0 $J zone show 1.99.10.in-addr.arpa --view external
|
||||
not_contains "Missing PTRs"
|
||||
contains "^Other views: *2 addresses"
|
||||
contains "^Other views: *2 addresses with A or AAAA records only in view internal"
|
||||
contains "10.99.1.11 -> pi.revtest.test (view internal)"
|
||||
run "fill-ptr there: nothing of its own (exit 6)" 6 $J zone fill-ptr 10.99.1.0/24 --view external
|
||||
contains "--other-views"
|
||||
run "fill-ptr --other-views" 0 $J zone fill-ptr 10.99.1.0/24 --view external --other-views
|
||||
|
||||
Reference in new issue
Block a user