(export 'timespec-seconds)
(defun timespec-seconds (ts)
- "Convert a timespec TS to seconds. A timespec may be a real count of
- seconds, or a list (COUNT UNIT): UNIT may be any of a number of obvious
- time units."
+ "Convert a timespec TS to seconds.
+
+ A timespec may be a real count of seconds, or a list (COUNT UNIT): UNIT
+ may be any of a number of obvious time units."
(cond ((null ts) 0)
((realp ts) (floor ts))
((atom ts)
(maphash (lambda (key val) (declare (ignore val)) (collect key)) ht)))
(defun iso-date (&optional time &key datep timep (sep #\ ))
- "Construct a textual date or time in ISO format. The TIME is the universal
- time to convert, which defaults to now; DATEP is whether to emit the date;
- TIMEP is whether to emit the time, and SEP (default is space) is how to
- separate the two."
+ "Construct a textual date or time in ISO format.
+
+ The TIME is the universal time to convert, which defaults to now; DATEP is
+ whether to emit the date; TIMEP is whether to emit the time, and
+ SEP (default is space) is how to separate the two."
(multiple-value-bind
(sec min hr day mon yr dow dstp tz)
(decode-universal-time (if (or (null time) (eq time :now))
(export 'preferred-subnet-case)
(defmacro preferred-subnet-case (&body clauses)
- "CLAUSES have the form (SUBNETS . FORMS) -- evaluate the first FORMS whose
- SUBNETS (a list or single symbol, not evaluated) are considered preferred
- by zone-preferred-subnet-p. If SUBNETS is the symbol t then the clause
- always matches."
+ "CLAUSES have the form (SUBNETS . FORMS).
+
+ Evaluate the first FORMS whose SUBNETS (a list or single symbol, not
+ evaluated) are considered preferred by zone-preferred-subnet-p. If
+ SUBNETS is the symbol t then the clause always matches."
`(cond
,@(mapcar (lambda (clause)
(let ((subnets (car clause)))
clauses)))
(defun zone-process-records (rec ttl func)
- "Sort out the list of records in REC, calling FUNC for each one. TTL is
- the default time-to-live for records which don't specify one."
+ "Sort out the list of records in REC, calling FUNC for each one.
+
+ TTL is the default time-to-live for records which don't specify one.
+
+ The syntax is a little fiddly to describe. It operates relative to a
+ subzone name NAME.
+
+ ZONE-RECORD: RR | TTL | SUBZONE
+ The body of a zone form is a sequence of these.
+
+ TTL: :ttl INTEGER
+ Sets the TTL for subsequent RRs in this zone or subzone.
+
+ RR: SYMBOL DATA
+ Adds a record for the current NAME; the SYMBOL denotes the record
+ type, and the DATA depends on the type.
+
+ SUBZONE: (LABELS ZONE-RECORD*)
+ Defines a subzone. The LABELS is either a list of labels, or a
+ singleton label. For each LABEL, evaluate the ZONE-RECORDs relative
+ to LABEL.NAME. The special LABEL `@' is a no-op."
(labels ((sift (rec ttl)
(collecting (top sub)
(loop
(process (rec dom ttl)
(multiple-value-bind (top sub) (sift rec ttl)
(if (and dom (null top) sub)
- (let ((preferred nil))
- (dolist (s sub)
- (when (some #'zone-preferred-subnet-p
- (listify (zs-name s)))
- (setf preferred s)))
- (unless preferred
- (setf preferred (car sub)))
+ (let ((preferred
+ (or (find-if (lambda (s)
+ (some #'zone-preferred-subnet-p
+ (listify (zs-name s))))
+ sub)
+ (car sub))))
(when preferred
(process (zs-records preferred)
dom
(export 'make-zone-serial)
(defun make-zone-serial (name)
- "Given a zone NAME, come up with a new serial number. This will (very
- carefully) update a file ZONE.serial in the current directory."
+ "Given a zone NAME, come up with a new serial number.
+
+ This will (very carefully) update a file ZONE.serial in the current
+ directory."
(let* ((file (zone-file-name name :serial))
(last (with-open-file (in file
:direction :input
(safely-writing (out file)
(format out
";; Serial number file for zone ~A~%~
- ;; (LAST-SEQ DAY MONTH YEAR)~%~
- ~S~%"
+ ;; (LAST-SEQ DAY MONTH YEAR)~%~
+ ~S~%"
name
(cons seq now)))
(from-mixed-base '(100 100 100) (reverse (cons seq now)))))
;;; Zone form parsing.
(defun zone-parse-head (head)
- "Parse the HEAD of a zone form. This has the form
+ "Parse the HEAD of a zone form.
+
+ This has the form
(NAME &key :source :admin :refresh :retry
- :expire :min-ttl :ttl :serial)
+ :expire :min-ttl :ttl :serial)
though a singleton NAME needn't be a list. Returns the default TTL and an
soa structure representing the zone head."
(export 'defzoneparse)
(defmacro defzoneparse (types (name data list
&key (prefix (gensym "PREFIX"))
- (zname (gensym "ZNAME"))
- (ttl (gensym "TTL")))
+ (zname (gensym "ZNAME"))
+ (ttl (gensym "TTL")))
&body body)
- "Define a new zone record type (or TYPES -- a list of synonyms is
- permitted). The arguments are as follows:
+ "Define a new zone record type.
+
+ The TYPES may be a list of synonyms. The other arguments are as follows:
NAME The name of the record to be added.
',type)))))
(defun zone-parse-records (zone records)
+ "Parse the body of a zone form.
+
+ ZONE is the zone object; RECORDS is the body of the form."
(let ((zname (zone-name zone)))
(with-collection (rec)
(flet ((parse-record (zr)
(export 'zone-parse)
(defun zone-parse (zf)
- "Parse a ZONE form. The syntax of a zone form is as follows:
+ "Parse a ZONE form.
+
+ The syntax of a zone form is as follows:
ZONE-FORM:
ZONE-HEAD ZONE-RECORD*
(rec :name (zone-parse-host "mask" name)
:type :a
:data (ipnet-mask n))
- (rec :name (zone-parse-host "broadcast" name)
+ (rec :name (zone-parse-host "bcast" name)
:type :a
:data (ipnet-broadcast n)))))
(defzoneparse (:rev :reverse) (name data rec)
- ":reverse ((NET :bytes BYTES) ZONE*)"
+ ":reverse ((NET :bytes BYTES) ZONE*)
+
+ Add a reverse record each host in the ZONEs (or all zones) that lies
+ within NET. The BYTES give the number of prefix labels generated; this
+ defaults to the smallest number of bytes needed to enumerate the net."
(setf data (listify data))
(destructuring-bind (net &key bytes) (listify (car data))
(setf net (zone-parse-net net name))
(setf (gethash name seen) t)))))))))
(defzoneparse (:cidr-delegation :cidr) (name data rec :zname zname)
- ":cidr-delegation ((NET :bytes BYTES) (TARGET-NET [TARGET-ZONE])*)"
+ ":cidr-delegation ((NET :bytes BYTES) ((TARGET-NET*) [TARGET-ZONE])*)
+
+ Insert CNAME records for delegating a portion of the reverse-lookup
+ namespace which doesn't align with an octet boundary.
+
+ The NET specifies the origin network, in which the reverse records
+ naturally lie. The BYTES are the number of labels to supply for each
+ address; the default is the smallest number which suffices to enumerate
+ the entire NET. The TARGET-NETs are subnets of NET which are to be
+ delegated. The TARGET-ZONEs are the zones to which we are delegating
+ authority for the reverse records: the default is to append labels for those
+ octets of the subnet base address which are not the same in all address in
+ the subnet."
(setf data (listify data))
(destructuring-bind (net &key bytes) (listify (car data))
(setf net (zone-parse-net net name))
(unless bytes
(setf bytes (ipnet-changeable-bytes (ipnet-mask net))))
(dolist (map (or (cdr data) (list (list net))))
- (destructuring-bind (tnet &optional tdom) (listify map)
- (setf tnet (zone-parse-net tnet name))
- (unless (ipnet-subnetp net tnet)
- (error "~A is not a subnet of ~A."
- (ipnet-pretty tnet)
- (ipnet-pretty net)))
- (unless tdom
- (with-ipnet (net mask) tnet
- (setf tdom
- (join-strings
- #\.
- (append (reverse (loop
- for i from (1- bytes) downto 0
- until (zerop (logand mask
- (ash #xff
- (* 8 i))))
- collect (ldb (byte 8 (* i 8)) net)))
- (list name))))))
- (setf tdom (string-downcase (stringify tdom)))
- (dotimes (i (ipnet-hosts tnet))
- (unless (zerop i)
- (let* ((addr (ipnet-host tnet i))
- (tail (join-strings #\.
- (loop
- for i from 0 below bytes
- collect
- (logand #xff
- (ash addr (* 8 i)))))))
- (rec :name (format nil "~A.~A" tail name)
- :type :cname
- :data (format nil "~A.~A" tail tdom)))))))))
+ (destructuring-bind (tnets &optional tdom) (listify map)
+ (dolist (tnet (listify tnets))
+ (setf tnet (zone-parse-net tnet name))
+ (unless (ipnet-subnetp net tnet)
+ (error "~A is not a subnet of ~A."
+ (ipnet-pretty tnet)
+ (ipnet-pretty net)))
+ (unless tdom
+ (with-ipnet (net mask) tnet
+ (setf tdom
+ (join-strings
+ #\.
+ (append (reverse (loop
+ for i from (1- bytes) downto 0
+ until (zerop (logand mask
+ (ash #xff
+ (* 8 i))))
+ collect (ldb (byte 8 (* i 8)) net)))
+ (list name))))))
+ (setf tdom (string-downcase (stringify tdom)))
+ (dotimes (i (ipnet-hosts tnet))
+ (unless (zerop i)
+ (let* ((addr (ipnet-host tnet i))
+ (tail (join-strings #\.
+ (loop
+ for i from 0 below bytes
+ collect
+ (logand #xff
+ (ash addr (* 8 i)))))))
+ (rec :name (format nil "~A.~A" tail name)
+ :type :cname
+ :data (format nil "~A.~A" tail tdom))))))))))
;;;--------------------------------------------------------------------------
;;; Zone file output.