chiark / gitweb /
zoneconf: Awful kludge in `primary': masters never considered local.
[zoneconf] / zoneconf.in
CommitLineData
a37c695a 1#! @TCLSH@
2a65b7cf
MW
2### -*-tcl-*-
3###
4### Generate `named.conf' stanze for multiple views.
5###
6### (c) 2011 Mark Wooding
7###
8
9###----- Licensing notice ---------------------------------------------------
10###
11### This program is free software; you can redistribute it and/or modify
12### it under the terms of the GNU General Public License as published by
13### the Free Software Foundation; either version 2 of the License, or
14### (at your option) any later version.
15###
16### This program is distributed in the hope that it will be useful,
17### but WITHOUT ANY WARRANTY; without even the implied warranty of
18### MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
19### GNU General Public License for more details.
20###
21### You should have received a copy of the GNU General Public License
22### along with this program; if not, write to the Free Software Foundation,
23### Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
24
25###--------------------------------------------------------------------------
26### Utility functions.
27
28proc pushnew {var args} {
29 ## Append each of the ARGS onto the list VAR if they're not there already.
30
31 upvar 1 $var list
32 foreach item $list { set found($item) t }
33 foreach item $args {
34 if {![info exists found($item)]} {
35 lappend list $item
36 set found($item) t
37 }
38 }
39}
40
41proc merge-lists {lists} {
42 ## Merge the given LISTS into a single list, respecting the order of the
43 ## items in the original list. If that's not possible, signal an error.
44 ## Any ambiguity is resolved by choosing the item from the earlier list.
45
46 ## Strip out any empty lists in the input.
47 set nlists {}
48 foreach list $lists {
49 if {[llength $list]} { lappend nlists $list }
50 }
51 set lists $nlists
52
53 ## Clear the output list.
54 set output {}
55
56 ## Now pick out items one by one.
57 while {[llength $lists]} {
58
59 ## Find the candidate items
60 set cand {}
61 foreach list $lists { pushnew cand [lindex $list 0] }
62
2042733d 63 ## Remove candidate items which are not first in some other list.
2a65b7cf
MW
64 set ncand {}
65 foreach cand $cand {
66 foreach list $lists {
2042733d 67 if {[lsearch -exact $list $cand] <= 0} { lappend ncand $cand }
2a65b7cf
MW
68 }
69 }
2042733d 70 set cand $ncand
2a65b7cf
MW
71
72 ## If there's nothing left, report an error.
73 if {![llength $cand]} {
74 error "Inconsistent lists in `merge-lists'."
75 }
76
77 ## Otherwise take the first item.
78 set chosen [lindex $cand 0]
79 lappend output $chosen
80
81 ## Remove the chosen item from the input lists.
82 set nlists {}
83 foreach list $lists {
84 if {[string equal $chosen [lindex $list 0]]} {
85 set list [lrange $list 1 end]
86 if {![llength $list]} { continue }
87 }
88 lappend nlists $list
89 }
90 set lists $nlists
91 }
92
93 return $output
94}
95
96proc adjust-uplevel {spec offset} {
97 ## Adjust an `uplevel' SPEC by OFFSET to take account of intervening call
98 ## frames. If SPEC begins with `#' then it is left alone; otherwise it is
99 ## incremented by OFFSET.
100
101 switch -glob -- $spec {
102 \#* { return $spec }
103 default { return [expr {$spec + $offset}] }
104 }
105}
106
107proc unwind-protect {body cleanup} {
108 ## Evaluate BODY; then evaluate CLEANUP, regardless of whether BODY
109 ## returned normally or did something complicated. If CLEANUP completes
110 ## normally then the final result is that of BODY (including any errors or
111 ## abnormal returns it made); otherwise the result of CLEANUP takes
112 ## precedence and the results of BODY are discarded.
113
114 catch { uplevel 1 $body } bodyval bodyopts
115 if {[catch { uplevel 1 $cleanup } cleanval cleanopts]} {
116 return -options $cleanopts $cleanval
117 } else {
118 return -options $bodyopts $bodyval
119 }
120}
121
122proc let {args} {
123 ## Syntax: let VAR VALUE ... BODY
124 ##
125 ## Evaluate BODY with the VARs bound to the VALUEs. Reestore the previous
126 ## values when the BODY returns.
127
128 ## Parse the argument syntax.
129 if {[llength $args] % 2 == 0} {
130 error "bad number of arguments to `let'"
131 }
132 set body [lindex $args end]
133
134 ## Now work through the bindings, setting the variables to their new
135 ## values. As we go, also build up code in `cleanup' to restore everything
136 ## the way it's meant to be.
137 set cleanup {}
138 set i 0
139 foreach {var value} [lrange $args 0 end-1] {
140 upvar 1 $var fluid-$i
141 if {[info exists fluid-$i]} {
142 append cleanup "set fluid-$i [list [set fluid-$i]]\n"
143 } else {
144 append cleanup "unset fluid-$i\n"
145 }
146 set fluid-$i $value
147 incr i
148 }
149
150 ## Now evaluate the body.
151 unwind-protect { uplevel 1 $body } $cleanup
152}
153
154proc set* {names values} {
155 ## Set each of the variables listed in NAMES to the corresponding element
156 ## of VALUES. The two lists must have the same length.
157
158 if {[llength $names] != [llength $values]} {
159 error "length mismatch"
160 }
161 foreach name $names value $values {
162 upvar 1 $name var
163 set var $value
164 }
165}
166
400655ac
MW
167proc run {what command args} {
168 ## Run a command, reporting the result. WHAT is shown in the output;
169 ## COMMAND are the command and arguments as a list; these are substituted
170 ## according to the string map ARGS. Return true if the command succeeded,
171 ## false if it failed.
172
173 global QUIS
174
175 ## Substitute tokens in the command.
176 set cmd {}
177 set subst [concat [list "%%" "%"] $args]
178 foreach item $command { lappend cmd [string map $subst $item] }
179
180 ## Run the command.
181 set rc [catch {
182 set out [eval exec -ignorestderr $cmd 2>@1]
183 } msg]
184
185 ## Sort out the report.
186 if {$rc} { set out $msg }
187 set out "| [string map [list "\n" "\n| "] $out]"
188
189 ## Announce the result.
190 if {$rc} {
191 puts stderr "$QUIS: $what failed..."
192 puts stderr $out
193 return false
194 } else {
195 puts "$QUIS: $what output..."
196 puts $out
197 return true
198 }
199}
200
2a65b7cf
MW
201###--------------------------------------------------------------------------
202### Configuration spaces.
203###
204### A configuration space is essentially a collection of Tcl commands and a
205### global array which the commands act on. The commands live in their own
206### namespace and their availability can be altered by modifying the
207### namespace path. The basic idea is to support a structured configuration
208### language with short directive names and where the available directives
209### varies in a context-sensitive manner.
210###
211### A configuration space can include other spaces, and they can include
212### further spaces. The graph of inclusions must be acyclic; further, since
213### the available commands are determined using the C3 linearization
214### algorithm, the relation in which a space precedes the spaces it includes,
215### and a space A precedes another space B if a third space includes A before
216### B, must be a partial order, and the linearizations of all of the spaces
217### must be monotonic. Don't worry about that if you don't know what it
218### means. If you don't do anything weird, it'll probably be all right.
219
220proc confspc-create {space confvar} {
221 ## Define a new configuration space called SPACE. You must do this before
222 ## defining directives or including other spaces.
223
224 global CONFSPC_CMD CONFSPC_INCL CONFSPC_CPL CONFSPC_CHANGE CONFSPC_VAR
225 if {![info exists CONFSPC_CMD($space)]} {
226 set CONFSPC_CMD($space) {}
227 set CONFSPC_INCL($space) {}
228 set CONFSPC_CPL($space) [list $space]
229 set CONFSPC_CHANGE($space) 0
230 set CONFSPC_VAR($space) $confvar
231 namespace eval ::confspc::$space {}
232 }
233}
234
235## Change sequence numbers are used to decide whether the linearized
236## inclusion caches are up to date.
237set CONFSPC_LASTCHANGESEQ 0
238set CONFSPC_CHANGESEQ 0
239
240proc confspc-command {space name bvl body} {
241 ## Define a configuration directive NAME in SPACE, accepting the arguments
242 ## specified by the BVL, and executing BODY when invoked. The SPACE's
243 ## configuration array is available within the BODY.
244
245 global CONFSPC_CMD CONFSPC_VAR
246 pushnew CONFSPC_CMD($space) $name
247
248 ## Define the configuration command in the caller's namespace.
249 set ns [uplevel 1 { namespace current }]
250 eval [list proc ${ns}::conf/$space/$name $bvl \
251 "global $CONFSPC_VAR($space)\n$body"]
252 namespace eval $ns [list namespace export conf/$space/$name]
253
254 ## Now arrange for this command to exist properly in the configuration
255 ## space.
256 namespace eval ::confspc::$space \
257 [list namespace import ${ns}::conf/$space/$name]
258 catch {
259 namespace eval ::confspc::$space [list rename $name {}]
260 }
261 namespace eval ::confspc::$space \
262 [list rename conf/$space/$name $name]
263}
264
265proc confspc-include {space includes} {
266 ## Arrange for SPACE to include the directives from the INCLUDES spaces.
267
268 global CONFSPC_INCL CONFSPC_LASTCHANGESEQ CONFSPC_CHANGESEQ
269 pushnew CONFSPC_INCL($space) $includes
270 if {$CONFSPC_CHANGESEQ <= $CONFSPC_LASTCHANGESEQ} {
271 set CONFSPC_CHANGESEQ [expr {$CONFSPC_LASTCHANGESEQ + 1}]
272 }
273}
274
275proc confspc-update {space} {
276 ## Update cached data for SPACE and its included spaces. We recompute the
277 ## space's class-precedence list, for which we use the C3 linearization
278 ## algorithm, which has known good properties.
279
280 global CONFSPC_CPL CONFSPC_CHANGE CONFSPC_INCL
281 global CONFSPC_CHANGESEQ CONFSPC_LASTCHANGESEQ
282 set CONFSPC_LASTCHANGESEQ $CONFSPC_CHANGESEQ
283
284 ## If the space is already up-to-date, do nothing.
285 if {$CONFSPC_CHANGE($space) == $CONFSPC_CHANGESEQ} { return }
286
287 ## Arrange for the included spaces to be up-to-date, and gather the CPLs
288 ## together so we can merge them.
289 set merge {}
290 lappend merge [concat $space $CONFSPC_INCL($space)]
291 foreach included $CONFSPC_INCL($space) {
292 confspc-update $included
293 lappend merge $CONFSPC_CPL($included)
294 }
295
296 ## Do the merge and update the change indicator.
297 set CONFSPC_CPL($space) [merge-lists $merge]
298 set CONFSPC_CHANGE($space) $CONFSPC_CHANGESEQ
299}
300
301proc confspc-path {ns cpl} {
302 ## Update namespace NS's command path so that it has (only) the
303 ## directives of the given CPL. Pass an empty CPL to clear the
304 ## configuration space hacking.
305
306 set path {}
307
308 ## Add the new namespaces to the front.
309 foreach spc $cpl { lappend path ::confspc::$spc }
310
311 ## Now add the existing path items, with any existing confspc hacking
312 ## stripped out.
313 foreach item [namespace eval $ns { namespace path }] {
314 if {![string match "::confspc::*" $item]} { lappend npath $item }
315 }
316
317 ## Commit the result.
318 namespace eval $ns [list namespace path $path]
319}
320
321proc confspc-set {ns space} {
322 ## Set the command path for namespace NS to include the configuration
323 ## directives of SPACE (and its included spaces).
324
325 global CONFSPC_CPL
326 confspc-update $space
327 confspc-path $ns $CONFSPC_CPL($space)
328}
329
330proc confspc-eval {space body} {
331 ## Evaluate BODY in the current namespace, but augmented with the
332 ## directives from the named SPACE. The command path of the current
333 ## namespace is restored afterwards.
334
335 set ns [uplevel 1 { namespace current }]
336 set path [namespace eval $ns { namespace path }]
337 unwind-protect {
338 confspc-set $ns $space
339 uplevel 1 $body
340 } {
341 namespace eval $ns [list namespace path $path]
342 }
343}
344
345proc preserving-config {confvar body} {
346 ## Evaluate BODY, but on exit restore the CONFVAR array so that the BODY
347 ## has no lasting effect on it.
348
63ba7a29 349 upvar #0 $confvar CONFIG
2a65b7cf
MW
350 set old [array get CONFIG]
351 unwind-protect {
352 uplevel 1 $body
353 } {
354 array unset CONFIG
355 array set CONFIG $old
356 }
357}
358
359confspc-create confspc CONFSPC_CONFIG
360
361confspc-command confspc include {args} {
362 ## Include the named configuration spaces in the current one.
363
364 confspc-include $CONFSPC_CONFIG(space) $args
365}
366
367confspc-command confspc define {name bvl body} {
368 ## Define a directive NAME in the current space, taking arguments BVL, and
369 ## having the given BODY.
370
371 confspc-command $CONFSPC_CONFIG(space) $name $bvl $body
372}
373
374confspc-command confspc define-simple {setting default} {
375 ## Define a directive SETTING which sets the appropriately prefixed entry
376 ## in the CONFIG array to its single arguments, and immediately set the
377 ## CONFIG entry to DEFAULT.
378
379 global CONFSPC_VAR
380 set space $CONFSPC_CONFIG(space)
381 upvar #0 $CONFSPC_VAR($space) config
382 confspc-command $space $setting arg \
383 "set $CONFSPC_VAR($space)($CONFSPC_CONFIG(prefix)$setting) \$arg"
384 set config($CONFSPC_CONFIG(prefix)$setting) $default
385}
386
387confspc-command confspc define-list {setting default} {
388 ## Define a directive SETTING which sets the appropriately prefixed entry
389 ## in the CONFIG array to its entire argument list, and immediately set the
390 ## CONFIG entry to DEFAULT (which should be a Tcl list, not a collection of
391 ## arguments).
392
393 global CONFSPC_VAR
394 set space $CONFSPC_CONFIG(space)
395 upvar #0 $CONFSPC_VAR($space) config
396 confspc-command $space $setting args \
397 "set $CONFSPC_VAR($space)($CONFSPC_CONFIG(prefix)$setting) \$args"
398 set config($CONFSPC_CONFIG(prefix)$setting) $default
399}
400
401confspc-command confspc prefix {prefix} {
402 set CONFSPC_CONFIG(prefix) $prefix
403}
404
405proc define-configuration-space {space confvar body} {
406 ## Define a new configuration space named SPACE. The BODY is Tcl code,
407 ## though it may make use of `include' and `define'.
408
409 global CONFSPC_CONFIG
410 set ns [uplevel 1 { namespace current }]
411 set oldpath [namespace eval $ns { namespace path }]
412 confspc-create $space $confvar
413 unwind-protect {
414 preserving-config CONFSPC_CONFIG {
415 array set CONFSPC_CONFIG [list space $space \
416 prefix ""]
417 confspc-set $ns confspc
418 uplevel 1 $body
419 }
420 } {
421 namespace eval $ns [list namespace path $oldpath]
422 }
423}
424
425###--------------------------------------------------------------------------
426### Option parsing.
427###
428### The option parsing machinery makes extensive use of a state array
429### OPTPARSE_STATE in order to maintain its context. The procedure
430### `with-option-parser' establishes this array correctly, and preserves any
431### existing state, so there should be no trouble with multiple parsers in
432### the same program.
433
434proc optparse-more-p {} {
435 ## Answer whether there are more argument words available.
436
437 upvar #0 OPTPARSE_STATE state
438 if {[llength $state(words)]} { return true } else { return false }
439}
440
441proc optparse-next-word {} {
442 ## Return the next word in the argument list. It is an error if there are
443 ## no more words left.
444
445 upvar #0 OPTPARSE_STATE state
446 set word [lindex $state(words) 0]
447 set state(words) [lrange $state(words) 1 end]
448 return $word
449}
450
451proc optparse-error {message} {
452 ## Report an error message and exit.
453
454 global QUIS
455 puts stderr "$QUIS: $message"
456 exit 1
457}
458
459proc optparse-option/short {var} {
460 ## Parse the next short option from the current cluster. If there are no
461 ## more short options, set the mode back to `free' and call back into
462 ## `optparse-option/free'.
463 ##
464 ## See the description of `optparse-option/free' for the interface
465 ## implemented by this procedure.
466
467 ## Get hold of my state and the caller's array.
468 upvar #0 OPTPARSE_STATE state
469 upvar 1 $var opt
470
471 ## Work out what to do based on the remaining length of the cluster. (The
472 ## cluster shouldn't be empty because the mode should only be set to
473 ## `short' if there is an initial nonempty cluster to parse, and we set it
474 ## back to `free' when we consume the final character from the cluster.)
475 ## Specifically, set `argp' according to whether we have a potential
476 ## argument in the cluster, and `name' to the option character extracted.
477 array unset opt
478 switch [string length $state(rest)] {
479 0 {
480 error "empty cluster"
481 }
482 1 {
483 set argp false
484 set state(mode) free
485 set name $state(rest)
486 }
487 default {
488 set argp true
489 set name [string index $state(rest) 0]
490 set state(rest) [string range $state(rest) 1 end]
491 }
492 }
493
494 ## Try to look up the option in the map.
495 if {![dict exists $state(short-map) $name]} {
496 optparse-error "Unknown option `$state(prefix)$name'"
497 }
498 array set opt [dict get $state(short-map) $name]
499 set state(name) $name
500
501 ## Collect an argument if one is required.
502 catch { unset state(arg) }
503 switch -glob -- "$opt(arg),$argp" {
504 "required,false" {
505 if {![optparse-more-p]} {
506 optparse-error "Option `$state(prefix)$name' requires an argument"
507 }
508 set state(arg) [optparse-next-word]
509 }
510 "required,true" - "optional,true" {
511 set state(arg) $state(rest)
512 set state(mode) free
513 }
514 }
515
516 ## Report success.
517 return 1
518}
519
520proc optparse-option/free {var} {
521 ## Parse the next option from the argument list. This procedure is called
522 ## to process a new argument word, i.e., we are in `free' mode. It
523 ## analyses the next argument word and either processes it internally or
524 ## sets the mode appropriately and calls a specialized handler
525 ## `optparse-option/MODE' for that mode.
526 ##
527 ## The interface works as follows. If an option was found, then the array
528 ## VAR is set according to the option's settings dictionary; and state
529 ## variables are set as follows.
530 ##
531 ## prefix The prefix character(s) to write before the option name in
532 ## messages, e.g., `--' for long options.
533 ##
534 ## name The option name without any prefix attached.
535 ##
536 ## arg The option's argument, if there is one; otherwise unset.
537
538 upvar #0 OPTPARSE_STATE state
539 upvar 1 $var opt
540
541 ## Set stuff up.
542 array unset opt
543 catch { unset state(arg) }
544 if {![optparse-more-p]} { return 0 }
545 set word [optparse-next-word]
546
547 ## Work out what to do based on the word. The order of these tests is
548 ## critically important.
549 switch -glob -- $word {
550
551 "--" {
552 ## End-of-options marker.
553
554 return 0
555 }
556
557 "--*" {
558 ## Long option.
559
560 set state(prefix) "--"
561
562 ## If there's an equals sign, the name is the bit to the left; keep the
563 ## remainder as an argument.
564 set eq [string first "=" $word 2]
565 if {$eq >= 0} {
566 set name [string range $word 2 [expr {$eq - 1}]]
567 set state(arg) [string range $word [expr {$eq + 1}] end]
568 set argp true
569 } else {
570 set name [string range $word 2 end]
571 set argp false
572 }
573 set state(name) name
574
575 ## Look the name up in the map.
576 if {[dict exists $state(long-map) $name]} {
577 array set opt [dict get $state(long-map) $name]
578 } else {
579 set matches [dict keys $state(long-map) "$name*"]
580 switch -exact -- [llength $matches] {
581 1 { array set opt [dict get $state(long-map) [lindex $matches 0]] }
582 0 { optparse-error "Unknown option `--$name'" }
583 default {
584 optparse-error "Ambiaguous option `--$name' \
585 (matches: --[join $matches {, --}])"
586 }
587 }
588 }
589
590 ## Now check whether we want an argument. The missing cases are
591 ## because we are already in the correct state.
592 switch -glob -- "$opt(arg),$argp" {
593 "none,true" {
594 optparse-error "Option `$name' doesn't accept an argument"
595 }
596 "required,false" {
597 if {![optparse-more-p]} {
598 optparse-error "Option `$name' requires an argument"
599 }
600 set state(arg) [optparse-next-word]
601 }
602 }
603
604 ## Done. We consumed either one or two entire argument words, so we
605 ## should remain in the `free' state.
606 return 1
607 }
608
609 "-?*" {
610 ## Short option. Set state, initialize the cluster, and go.
611
612 set state(rest) [string range $word 1 end]
613 set state(mode) short
614 set state(prefix) "-"
615 return [optparse-option/short opt]
616 }
617
618 default {
619 ## Some non-option thing. Under POSIX rules, this ends the parse. (We
620 ## could do something more adventurous later.)
621
622 set state(words) [concat [list $word] $state(words)]
623 return 0
624 }
625 }
626}
627
628proc optparse-arg-p {} {
629 ## Return the whether the most recently processed option had an argument.
630
631 upvar #0 OPTPARSE_STATE state
632 return [info exists state(arg)]
633}
634
635proc optparse-arg {} {
636 ## Return the argument from the most recently processed option. It is an
637 ## error if no argument was supplied.
638
639 upvar #0 OPTPARSE_STATE state
640 return $state(arg)
641}
642
643proc optparse-words {} {
644 ## Return the remaining unparsed argument words as a list.
645
646 upvar #0 OPTPARSE_STATE state
647 return $state(words)
648}
649
650proc optparse-option {} {
651 ## Parse the next option(s). The action taken depends on the option
652 ## dictionary: if an `action' is provided then it is evaluated in the
653 ## caller's context; otherwise the option's `tag' is returned.
654
655 upvar #0 OPTPARSE_STATE state
656 while 1 {
657 if {![optparse-option/$state(mode) opt]} {
658 return done
659 } elseif {[info exists opt(action)]} {
660 uplevel 1 $opt(action)
661 } elseif {[info exists opt(tag)]} {
662 return $opt(tag)
663 } else {
664 error "Don't know what to do with option `$state(prefix)$state(name)'"
665 }
666 }
667}
668
669proc with-option-parser {state words body} {
670 ## Establish an option parsing context, initialized with the STATE
671 ## (constructed using `define-options') and the lits of argument WORDS.
672 ## The BODY may use `optparse-option', `optparse-arg', etc. to parse the
673 ## options.
674
675 global OPTPARSE_STATE
676 set old [array get OPTPARSE_STATE]
677
678 unwind-protect {
679 array unset OPTPARSE_STATE
680 array set OPTPARSE_STATE $state
681 set OPTPARSE_STATE(mode) free
682 set OPTPARSE_STATE(words) $words
683 uplevel 1 $body
684 } {
685 array set OPTPARSE_STATE $old
686 }
687}
688
689define-configuration-space optparse-option OPTCFG {
690 define-list short {}
691 define-list long {}
692 define action {act} { set OPTCFG(action) $act }
693 define tag {tag} { set OPTCFG(tag) $tag }
694 define-simple arg none
695}
696
697define-configuration-space optparse OPTCFG {
698 define option {body} {
699 upvar #0 OPTPARSE_STATE state
700 uplevel 1 [list confspc-eval optparse-option $body]
701 set opt [array get OPTCFG]
702 foreach kind {long short} {
703 foreach name $OPTCFG($kind) {
704 if {[dict exists $state($kind-map) $name]} {
705 error "Already have an option with $kind name `$name'"
706 }
707 dict set state($kind-map) $name $opt
708 }
709 }
710 }
711}
712
713proc define-options {statevar body} {
714 ## Define an option state, and write it to STATEVAR. The BODY may contain
715 ## `optparse' configuration directives to define the available options.
716
717 global OPTPARSE_STATE
718 upvar 1 $statevar state
719 set old [array get OPTPARSE_STATE]
720 unwind-protect {
721 array unset OPTPARSE_STATE
722 if {[info exists state]} {
723 array set OPTPARSE_STATE $state
724 } else {
725 array set OPTPARSE_STATE {
726 long-map {}
727 short-map {}
728 }
729 }
730 uplevel 1 [list confspc-eval optparse $body]
731 set state [array get OPTPARSE_STATE]
732 } {
733 array set OPTPARSE_STATE $old
734 }
735}
736
737###--------------------------------------------------------------------------
738### Subcommand handling.
739
740## Determine the program name.
741set QUIS [file tail $argv0]
742
743## This is fluid-bound to the name of the current command.
744set COMMAND {}
745
746proc find-command {name} {
747 ## Given a command NAME as typed by the user, find the actual command and
748 ## return it.
749
750 global HELP
751 set matches [info commands cmd/$name*]
752 set cmds {}
753 set doc {}
754 foreach match $matches {
755 set cmd [string range $match 4 end]
756 lappend cmds $cmd
757 if {[info exists HELP($cmd)]} { lappend doc $cmd }
758 }
759 switch -exact -- [llength $cmds] {
760 1 { return [lindex $cmds 0] }
761 0 { optparse-error "Unknown command `$name'" }
762 }
763 if {[llength $doc]} { set cmds $doc }
764 switch -exact -- [llength $cmds] {
765 1 { return [lindex $cmds 0] }
766 0 { optparse-error "Unknown command `$name'" }
767 default { optparse-error "Ambiguous command `$name' -- matches: $cmds" }
768 }
769}
770
771proc usage {cmd} {
772 ## Return a usage message for CMD. The message is taken from the `USAGE'
773 ## array if that contains an entry for CMD (it should not include the
774 ## command name, and should begin with a leading space); otherwise a
775 ## message is constructed by examining the argument names and defaulting
776 ## arrangements of the Tcl command cmd/CMD.
777 ##
778 ## By convention, the main program is denoted by an empty CMD name.
779
780 global USAGE
781 if {[info exists USAGE($cmd)]} {
782 set usage $USAGE($cmd)
783 } else {
784 set usage ""
785 foreach arg [info args cmd/$cmd] {
786 if {[string equal $arg "args"]} {
787 append usage " ..."
788 } elseif {[info default cmd/$cmd $arg hunoz]} {
789 append usage " \[[string toupper $arg]\]"
790 } else {
791 append usage " [string toupper $arg]"
792 }
793 }
794 }
795 return $usage
796}
797
798proc usage-error {} {
799 ## Report a usage error in the current command. The message is obtained by
800 ## the `usage' procedure.
801
802 global QUIS COMMAND
803 if {[string length $COMMAND]} { set cmd " $COMMAND" } else { set cmd "" }
804 puts stderr "Usage: $QUIS$cmd[usage $COMMAND]"
805 exit 1
806}
807
808proc dispatch {name argv} {
809 ## Invokes the handler for CMD, passing it the argument list ARGV. This
810 ## does some minimal syntax checking by examining the argument list to the
811 ## command handler procedure cmd/COMMAND and issuing a usage error if
812 ## there's a mismatch.
813
814 global COMMAND
815 let COMMAND [find-command $name] {
816
817 ## Decode the argument list of the handler and set min and max
818 ## appropriately.
819 set args [info args cmd/$COMMAND]
820 if {![llength $args]} {
821 set* {min max} {0 0}
822 } else {
823 if {[string equal [lindex $args end] "args"]} {
824 set max inf
825 set args [lrange $args 0 end-1]
826 } else {
827 set max [llength $args]
828 }
829 set min 0
830 foreach arg $args {
831 if {[info default cmd/$COMMAND $arg hunoz]} { break }
832 incr min
833 }
834 }
835
836 ## Complain if the number of arguments is inappropriate.
837 set n [llength $argv]
838 if {$n < $min || ($max != inf && $n > $max)} { usage-error }
839
840 ## Invoke the handler.
841 eval cmd/$COMMAND $argv
842 }
843}
844
845define-configuration-space subcommand SUBCMD {
846 define-simple help-text -
847 define-simple usage-text -
848}
849
850proc defcmd {name bvl defs body} {
851 ## Define a command NAME with arguments BVL. The `usage-text' and
852 ## `help-text' commands can be used in DEFS to set messages for the new
853 ## command.
854
855 global SUBCMD USAGE HELP
856
857 preserving-config SUBCMD {
858 confspc-eval subcommand { uplevel 1 $defs }
859 foreach tag {usage-text help-text} array {USAGE HELP} {
860 if {![string equal $SUBCMD($tag) -]} {
861 set ${array}($name) $SUBCMD($tag)
862 }
863 }
864 }
865 proc cmd/$name $bvl $body
866}
867
868## Standard subcommand handler to show information about the program or its
869## subcommands. To use this, you need to set a bunch of variables.
870##
871## USAGE(cmd) Contains the usage message for cmd -- including
872## leading space -- to use instead of the `usage'
873## procedure's automagic.
874##
875## HELP(cmd) Contains descriptive text -- not including a final
876## trailing newline -- about the command.
877##
878## VERSION The program's version number.
879##
880## The `defcmd' procedure can be used to set these things up conveniently.
881defcmd help {args} {
882 usage-text " \[SUBCOMMAND ...]"
883 help-text "Show help on the given SUBCOMMANDs, or on the overall program."
884} {
885 global QUIS VERSION USAGE HELP
886 if {[llength $args]} {
887 foreach name $args {
888 set cmd [find-command $name]
889 puts "Usage: $QUIS $cmd[usage $cmd]"
890 if {[info exists HELP($cmd)]} { puts "\n$HELP($cmd)" }
891 }
892 } else {
893 puts "$QUIS, version $VERSION\n"
894 puts "Usage: $QUIS$USAGE()\n"
895 if {[info exists HELP()]} { puts "$HELP()\n" }
896 puts "Subcommands available:"
897 foreach name [info commands cmd/*] {
898 set cmd [string range $name 4 end]
899 puts "\t$cmd[usage $cmd]"
900 }
901 }
902}
903
904###--------------------------------------------------------------------------
905### Build the configuration space for zone files.
906
907proc host-addr {host} {
908 ## Given a HOST name, return a list of its addresses.
909
910 if {![string match $host {*[!0-9.]*}]} { return $host }
911 set adns [open [list | adnshost +Dc -s $host] r]
912 unwind-protect {
913 set addrs {}
914 while {[gets $adns line] >= 0} {
915 set* {name type fam addr} $line
916 switch -glob -- $type:$fam {
917 A:INET { lappend addrs $addr }
918 }
919 }
920 return [lindex $addrs 0]
921 } {
922 close $adns
923 }
924}
925
926proc host-canonify {host} {
927 ## Given a HOST name, return a canonical version of it.
928
929 set adns [open [list | adnshost -Dc -s $host] r]
930 unwind-protect {
931 while {[gets $adns line] >= 0} {
932 switch -exact -- [lindex $line 1] {
933 CNAME { return [lindex $line 2] }
934 A - AAAA { return [lindex $line 0] }
935 }
936 }
937 error "failed to canonify $host"
938 } {
939 close $adns
940 }
941}
942
943proc local-address-p {addr} {
944 ## Answer whether the ADDR is one of the host's addresses.
945
946 if {[catch { set sk [socket -server {} -myaddr $addr 0] }]} {
947 return false
948 } else {
949 close $sk
950 return true
951 }
952}
953
954## The list of zones configured by the user.
955set ZONES {}
956
957## Dynamic zone update policy specifications.
958define-configuration-space policy ZONECFG {
959 define allow {identity nametype name args} {
960 lappend ZONECFG(ddns-policy) \
961 [concat grant [list $identity $nametype $name] $args]
962 }
963 define deny {identity nametype name args} {
964 lappend ZONECFG(ddns-policy) \
965 [concat deny [list $identity $nametype $name] $args]
966 }
967}
968
969## Dynamic zone details.
970define-configuration-space dynamic ZONECFG {
971 prefix "ddns-"
972 define-simple key "ddns"
db6576c8 973 define-simple auto-dnssec off
2a65b7cf
MW
974 define-list types {A TXT PTR}
975
976 define policy {body} {
977 set ZONECFG(ddns-policy) {}
978 uplevel 1 [list confspc-eval policy $body]
979 }
980
981 set ZONECFG(ddns-policy) {}
982}
983
984## Everything about a zone.
a37c695a
MW
985set HOME "@pkgstatedir@"
986set BINDPROGS "@bindprogsdir@"
2a65b7cf
MW
987define-configuration-space zone ZONECFG {
988 define-simple user root
a37c695a
MW
989 define-simple home-dir $HOME
990 define-simple static-dir "$HOME/static"
991 define-simple dynamic-dir "$HOME/dynamic"
2a65b7cf
MW
992 define-simple dir-mode 2775
993 define-simple zone-file "%v/%z.zone"
0af96368 994 define-simple soa-format increment
2a65b7cf 995 define-list views *
0af96368 996 define-list sign-views {}
a37c695a
MW
997 define-list signzone-command \
998 [list "$BINDPROGS/dnssec-signzone" \
999 "-g" \
1000 "-S" \
1001 "-K%h/key" \
1002 "-d%h/ds" \
5860c677 1003 "-s-3600" "-e+176400" "-i90000" \
a37c695a
MW
1004 "-N%q" \
1005 "-o%z" \
1006 "-f%o" \
1007 "%f"]
a37c695a
MW
1008 define-list reload-command [list "$BINDPROGS/rndc" "reload" "%z" "IN" "%v"]
1009 define-list autosign-command [list "$BINDPROGS/rndc" "sign" "%z" "IN" "%v"]
1010 define-list checkzone-command \
1011 [list "$BINDPROGS/named-checkzone" \
1012 "-ifull" \
1013 "-kfail" \
1014 "-Mfail" \
1015 "-nfail" \
1016 "-Sfail" \
1017 "-Wfail" \
1018 "%z" "%f"]
1019
1020 define setvar {name value} {
1021 dict set ZONECFG(var) $name $value
2a65b7cf
MW
1022 }
1023
1024 define primary {map} {
e4ce9d1e
MW
1025 ## There's a grim hack here: a primary-address entry may have the form
1026 ## REAL!FAKE. If the REAL address is not a local address then this
1027 ## is used as the master address; otherwise the FAKE address is used.
1028 ## This is useful for inter-view updates of dynamic zones on the same
1029 ## host. I suggest abusing 127.0.0.0/8 addresses for this kind of
1030 ## chicanery.
2a65b7cf
MW
1031 if {[llength $map] % 2} {
1032 error "master map must have an even number of items"
1033 }
1034 set ZONECFG(master-map) $map
1035 }
1036
1037 define dynamic {{body {}}} {
1038 array set ZONECFG [list type dynamic]
1039 uplevel 1 [list confspc-eval dynamic $body]
1040 }
1041
1042 define view-map {map} {
e83cb954
MW
1043
1044 ## OK, this needs careful documentation.
1045 ##
1046 ## The local nameserver presents a number of views according to its
1047 ## configuration. It is our purpose here to generate a configuration
1048 ## snippet for such a view.
1049 ##
1050 ## A user might have several different views of a zone which are meant to
1051 ## be presented to different clients. These map on to the server views
1052 ## in a one-to-many fashion. The `view-map' option defines this mapping.
1053 ## The argument is a list of alternating SERVER-VIEW USER-VIEW pairs; the
1054 ## SERVER-VIEW may be a glob pattern; the USER-VIEW may be the special
1055 ## token `=' to mean `same as the SERVER-VIEW'.
1056 ##
1057 ## We only keep one copy of the zone file for each user view: if the user
1058 ## view is used by many server views, then the zone stanza for each of
1059 ## those views refers to the same zone file.
1060
2a65b7cf
MW
1061 if {[llength $map] % 2} {
1062 error "view map must have an even number of items"
1063 }
1064 set ZONECFG(view-map) $map
1065 }
1066
1067 array set ZONECFG {
1068 type static
1069 view-map {* =}
1070 }
1071}
1072
1073## Top-level configuration. Allow most zone options to be set here, so that
1074## one can set defaults for multiple zones conveniently.
1075define-configuration-space toplevel ZONECFG {
1076 include zone
1077
1078 define-list all-views {}
a37c695a 1079 define-simple conf-file "$HOME/config/%v.conf"
2a65b7cf
MW
1080 define-simple max-zone-size [expr {512*1024}]
1081 define-list reconfig-command {/usr/sbin/rndc reconfig}
1082
0af96368
MW
1083 define scope {body} {
1084 preserving-config ZONECFG { uplevel 1 $body }
1085 }
2a65b7cf
MW
1086
1087 define zone {name {body {}}} {
1088 global ZONES
1089 preserving-config ZONECFG {
1090 array set ZONECFG \
1091 [list name $name \
1092 type static]
1093 uplevel 1 [list confspc-eval zone $body]
1094 lappend ZONES [array get ZONECFG]
1095 }
1096 }
1097}
1098
1099###--------------------------------------------------------------------------
1100### Processing the results.
1101
1102proc zone-file-name {view config} {
1103 ## Return the relative file name for the zone described by CONFIG, relative
1104 ## to the given VIEW. An absolute filename may be derived later, depending
1105 ## on whether the zone data is static and the calling host is the master
1106 ## for the zone.
1107
1108 array set zone $config
1109 return [string map [list \
1110 "%v" $view \
1111 "%z" $zone(name)] \
1112 $zone(zone-file)]
1113}
1114
1115proc output-file-name {view} {
1116 ## Return the output file name for the given VIEW.
1117
1118 global ZONECFG
1119 return [string map [list %v $view] $ZONECFG(conf-file)]
1120}
1121
1122proc compute-zone-properties {view config} {
1123 ## Derive interesting information from the zone configuration plist CONFIG,
e83cb954 1124 ## relative to the stated server VIEW. Return a new plist.
2a65b7cf
MW
1125
1126 array set zone $config
1127
1128 ## See whether the zone matches the view.
1129 set match 0
1130 foreach wanted $zone(views) {
1131 if {[string match $wanted $view]} { set match 1; break }
1132 }
1133 if {!$match} { return {config-type ignore} }
1134
1135 ## Transform the view name according to the view map.
1136 foreach {inview outview} $zone(view-map) {
1137 if {![string match $inview $view]} { continue }
1138 switch -exact -- $outview {
1139 = { set zone(mapped-view) $view }
1140 default { set zone(mapped-view) $outview }
1141 }
1142 break
1143 }
1144
1145 ## Find out where the master is supposed to be.
1146 set zone(config-type) ignore
1147 if {[info exists zone(mapped-view)]} {
1148 foreach {outview hosts} $zone(master-map) {
1149 if {[string match $outview $zone(mapped-view)]} {
e4ce9d1e 1150 set masters {}
2a65b7cf
MW
1151 set zone(config-type) slave
1152 foreach host $hosts {
e4ce9d1e
MW
1153 set bang [string first "!" $host]
1154 if {$bang >= 0} {
1155 set before [string range $host 0 [expr {$bang - 1}]]
1156 set after [string range $host [expr {$bang + 1}] end]
1157 if {[local-address-p $before]} {
1158 set host $after
1159 } else {
1160 set host $before
1161 }
1162 } elseif {[local-address-p $host]} {
2a65b7cf
MW
1163 set zone(config-type) master
1164 }
e4ce9d1e 1165 lappend masters $host
2a65b7cf 1166 }
e4ce9d1e 1167 set zone(masters) $masters
2a65b7cf
MW
1168 break
1169 }
1170 }
1171 }
1172
0af96368
MW
1173 ## Work out the file names.
1174 switch -glob -- $zone(config-type):$zone(type) {
1175 master:static {
a37c695a 1176 set dir $zone(static-dir)
0af96368 1177 set nameview $zone(mapped-view)
2a65b7cf 1178 }
0af96368 1179 default {
a37c695a 1180 set dir $zone(dynamic-dir)
0af96368
MW
1181 set nameview $view
1182 }
1183 }
1184 set zone(file-name) [file join $dir \
1185 [zone-file-name $nameview $config]]
1186
1187 ## Find out whether this zone wants signing.
1188 set zone(sign) false
1189 switch -glob -- $zone(config-type):$zone(type) {
1190 master:static {
1191 foreach sview $zone(sign-views) {
1192 if {[string match $zone(mapped-view) $sview]} { set zone(sign) true }
1193 }
2a65b7cf
MW
1194 }
1195 }
0af96368
MW
1196 if {$zone(sign)} {
1197 set zone(server-file-name) "$zone(file-name).sig"
1198 } else {
1199 set zone(server-file-name) $zone(file-name)
1200 }
2a65b7cf
MW
1201
1202 ## Done.
1203 return [array get zone]
1204}
1205
1206proc write-ddns-update-policy {prefix chan config} {
1207 ## Write an `update-policy' stanza to CHAN for the zone described by the
1208 ## CONFIG plist. The PREFIX is written to the start of each line.
1209
1210 array set zone $config
1211 puts $chan "${prefix}update-policy {"
1212 set policyskel "${prefix}\t%s %s %s \"%s\" %s;"
1213
1214 foreach item $zone(ddns-policy) {
1215 set* {verb ident type name} [lrange $item 0 3]
1216 set rrtypes [lrange $item 4 end]
1217 puts $chan [format $policyskel \
1218 $verb \
a37c695a
MW
1219 $ident \
1220 $type \
1221 $name \
1222 $rrtypes]
2a65b7cf
MW
1223 }
1224
1225 puts $chan [format $policyskel \
1226 grant \
1227 $zone(ddns-key) \
1228 subdomain \
1229 $zone(name) \
1230 $zone(ddns-types)]
1231
1232 puts $chan "${prefix}};"
1233}
1234
a37c695a 1235proc sign-zone-file {info soafmt infile} {
0af96368
MW
1236 ## Sign the zone described by INFO. The input zone file is INPUT; the SOA
1237 ## should be updated according to SOAFMT.
1238
1239 global QUIS
1240
1241 array set zone $info
a37c695a 1242 set outfile "$zone(server-file-name).new"
9fe4d067 1243 if {![run "sign zone `$zone(name)' in view `$zone(mapped-view)'" \
a37c695a
MW
1244 $zone(signzone-command) \
1245 "%h" $zone(home-dir) \
1246 "%m" $zone(static-dir) \
1247 "%s" $zone(dynamic-dir) \
1248 "%z" $zone(name) \
1249 "%f" $infile \
1250 "%o" $outfile \
1251 "%q" $soafmt]} {
1252 file delete -force $outfile
1253 return false
1254 }
1255 file rename -force $outfile $zone(server-file-name)
1256 return true
0af96368
MW
1257}
1258
2a65b7cf
MW
1259proc write-zone-stanza {view chan config} {
1260 ## Write a `zone' stanza to CHAN for the zone described by the CONFIG
1261 ## plist in the given VIEW.
1262
1263 array set zone [compute-zone-properties $view $config]
1264 if {[string equal $zone(config-type) "ignore"]} { return }
1265
1266 ## Create the directory for the zone files.
1267 set dir [file dirname $zone(file-name)]
1268 if {![file isdirectory $dir]} {
1269 file mkdir $dir
1270 exec chmod $zone(dir-mode) $dir
1271 }
1272
1273 ## Write the configuration fragment.
1274 puts $chan "\nzone \"$zone(name)\" {"
1275 switch -glob -- $zone(config-type) {
1276 master {
1277 puts $chan "\ttype master;"
0af96368 1278 puts $chan "\tfile \"$zone(server-file-name)\";"
2a65b7cf 1279 switch -exact -- $zone(type) {
db6576c8
MW
1280 dynamic {
1281 write-ddns-update-policy "\t" $chan $config
1282 if {![string equal $zone(ddns-auto-dnssec) off]} {
1283 puts $chan "\tauto-dnssec $zone(ddns-auto-dnssec);"
1284 }
1285 }
2a65b7cf
MW
1286 }
1287 }
1288 slave {
1289 puts $chan "\ttype slave;"
1290 set masters {}
1291 foreach host $zone(masters) { lappend masters [host-addr $host] }
1292 puts $chan "\tmasters { [join $masters {; }]; };"
1293 puts $chan "\tfile \"$zone(file-name)\";"
1294 switch -exact -- $zone(type) {
1295 dynamic { puts $chan "\tallow-update-forwarding { any; };" }
1296 }
1297 }
1298 }
1299 puts $chan "};";
1300}
1301
1302###--------------------------------------------------------------------------
1303### Command-line interface.
1304
a37c695a 1305set CONFFILE "@pkgconfdir@/zones.in"
2a65b7cf
MW
1306
1307defcmd outputs {} {
1308 help-text "List the output file names to stdout."
1309} {
1310 global ZONECFG CONFFILE
1311
1312 confspc-eval toplevel [list source $CONFFILE]
1313 foreach view $ZONECFG(all-views) { puts [output-file-name $view] }
1314}
1315
1316defcmd update {} {
1317 help-text "Generate BIND configuration files."
1318} {
1319 global ZONECFG ZONES CONFFILE
1320
0af96368 1321 ## Read the configuration.
2a65b7cf 1322 confspc-eval toplevel [list source $CONFFILE]
0af96368
MW
1323
1324 ## Safely update the files.
2a65b7cf
MW
1325 set win false
1326 unwind-protect {
0af96368
MW
1327
1328 ## Work through each server view.
2a65b7cf 1329 foreach view $ZONECFG(all-views) {
0af96368
MW
1330
1331 ## Open an output file.
2a65b7cf
MW
1332 set out($view) [output-file-name $view]
1333 set chan($view) [open "$out($view).new" w]
0af96368
MW
1334
1335 ## Write a header.
2a65b7cf
MW
1336 set now [clock format [clock seconds] -format "%Y-%m-%d %H:%M:%S"]
1337 puts $chan($view) "### -*-conf-javaprop-*-"
1338 puts $chan($view) "### Generated at $now: do not edit"
0af96368
MW
1339
1340 ## Now print a stanza for each zone in the view.
2a65b7cf
MW
1341 foreach zone $ZONES {
1342 write-zone-stanza $view $chan($view) $zone
1343 }
1344 }
0af96368
MW
1345
1346 ## Done: don't delete the output.
2a65b7cf
MW
1347 set win true
1348 } {
0af96368
MW
1349
1350 ## Close the open files.
a37c695a
MW
1351 foreach view $ZONECFG(all-views) {
1352 catch { close $chan($view) }
1353 }
0af96368
MW
1354
1355 ## If we succeeded, rename the output files into their proper places;
1356 ## otherwise, delete them.
2a65b7cf
MW
1357 if {$win} {
1358 foreach view $ZONECFG(all-views) {
1359 file rename -force -- "$out($view).new" $out($view)
1360 }
1361 eval exec $ZONECFG(reconfig-command)
1362 } else {
a37c695a 1363 catch { file delete -force -- "$out($view).new" }
2a65b7cf
MW
1364 }
1365 }
1366}
1367
1368defcmd install {user view name} {
1369 help-text "Install a new zone file.
1370
9fe4d067
MW
1371The file is for the given zone NAME and \(user-side) VIEW. The file is
1372provided by the named USER."
2a65b7cf
MW
1373} {
1374 global QUIS ZONECFG ZONES CONFFILE errorInfo errorCode
1375
0af96368 1376 ## Read the configuration.
2a65b7cf
MW
1377 confspc-eval toplevel [list source $CONFFILE]
1378
0af96368 1379 ## Make sure there's a temporary directory.
a37c695a 1380 file mkdir [file join $ZONECFG(home-dir) "tmp"]
2a65b7cf 1381
0af96368 1382 ## Keep track of cleanup jobs.
2a65b7cf
MW
1383 set cleanup {}
1384 unwind-protect {
1385
0af96368 1386 ## Find out which server views are affected by this update.
2a65b7cf
MW
1387 set matchview {}
1388 foreach iview $ZONECFG(all-views) {
1389 foreach info $ZONES {
1390 array unset zone
1391 array set zone [compute-zone-properties $iview $info]
1392 if {[string equal $user $zone(user)] && \
1393 [string equal "$zone(config-type)/$zone(type)" \
1394 "master/static"] && \
1395 [string equal $zone(name) $name] && \
1396 [string equal $zone(mapped-view) $view]} {
1397 lappend matchview $iview
1398 if {![info exists matchinfo]} { set matchinfo [array get zone] }
1399 }
1400 }
1401 }
1402 if {![llength $matchview]} {
1403 optparse-error "No match for zone `$name' in view `$view'"
1404 }
1405 array unset zone
1406 array set zone $matchinfo
1407
0af96368 1408 ## Make a new temporary file to read the zone into.
2a65b7cf
MW
1409 set pid [pid]
1410 for {set i 0} {$i < 1000} {incr i} {
a37c695a 1411 set tmp [file join $ZONECFG(home-dir) "tmp" \
2a65b7cf
MW
1412 "tmp.$pid.$i.$user.$name"]
1413 if {![catch { set chan [open $tmp {WRONLY CREAT EXCL}] } msg]} {
1414 break
1415 } elseif {[string equal [lindex $errorCode 0] POSIX] && \
1416 ![string equal [lindex $errorCode 1] EEXIST]} {
1417 error $msg $errorInfo $errorCode
1418 }
1419 }
1420 if {![info exists chan]} { error "failed to create temporary file" }
1421 set cleanup [list file delete $tmp]
1422
0af96368 1423 ## Read the zone data from standard input into the file.
2a65b7cf
MW
1424 set total 0
1425 while {true} {
1426 set stuff [read stdin 4096]
1427 if {![string length $stuff]} { break }
1428 puts -nonewline $chan $stuff
1429 incr total [string bytelength $stuff]
1430 if {$total > $ZONECFG(max-zone-size)} {
1431 error "zone file size limit exceeded"
1432 }
1433 }
1434 close $chan
1435
a97a6929 1436 ## Check the zone for sanity.
400655ac 1437 if {![run "zone check" $zone(checkzone-command) \
a37c695a
MW
1438 "%z" $name \
1439 "%v" $view \
1440 "%f" $tmp]} {
0af96368 1441 eval $cleanup
2a65b7cf 1442 exit 1
2a65b7cf
MW
1443 }
1444
0af96368 1445 ## If the zone wants signing, better to do that now.
39bfd16a 1446 if {$zone(sign) && ![sign-zone-file $matchinfo keep $tmp]} {
0af96368
MW
1447 eval $cleanup
1448 exit 2
1449 }
1450
1451 ## All seems good: stash the file in the proper place and reload the
1452 ## necessary server views.
2a65b7cf
MW
1453 file rename -force -- $tmp $zone(file-name)
1454 set cleanup {}
1455 foreach view $matchview {
400655ac
MW
1456 if {![run "reload zone `$zone(name) in view `$view'" \
1457 $zone(reload-command) \
1458 "%v" $view \
1459 "%z" $zone(name)]} {
1460 exit 3
1461 }
2a65b7cf
MW
1462 }
1463 } {
1464 eval $cleanup
1465 }
1466}
1467
0af96368
MW
1468defcmd sign {} {
1469 help-text "Sign DNSSEC zones."
1470} {
1471 global QUIS ZONECFG ZONES CONFFILE
1472
1473 set rc 0
1474
1475 ## Read the configuration.
1476 confspc-eval toplevel [list source $CONFFILE]
1477
1478 ## Grind through all of the zones.
400655ac
MW
1479 array unset seen
1480 foreach view $ZONECFG(all-views) {
0af96368 1481 foreach info $ZONES {
400655ac
MW
1482
1483 ## Fetch the zone information.
0af96368 1484 array unset zone
400655ac 1485 set compinfo [compute-zone-properties $view $info]
0af96368
MW
1486 array set zone $compinfo
1487 if {![string equal $zone(config-type) master]} { continue }
400655ac 1488
0af96368 1489 if {[string equal $zone(type) static] && $zone(sign)} {
400655ac
MW
1490 ## Static zone: re-sign it if we haven't seen this user view before,
1491 ## and then reload.
1492
1493 ## Sign the zone file if we haven't tried before.
1494 set id [list $zone(name) $zone(mapped-view)]
1495 if {![info exists seen($id)]} {
a37c695a
MW
1496 if {[sign-zone-file $compinfo $zone(soa-format) \
1497 $zone(server-file-name)]} {
400655ac
MW
1498 set seen($id) true
1499 } else {
1500 set rc 2
1501 set seen($id) failed
1502 }
1503 }
1504
1505 ## If we succeeded, reload the zone in this server view.
1506 if {[string equal $seen($id) true]} {
1507 if {![run "reload zone `$zone(name) in server view `$view'" \
1508 $zone(reload-command) \
1509 "%z" $zone(name) \
1510 "%v" $view]} {
1511 set rc 2
1512 }
0af96368
MW
1513 }
1514 } elseif {[string equal $zone(type) dynamic] &&
db6576c8 1515 ![string equal $zone(ddns-auto-dnssec) off]} {
400655ac
MW
1516 ## Dynamic zone: get BIND to re-sign it.
1517
1518 if {![run "re-sign zone `$zone(name) in server view `$view'" \
1519 $zone(autosign-command) \
1520 "%z" $zone(name) \
1521 "%v" $view]} {
0af96368
MW
1522 set rc 2
1523 }
1524 }
1525 }
1526 }
1527 exit $rc
1528}
1529
2a65b7cf
MW
1530###--------------------------------------------------------------------------
1531### Main program.
1532
a37c695a 1533set VERSION "@VERSION@"
2a65b7cf
MW
1534set USAGE() " \[-OPTIONS] SUBCOMMAND \[ARGUMENTS...]"
1535
1536define-options OPTS {
1537 option {
1538 short "h"; long "help"
1539 action { eval cmd/help [optparse-words]; exit }
1540 }
1541 option {
1542 short "v"; long "version"
1543 action { puts "$QUIS, version $VERSION"; exit }
1544 }
1545 option {
1546 short "c"; long "config"; arg required
1547 action { set CONFFILE [optparse-arg] }
1548 }
1549}
1550
1551with-option-parser $OPTS $argv {
1552 optparse-option
1553 set argv [optparse-words]
1554}
1555
1556if {![llength $argv]} { usage-error }
1557dispatch [lindex $argv 0] [lrange $argv 1 end]
1558
1559###----- That's all, folks --------------------------------------------------