Jarl keeps a BIND 9.20 server's whole configuration (views, ACLs, TSIG keys, zones of every type, records, named.conf options) in SQLite and renders named.conf, keys.conf and zone files from it. Every change goes through one changeset pipeline: whole-configuration validation, named-checkconf -z and named-checkzone in a staging directory, atomic install, rndc reconfig/reload, serial verification with rndc zonestatus, and rollback if named does not load the change. Changes apply one at a time by default or are bundled and applied together. Applied changesets can be reverted. - jarld: the daemon with the REST API and an embedded web UI, plus import, adopt, check, resync, backup and local user recovery commands - jarl: the CLI, including ACME DNS-01 hooks for certbot and acme.sh - users with argon2id passwords and CSRF-protected sessions; hashed API tokens with granular grants and roles; an audit log - systemd unit, sysusers.d/tmpfiles.d files, Makefile - unit tests, integration tests against a real named, scripts/e2e.sh - docs/ guides, README and CHANGELOG Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
736 lines
21 KiB
Go
736 lines
21 KiB
Go
package main
|
|
|
|
import (
|
|
"fmt"
|
|
"io"
|
|
"net/url"
|
|
"strings"
|
|
|
|
"github.com/spf13/cobra"
|
|
|
|
"jarl/internal/api"
|
|
"jarl/internal/model"
|
|
)
|
|
|
|
func viewCmd() *cobra.Command {
|
|
cmd := &cobra.Command{Use: "view", Aliases: []string{"views"}, Short: "Manage views"}
|
|
list := &cobra.Command{
|
|
Use: "list",
|
|
Short: "List views in match order",
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
c, err := newClient()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
var views []api.View
|
|
if err := c.Get(ctx(), "/views", nil, &views); err != nil {
|
|
return err
|
|
}
|
|
return output(views, func(w io.Writer) {
|
|
fmt.Fprintln(w, "#\tID\tVIEW\tMATCH-CLIENTS\tMATCH-DESTINATIONS\tZONES")
|
|
for i, v := range views {
|
|
fmt.Fprintf(w, "%d\t%d\t%s\t%s\t%s\t%d\n", i+1, v.ID, v.Name, orAny(v.MatchClients), orAny(v.MatchDestinations), v.Zones)
|
|
}
|
|
})
|
|
},
|
|
}
|
|
var clients, dests []string
|
|
var recursiveOnly, comment string
|
|
var adopt, cascade, yes bool
|
|
viewBody := func(cmd *cobra.Command) map[string]any {
|
|
body := map[string]any{}
|
|
f := cmd.Flags()
|
|
if f.Changed("match-clients") {
|
|
body["match_clients"] = clients
|
|
}
|
|
if f.Changed("match-destinations") {
|
|
body["match_destinations"] = dests
|
|
}
|
|
if f.Changed("recursive-only") {
|
|
switch recursiveOnly {
|
|
case "yes", "true":
|
|
body["match_recursive_only"] = true
|
|
case "no", "false":
|
|
body["match_recursive_only"] = false
|
|
default:
|
|
body["match_recursive_only"] = nil
|
|
}
|
|
}
|
|
if f.Changed("comment") {
|
|
body["comment"] = comment
|
|
}
|
|
return body
|
|
}
|
|
addViewFlags := func(c *cobra.Command) {
|
|
c.Flags().StringSliceVar(&clients, "match-clients", nil, "match-clients elements (comma separated or repeated)")
|
|
c.Flags().StringSliceVar(&dests, "match-destinations", nil, "match-destinations elements")
|
|
c.Flags().StringVar(&recursiveOnly, "recursive-only", "", "match-recursive-only: yes, no or default")
|
|
c.Flags().StringVar(&comment, "comment", "", "comment")
|
|
}
|
|
create := &cobra.Command{
|
|
Use: "create NAME",
|
|
Short: "Create a view (added after the existing views)",
|
|
Example: ` jarl view create internal --match-clients 192.168.0.0/16,localhost --adopt-zones
|
|
jarl view create external --match-clients any`,
|
|
Args: cobra.ExactArgs(1),
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
c, err := newClient()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
body := viewBody(cmd)
|
|
body["name"] = args[0]
|
|
if adopt {
|
|
body["adopt_zones"] = true
|
|
}
|
|
resp, err := c.Mutate(ctx(), "POST", "/views", nil, body)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return report(resp)
|
|
},
|
|
}
|
|
addViewFlags(create)
|
|
create.Flags().BoolVar(&adopt, "adopt-zones", false, "move zones that are not in a view into this one (required for the first view if zones exist)")
|
|
var rename string
|
|
update := &cobra.Command{
|
|
Use: "update VIEW",
|
|
Short: "Change a view",
|
|
Args: cobra.ExactArgs(1),
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
c, err := newClient()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
body := viewBody(cmd)
|
|
if rename != "" {
|
|
body["name"] = rename
|
|
}
|
|
resp, err := c.Mutate(ctx(), "PATCH", "/views/"+url.PathEscape(args[0]), nil, body)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return report(resp)
|
|
},
|
|
}
|
|
addViewFlags(update)
|
|
update.Flags().StringVar(&rename, "rename", "", "new name")
|
|
del := &cobra.Command{
|
|
Use: "delete VIEW",
|
|
Short: "Delete a view",
|
|
Args: cobra.ExactArgs(1),
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
if cascade && !confirm("Delete view "+args[0]+" with all its zones?", yes) {
|
|
return fmt.Errorf("canceled")
|
|
}
|
|
c, err := newClient()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
qv := q()
|
|
if cascade {
|
|
qv.Set("cascade", "1")
|
|
}
|
|
resp, err := c.Mutate(ctx(), "DELETE", "/views/"+url.PathEscape(args[0]), qv, nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return report(resp)
|
|
},
|
|
}
|
|
del.Flags().BoolVar(&cascade, "cascade", false, "also delete the view's zones")
|
|
del.Flags().BoolVarP(&yes, "yes", "y", false, "don't ask for confirmation")
|
|
order := &cobra.Command{
|
|
Use: "order VIEW...",
|
|
Short: "Set the order in which views are matched (list every view)",
|
|
Args: cobra.MinimumNArgs(1),
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
c, err := newClient()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
var views []api.View
|
|
if err := c.Get(ctx(), "/views", nil, &views); err != nil {
|
|
return err
|
|
}
|
|
byName := map[string]int64{}
|
|
for _, v := range views {
|
|
byName[strings.ToLower(v.Name)] = v.ID
|
|
}
|
|
var ids []int64
|
|
for _, a := range args {
|
|
id, ok := byName[strings.ToLower(a)]
|
|
if !ok {
|
|
return fmt.Errorf("no view %q", a)
|
|
}
|
|
ids = append(ids, id)
|
|
}
|
|
resp, err := c.Mutate(ctx(), "POST", "/views/order", nil, map[string]any{"ids": ids})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return report(resp)
|
|
},
|
|
}
|
|
cmd.AddCommand(list, create, update, del, order)
|
|
return cmd
|
|
}
|
|
|
|
func orAny(list []string) string {
|
|
if len(list) == 0 {
|
|
return "any"
|
|
}
|
|
return strings.Join(list, "; ")
|
|
}
|
|
|
|
func aclCmd() *cobra.Command {
|
|
cmd := &cobra.Command{Use: "acl", Aliases: []string{"acls"}, Short: "Manage ACLs (named address match lists)"}
|
|
list := &cobra.Command{
|
|
Use: "list",
|
|
Short: "List ACLs",
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
c, err := newClient()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
var acls []api.ACL
|
|
if err := c.Get(ctx(), "/acls", nil, &acls); err != nil {
|
|
return err
|
|
}
|
|
return output(acls, func(w io.Writer) {
|
|
fmt.Fprintln(w, "ID\tACL\tELEMENTS\tUSED BY")
|
|
for _, a := range acls {
|
|
fmt.Fprintf(w, "%d\t%s\t%s\t%s\n", a.ID, a.Name, strings.Join(a.Elements, "; "), strings.Join(a.UsedBy, ", "))
|
|
}
|
|
})
|
|
},
|
|
}
|
|
var comment, rename string
|
|
var add, remove []string
|
|
create := &cobra.Command{
|
|
Use: "create NAME ELEMENT...",
|
|
Short: "Create an ACL",
|
|
Example: ` jarl acl create homelab 192.168.1.0/24 10.10.0.0/16 localhost`,
|
|
Args: cobra.MinimumNArgs(1),
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
c, err := newClient()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
body := map[string]any{"name": args[0], "elements": args[1:], "comment": comment}
|
|
resp, err := c.Mutate(ctx(), "POST", "/acls", nil, body)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return report(resp)
|
|
},
|
|
}
|
|
create.Flags().StringVar(&comment, "comment", "", "comment")
|
|
var set []string
|
|
update := &cobra.Command{
|
|
Use: "update ACL",
|
|
Short: "Change an ACL (--add/--remove elements, --set all elements, --rename)",
|
|
Args: cobra.ExactArgs(1),
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
c, err := newClient()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
var cur api.ACL
|
|
if err := c.Get(ctx(), "/acls/"+url.PathEscape(args[0]), nil, &cur); err != nil {
|
|
return err
|
|
}
|
|
body := map[string]any{}
|
|
elems := cur.Elements
|
|
if cmd.Flags().Changed("set") {
|
|
elems = set
|
|
}
|
|
for _, a := range add {
|
|
elems = append(elems, a)
|
|
}
|
|
if len(remove) > 0 {
|
|
drop := map[string]bool{}
|
|
for _, r := range remove {
|
|
drop[r] = true
|
|
}
|
|
var kept []string
|
|
for _, e := range elems {
|
|
if !drop[e] {
|
|
kept = append(kept, e)
|
|
}
|
|
}
|
|
elems = kept
|
|
}
|
|
if elems == nil {
|
|
elems = []string{}
|
|
}
|
|
if cmd.Flags().Changed("set") || len(add) > 0 || len(remove) > 0 {
|
|
body["elements"] = elems
|
|
}
|
|
if rename != "" {
|
|
body["name"] = rename
|
|
}
|
|
if cmd.Flags().Changed("comment") {
|
|
body["comment"] = comment
|
|
}
|
|
if len(body) == 0 {
|
|
return fmt.Errorf("nothing to change (see --help)")
|
|
}
|
|
resp, err := c.Mutate(ctx(), "PATCH", "/acls/"+url.PathEscape(args[0]), nil, body)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return report(resp)
|
|
},
|
|
}
|
|
update.Flags().StringSliceVar(&add, "add", nil, "add elements")
|
|
update.Flags().StringSliceVar(&remove, "remove", nil, "remove elements (exact text)")
|
|
update.Flags().StringSliceVar(&set, "set", nil, "replace all elements")
|
|
update.Flags().StringVar(&rename, "rename", "", "new name (references are updated)")
|
|
update.Flags().StringVar(&comment, "comment", "", "comment")
|
|
del := &cobra.Command{
|
|
Use: "delete ACL",
|
|
Short: "Delete an ACL (refused while it is in use)",
|
|
Args: cobra.ExactArgs(1),
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
c, err := newClient()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
resp, err := c.Mutate(ctx(), "DELETE", "/acls/"+url.PathEscape(args[0]), nil, nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return report(resp)
|
|
},
|
|
}
|
|
cmd.AddCommand(list, create, update, del)
|
|
return cmd
|
|
}
|
|
|
|
func tsigCmd() *cobra.Command {
|
|
cmd := &cobra.Command{Use: "tsig", Aliases: []string{"key", "keys"}, Short: "Manage TSIG keys"}
|
|
list := &cobra.Command{
|
|
Use: "list",
|
|
Short: "List TSIG keys",
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
c, err := newClient()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
var keys []api.TSIGKey
|
|
if err := c.Get(ctx(), "/tsig-keys", nil, &keys); err != nil {
|
|
return err
|
|
}
|
|
return output(keys, func(w io.Writer) {
|
|
fmt.Fprintln(w, "ID\tKEY\tALGORITHM\tUSED BY")
|
|
for _, k := range keys {
|
|
fmt.Fprintf(w, "%d\t%s\t%s\t%s\n", k.ID, k.Name, k.Algorithm, strings.Join(k.UsedBy, ", "))
|
|
}
|
|
})
|
|
},
|
|
}
|
|
var secret, algorithm, comment string
|
|
var named bool
|
|
show := &cobra.Command{
|
|
Use: "show KEY",
|
|
Short: "Show a key and its secret",
|
|
Args: cobra.ExactArgs(1),
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
c, err := newClient()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
var k api.TSIGKey
|
|
if err := c.Get(ctx(), "/tsig-keys/"+url.PathEscape(args[0]), q("secret", "1"), &k); err != nil {
|
|
return err
|
|
}
|
|
if named {
|
|
fmt.Printf("key %q {\n\talgorithm %s;\n\tsecret %q;\n};\n", k.Name, k.Algorithm, k.Secret)
|
|
return nil
|
|
}
|
|
return output(k, func(w io.Writer) {
|
|
fmt.Fprintf(w, "Key:\t%s\nAlgorithm:\t%s\nSecret:\t%s\nUsed by:\t%s\n", k.Name, k.Algorithm, k.Secret, strings.Join(k.UsedBy, ", "))
|
|
})
|
|
},
|
|
}
|
|
show.Flags().BoolVar(&named, "named-conf", false, "print a key statement for another server's named.conf")
|
|
create := &cobra.Command{
|
|
Use: "create NAME",
|
|
Short: "Create a TSIG key (the secret is generated unless given)",
|
|
Args: cobra.ExactArgs(1),
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
c, err := newClient()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
body := map[string]any{"name": args[0], "algorithm": algorithm, "comment": comment}
|
|
if secret != "" {
|
|
body["secret"] = secret
|
|
}
|
|
resp, err := c.Mutate(ctx(), "POST", "/tsig-keys", nil, body)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := report(resp); err != nil {
|
|
return err
|
|
}
|
|
if resp.Applied && flagOutput == "table" {
|
|
var k api.TSIGKey
|
|
if jsonUnmarshal(resp.Object, &k) == nil && k.Secret != "" {
|
|
fmt.Printf("secret: %s\n", k.Secret)
|
|
}
|
|
}
|
|
return nil
|
|
},
|
|
}
|
|
create.Flags().StringVar(&algorithm, "algorithm", "hmac-sha256", "algorithm")
|
|
create.Flags().StringVar(&secret, "secret", "", "base64 secret (default: generated)")
|
|
create.Flags().StringVar(&comment, "comment", "", "comment")
|
|
rotate := &cobra.Command{
|
|
Use: "rotate KEY",
|
|
Short: "Give a key a new random secret (or --secret)",
|
|
Args: cobra.ExactArgs(1),
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
c, err := newClient()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if secret == "" {
|
|
var k api.TSIGKey
|
|
if err := c.Get(ctx(), "/tsig-keys/"+url.PathEscape(args[0]), nil, &k); err != nil {
|
|
return err
|
|
}
|
|
secret = randomSecret(k.Algorithm)
|
|
}
|
|
resp, err := c.Mutate(ctx(), "PATCH", "/tsig-keys/"+url.PathEscape(args[0]), nil, map[string]any{"secret": secret})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := report(resp); err != nil {
|
|
return err
|
|
}
|
|
fmt.Printf("new secret: %s\n", secret)
|
|
return nil
|
|
},
|
|
}
|
|
rotate.Flags().StringVar(&secret, "secret", "", "the new secret (default: generated)")
|
|
del := &cobra.Command{
|
|
Use: "delete KEY",
|
|
Short: "Delete a key (refused while it is in use)",
|
|
Args: cobra.ExactArgs(1),
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
c, err := newClient()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
resp, err := c.Mutate(ctx(), "DELETE", "/tsig-keys/"+url.PathEscape(args[0]), nil, nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return report(resp)
|
|
},
|
|
}
|
|
cmd.AddCommand(list, show, create, rotate, del)
|
|
return cmd
|
|
}
|
|
|
|
func optionCmd() *cobra.Command {
|
|
cmd := &cobra.Command{Use: "option", Aliases: []string{"options", "opt"}, Short: "Manage named options (server, view, zone, top-level)"}
|
|
var view, zone, zoneView string
|
|
var toplevel bool
|
|
scopeArgs := func(c *cobra.Command) {
|
|
c.Flags().StringVar(&view, "view", "", "options of this view")
|
|
c.Flags().StringVar(&zone, "zone", "", "options of this zone")
|
|
c.Flags().StringVar(&zoneView, "zone-view", "", "view of --zone")
|
|
c.Flags().BoolVar(&toplevel, "toplevel", false, "top-level statements (logging, controls, ...)")
|
|
}
|
|
scope := func() (string, int64, error) {
|
|
c, err := newClient()
|
|
if err != nil {
|
|
return "", 0, err
|
|
}
|
|
switch {
|
|
case zone != "":
|
|
id, err := resolveZoneID(c, zone, zoneView)
|
|
return model.ScopeZone, id, err
|
|
case view != "":
|
|
var v api.View
|
|
if err := c.Get(ctx(), "/views/"+url.PathEscape(view), nil, &v); err != nil {
|
|
return "", 0, err
|
|
}
|
|
return model.ScopeView, v.ID, nil
|
|
case toplevel:
|
|
return model.ScopeTopLevel, 0, nil
|
|
}
|
|
return model.ScopeOptions, 0, nil
|
|
}
|
|
list := &cobra.Command{
|
|
Use: "list",
|
|
Short: "List options (server options by default)",
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
c, err := newClient()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
qv := q()
|
|
if view != "" || zone != "" || toplevel || !cmd.Flags().Changed("all") {
|
|
sc, id, err := scope()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
qv = q("scope", sc, "scope_id", fmt.Sprint(id))
|
|
}
|
|
if all {
|
|
qv = q()
|
|
}
|
|
var opts []api.Option
|
|
if err := c.Get(ctx(), "/options", qv, &opts); err != nil {
|
|
return err
|
|
}
|
|
return output(opts, func(w io.Writer) {
|
|
fmt.Fprintln(w, "ID\tWHERE\tOPTION\tVALUE")
|
|
for _, o := range opts {
|
|
fmt.Fprintf(w, "%d\t%s\t%s\t%s\n", o.ID, o.Where, o.Name, o.Value)
|
|
}
|
|
})
|
|
},
|
|
}
|
|
scopeArgs(list)
|
|
list.Flags().BoolVar(&all, "all", false, "options of every scope")
|
|
var comment string
|
|
set := &cobra.Command{
|
|
Use: "set NAME [VALUE...]",
|
|
Short: "Set an option (replaces an existing option of the same name)",
|
|
Long: `Set an option; VALUE is named.conf syntax without the trailing semicolon.
|
|
|
|
jarl option set recursion no
|
|
jarl option set allow-recursion '{ homelab; localhost; }'
|
|
jarl option set forwarders '{ 1.1.1.1; 9.9.9.9; }'
|
|
jarl option set allow-transfer '{ key xfer; }' --zone example.com
|
|
jarl option set logging '{ channel q { file "/var/log/named/q.log"; }; category queries { q; }; }' --toplevel`,
|
|
Args: cobra.MinimumNArgs(1),
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
c, err := newClient()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
sc, id, err := scope()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
value := strings.Join(args[1:], " ")
|
|
var opts []api.Option
|
|
if err := c.Get(ctx(), "/options", q("scope", sc, "scope_id", fmt.Sprint(id)), &opts); err != nil {
|
|
return err
|
|
}
|
|
body := map[string]any{"value": value}
|
|
if cmd.Flags().Changed("comment") {
|
|
body["comment"] = comment
|
|
}
|
|
for _, o := range opts {
|
|
if o.Name == strings.ToLower(args[0]) && !multiple {
|
|
resp, err := c.Mutate(ctx(), "PATCH", fmt.Sprintf("/options/%d", o.ID), nil, body)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return report(resp)
|
|
}
|
|
}
|
|
body["scope"], body["scope_id"], body["name"] = sc, id, args[0]
|
|
resp, err := c.Mutate(ctx(), "POST", "/options", nil, body)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return report(resp)
|
|
},
|
|
}
|
|
scopeArgs(set)
|
|
set.Flags().StringVar(&comment, "comment", "", "comment")
|
|
set.Flags().BoolVar(&multiple, "add", false, "add another instance instead of replacing (for options that may occur several times)")
|
|
unset := &cobra.Command{
|
|
Use: "unset NAME | unset --id ID",
|
|
Short: "Remove an option (BIND's default applies again)",
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
c, err := newClient()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if optID > 0 {
|
|
resp, err := c.Mutate(ctx(), "DELETE", fmt.Sprintf("/options/%d", optID), nil, nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return report(resp)
|
|
}
|
|
if len(args) != 1 {
|
|
return fmt.Errorf("give the option name or --id")
|
|
}
|
|
sc, id, err := scope()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
var opts []api.Option
|
|
if err := c.Get(ctx(), "/options", q("scope", sc, "scope_id", fmt.Sprint(id)), &opts); err != nil {
|
|
return err
|
|
}
|
|
var matches []api.Option
|
|
for _, o := range opts {
|
|
if o.Name == strings.ToLower(args[0]) {
|
|
matches = append(matches, o)
|
|
}
|
|
}
|
|
switch len(matches) {
|
|
case 0:
|
|
return fmt.Errorf("option %s is not set here", args[0])
|
|
case 1:
|
|
default:
|
|
return fmt.Errorf("option %s is set %d times; remove one with --id", args[0], len(matches))
|
|
}
|
|
resp, err := c.Mutate(ctx(), "DELETE", fmt.Sprintf("/options/%d", matches[0].ID), nil, nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return report(resp)
|
|
},
|
|
}
|
|
scopeArgs(unset)
|
|
unset.Flags().Int64Var(&optID, "id", 0, "remove the option with this ID")
|
|
describe := &cobra.Command{
|
|
Use: "describe [NAME]",
|
|
Short: "Describe options from the BIND 9.20 catalog",
|
|
Args: cobra.MaximumNArgs(1),
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
c, err := newClient()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
cs := "options"
|
|
switch {
|
|
case toplevel:
|
|
cs = "toplevel"
|
|
case view != "":
|
|
cs = "view"
|
|
case zone != "":
|
|
cs = "zone:" + zoneType
|
|
}
|
|
var stmts []struct {
|
|
Name string `json:"name"`
|
|
Grammar string `json:"grammar"`
|
|
Kind string `json:"kind"`
|
|
Status string `json:"status"`
|
|
Description string `json:"description"`
|
|
}
|
|
if err := c.Get(ctx(), "/catalog/options", q("scope", cs), &stmts); err != nil {
|
|
return err
|
|
}
|
|
for _, s := range stmts {
|
|
if len(args) == 1 && s.Name != args[0] {
|
|
continue
|
|
}
|
|
if len(args) == 0 {
|
|
fmt.Printf("%-32s %s\n", s.Name, s.Description)
|
|
continue
|
|
}
|
|
fmt.Printf("%s (%s)%s\n %s\n\n%s\n", s.Name, s.Kind, statusText(s.Status), s.Description, s.Grammar)
|
|
return nil
|
|
}
|
|
if len(args) == 1 {
|
|
return fmt.Errorf("%s is not an option in scope %s", args[0], cs)
|
|
}
|
|
return nil
|
|
},
|
|
}
|
|
scopeArgs(describe)
|
|
describe.Flags().StringVar(&zoneType, "zone-type", "primary", "with --zone: the zone type")
|
|
cmd.AddCommand(list, set, unset, describe)
|
|
return cmd
|
|
}
|
|
|
|
var (
|
|
all bool
|
|
multiple bool
|
|
optID int64
|
|
zoneType string
|
|
)
|
|
|
|
func statusText(s string) string {
|
|
if s == "" {
|
|
return ""
|
|
}
|
|
return " [" + s + "]"
|
|
}
|
|
|
|
func settingsCmd() *cobra.Command {
|
|
cmd := &cobra.Command{Use: "settings", Short: "Show or change Jarl's defaults for new zones"}
|
|
show := &cobra.Command{
|
|
Use: "show",
|
|
Short: "Show settings",
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
c, err := newClient()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
var s model.Settings
|
|
if err := c.Get(ctx(), "/settings", nil, &s); err != nil {
|
|
return err
|
|
}
|
|
return output(s, func(w io.Writer) {
|
|
fmt.Fprintf(w, "Name servers:\t%s\nContact:\t%s\nDefault TTL:\t%d\nRefresh/retry/expire:\t%d/%d/%d\nNegative TTL:\t%d\nSerial policy:\t%s\n",
|
|
strings.Join(s.Nameservers, ", "), s.RName, s.DefaultTTL, s.Refresh, s.Retry, s.Expire, s.NegativeTTL, s.SerialPolicy)
|
|
})
|
|
},
|
|
}
|
|
var ns []string
|
|
var contact, policy string
|
|
var ttl, refresh, retry, expire, neg uint32
|
|
set := &cobra.Command{
|
|
Use: "set",
|
|
Short: "Change settings",
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
c, err := newClient()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
body := map[string]any{}
|
|
f := cmd.Flags()
|
|
if f.Changed("ns") {
|
|
body["nameservers"] = ns
|
|
}
|
|
if f.Changed("contact") {
|
|
body["rname"] = contact
|
|
}
|
|
if f.Changed("serial-policy") {
|
|
body["serial_policy"] = policy
|
|
}
|
|
for name, v := range map[string]uint32{"ttl": ttl, "refresh": refresh, "retry": retry, "expire": expire, "negative-ttl": neg} {
|
|
if f.Changed(name) {
|
|
key := map[string]string{"ttl": "default_ttl", "negative-ttl": "negative_ttl"}[name]
|
|
if key == "" {
|
|
key = name
|
|
}
|
|
body[key] = v
|
|
}
|
|
}
|
|
if len(body) == 0 {
|
|
return fmt.Errorf("nothing to change (see --help)")
|
|
}
|
|
resp, err := c.Mutate(ctx(), "PATCH", "/settings", nil, body)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return report(resp)
|
|
},
|
|
}
|
|
set.Flags().StringSliceVar(&ns, "ns", nil, "default name servers")
|
|
set.Flags().StringVar(&contact, "contact", "", "default SOA contact")
|
|
set.Flags().StringVar(&policy, "serial-policy", "", "date, increment or unixtime")
|
|
set.Flags().Uint32Var(&ttl, "ttl", 0, "default TTL")
|
|
set.Flags().Uint32Var(&refresh, "refresh", 0, "SOA refresh")
|
|
set.Flags().Uint32Var(&retry, "retry", 0, "SOA retry")
|
|
set.Flags().Uint32Var(&expire, "expire", 0, "SOA expire")
|
|
set.Flags().Uint32Var(&neg, "negative-ttl", 0, "SOA minimum")
|
|
cmd.AddCommand(show, set)
|
|
return cmd
|
|
}
|