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