xref: /illumos-gate/usr/src/cmd/logadm/conf.c (revision b6805bf78d2bbbeeaea8909a05623587b42d58b3)
1 /*
2  * CDDL HEADER START
3  *
4  * The contents of this file are subject to the terms of the
5  * Common Development and Distribution License (the "License").
6  * You may not use this file except in compliance with the License.
7  *
8  * You can obtain a copy of the license at usr/src/OPENSOLARIS.LICENSE
9  * or http://www.opensolaris.org/os/licensing.
10  * See the License for the specific language governing permissions
11  * and limitations under the License.
12  *
13  * When distributing Covered Code, include this CDDL HEADER in each
14  * file and include the License file at usr/src/OPENSOLARIS.LICENSE.
15  * If applicable, add the following below this CDDL HEADER, with the
16  * fields enclosed by brackets "[]" replaced with your own identifying
17  * information: Portions Copyright [yyyy] [name of copyright owner]
18  *
19  * CDDL HEADER END
20  */
21 
22 /*
23  * Copyright (c) 2001, 2010, Oracle and/or its affiliates. All rights reserved.
24  */
25 
26 /*
27  * logadm/conf.c -- configuration file module
28  */
29 
30 #include <stdio.h>
31 #include <libintl.h>
32 #include <fcntl.h>
33 #include <sys/types.h>
34 #include <sys/stat.h>
35 #include <sys/mman.h>
36 #include <ctype.h>
37 #include <strings.h>
38 #include <unistd.h>
39 #include <stdlib.h>
40 #include <limits.h>
41 #include "err.h"
42 #include "lut.h"
43 #include "fn.h"
44 #include "opts.h"
45 #include "conf.h"
46 
47 /* forward declarations of functions private to this module */
48 static void fillconflist(int lineno, const char *entry,
49     struct opts *opts, const char *com, int flags);
50 static void fillargs(char *arg);
51 static char *nexttok(char **ptrptr);
52 static void conf_print(FILE *cstream, FILE *tstream);
53 
54 static const char *Confname;	/* name of the confile file */
55 static int Conffd = -1;		/* file descriptor for config file */
56 static char *Confbuf;		/* copy of the config file (a la mmap()) */
57 static int Conflen;		/* length of mmap'd config file area */
58 static const char *Timesname;	/* name of the timestamps file */
59 static int Timesfd = -1;	/* file descriptor for timestamps file */
60 static char *Timesbuf;		/* copy of the timestamps file (a la mmap()) */
61 static int Timeslen;		/* length of mmap'd timestamps area */
62 static int Singlefile;		/* Conf and Times in the same file */
63 static int Changed;		/* what changes need to be written back */
64 static int Canchange;		/* what changes can be written back */
65 static int Changing;		/* what changes have been requested */
66 #define	CHG_NONE	0
67 #define	CHG_TIMES	1
68 #define	CHG_BOTH	3
69 
70 /*
71  * our structured representation of the configuration file
72  * is made up of a list of these
73  */
74 struct confinfo {
75 	struct confinfo *cf_next;
76 	int cf_lineno;		/* line number in file */
77 	const char *cf_entry;	/* name of entry, if line has an entry */
78 	struct opts *cf_opts;	/* parsed rhs of entry */
79 	const char *cf_com;	/* any comment text found */
80 	int cf_flags;
81 };
82 
83 #define	CONFF_DELETED	1	/* entry should be deleted on write back */
84 
85 static struct confinfo *Confinfo;	/* the entries in the config file */
86 static struct confinfo *Confinfolast;	/* end of list */
87 static struct lut *Conflut;		/* lookup table keyed by entry name */
88 static struct fn_list *Confentries;	/* list of valid entry names */
89 
90 /* allocate & fill in another entry in our list */
91 static void
92 fillconflist(int lineno, const char *entry,
93     struct opts *opts, const char *com, int flags)
94 {
95 	struct confinfo *cp = MALLOC(sizeof (*cp));
96 
97 	cp->cf_next = NULL;
98 	cp->cf_lineno = lineno;
99 	cp->cf_entry = entry;
100 	cp->cf_opts = opts;
101 	cp->cf_com = com;
102 	cp->cf_flags = flags;
103 	if (entry != NULL) {
104 		Conflut = lut_add(Conflut, entry, cp);
105 		fn_list_adds(Confentries, entry);
106 	}
107 	if (Confinfo == NULL)
108 		Confinfo = Confinfolast = cp;
109 	else {
110 		Confinfolast->cf_next = cp;
111 		Confinfolast = cp;
112 	}
113 }
114 
115 static char **Args;	/* static buffer for args */
116 static int ArgsN;	/* size of our static buffer */
117 static int ArgsI;	/* index into Cmdargs as we walk table */
118 #define	CONF_ARGS_INC	1024
119 
120 /* callback for lut_walk to build a cmdargs vector */
121 static void
122 fillargs(char *arg)
123 {
124 	if (ArgsI >= ArgsN) {
125 		/* need bigger table */
126 		Args = REALLOC(Args, sizeof (char *) * (ArgsN + CONF_ARGS_INC));
127 		ArgsN += CONF_ARGS_INC;
128 	}
129 	Args[ArgsI++] = arg;
130 }
131 
132 /* isolate and return the next token */
133 static char *
134 nexttok(char **ptrptr)
135 {
136 	char *ptr = *ptrptr;
137 	char *eptr;
138 	char *quote = NULL;
139 
140 	while (*ptr && isspace(*ptr))
141 		ptr++;
142 
143 	if (*ptr == '"' || *ptr == '\'')
144 		quote = ptr++;
145 
146 	for (eptr = ptr; *eptr; eptr++)
147 		if (quote && *eptr == *quote) {
148 			/* found end quote */
149 			*eptr++ = '\0';
150 			*ptrptr = eptr;
151 			return (ptr);
152 		} else if (!quote && isspace(*eptr)) {
153 			/* found end of unquoted area */
154 			*eptr++ = '\0';
155 			*ptrptr = eptr;
156 			return (ptr);
157 		}
158 
159 	if (quote != NULL)
160 		err(EF_FILE|EF_JMP, "Unbalanced %c quote", *quote);
161 		/*NOTREACHED*/
162 
163 	*ptrptr = eptr;
164 
165 	if (ptr == eptr)
166 		return (NULL);
167 	else
168 		return (ptr);
169 }
170 
171 /*
172  * scan the memory image of a file
173  *	returns: 0: error, 1: ok, 3: -P option found
174  */
175 static int
176 conf_scan(const char *fname, char *buf, int buflen, int timescan,
177     struct opts *cliopts)
178 {
179 	int ret = 1;
180 	int lineno = 0;
181 	char *line;
182 	char *eline;
183 	char *ebuf;
184 	char *entry, *comment;
185 
186 	ebuf = &buf[buflen];
187 
188 	if (buf[buflen - 1] != '\n')
189 		err(EF_WARN|EF_FILE, "file %s doesn't end with newline, "
190 		    "last line ignored.", fname);
191 
192 	for (line = buf; line < ebuf; line = eline) {
193 		char *ap;
194 		struct opts *opts = NULL;
195 		struct confinfo *cp;
196 
197 		lineno++;
198 		err_fileline(fname, lineno);
199 		eline = line;
200 		comment = NULL;
201 		for (; eline < ebuf; eline++) {
202 			/* check for continued lines */
203 			if (comment == NULL && *eline == '\\' &&
204 			    eline + 1 < ebuf && *(eline + 1) == '\n') {
205 				*eline = ' ';
206 				*(eline + 1) = ' ';
207 				lineno++;
208 				err_fileline(fname, lineno);
209 				continue;
210 			}
211 
212 			/* check for comments */
213 			if (comment == NULL && *eline == '#') {
214 				*eline = '\0';
215 				comment = (eline + 1);
216 				continue;
217 			}
218 
219 			/* check for end of line */
220 			if (*eline == '\n')
221 				break;
222 		}
223 		if (comment >= ebuf)
224 			comment = NULL;
225 		if (eline >= ebuf) {
226 			/* discard trailing unterminated line */
227 			continue;
228 		}
229 		*eline++ = '\0';
230 
231 		/*
232 		 * now we have the entry, if any, at "line"
233 		 * and the comment, if any, at "comment"
234 		 */
235 
236 		/* entry is first token */
237 		entry = nexttok(&line);
238 		if (entry == NULL) {
239 			/* it's just a comment line */
240 			if (!timescan)
241 				fillconflist(lineno, entry, NULL, comment, 0);
242 			continue;
243 		}
244 		if (strcmp(entry, "logadm-version") == 0) {
245 			/*
246 			 * we somehow opened some future format
247 			 * conffile that we likely don't understand.
248 			 * if the given version is "1" then go on,
249 			 * otherwise someone is mixing versions
250 			 * and we can't help them other than to
251 			 * print an error and exit.
252 			 */
253 			if ((entry = nexttok(&line)) != NULL &&
254 			    strcmp(entry, "1") != 0)
255 				err(0, "%s version not supported "
256 				    "by this version of logadm.",
257 				    fname);
258 			continue;
259 		}
260 
261 		/* form an argv array */
262 		ArgsI = 0;
263 		while (ap = nexttok(&line))
264 			fillargs(ap);
265 		Args[ArgsI] = NULL;
266 
267 		LOCAL_ERR_BEGIN {
268 			if (SETJMP) {
269 				err(EF_FILE, "cannot process invalid entry %s",
270 				    entry);
271 				ret = 0;
272 				LOCAL_ERR_BREAK;
273 			}
274 
275 			if (timescan) {
276 				/* append to config options */
277 				cp = lut_lookup(Conflut, entry);
278 				if (cp == NULL) {
279 					/* orphaned entry */
280 					if (opts_count(cliopts, "v"))
281 						err(EF_FILE, "stale timestamp "
282 						    "for %s", entry);
283 					LOCAL_ERR_BREAK;
284 				}
285 				opts = cp->cf_opts;
286 			}
287 			opts = opts_parse(opts, Args, OPTF_CONF);
288 			if (!timescan) {
289 				fillconflist(lineno, entry, opts, comment, 0);
290 			}
291 		LOCAL_ERR_END }
292 
293 		if (ret == 1 && opts && opts_optarg(opts, "P") != NULL)
294 			ret = 3;
295 	}
296 
297 	err_fileline(NULL, 0);
298 	return (ret);
299 }
300 
301 /*
302  * conf_open -- open the configuration file, lock it if we have write perms
303  */
304 int
305 conf_open(const char *cfname, const char *tfname, struct opts *cliopts)
306 {
307 	struct stat stbuf1, stbuf2, stbuf3;
308 	struct flock	flock;
309 	int ret;
310 
311 	Confname = cfname;
312 	Timesname = tfname;
313 	Confentries = fn_list_new(NULL);
314 	Changed = CHG_NONE;
315 
316 	Changing = CHG_TIMES;
317 	if (opts_count(cliopts, "Vn") != 0)
318 		Changing = CHG_NONE;
319 	else if (opts_count(cliopts, "rw") != 0)
320 		Changing = CHG_BOTH;
321 
322 	Singlefile = strcmp(Confname, Timesname) == 0;
323 	if (Singlefile && Changing == CHG_TIMES)
324 		Changing = CHG_BOTH;
325 
326 	/* special case this so we don't even try locking the file */
327 	if (strcmp(Confname, "/dev/null") == 0)
328 		return (0);
329 
330 	while (Conffd == -1) {
331 		Canchange = CHG_BOTH;
332 		if ((Conffd = open(Confname, O_RDWR)) < 0) {
333 			if (Changing == CHG_BOTH)
334 				err(EF_SYS, "open %s", Confname);
335 			Canchange = CHG_TIMES;
336 			if ((Conffd = open(Confname, O_RDONLY)) < 0)
337 				err(EF_SYS, "open %s", Confname);
338 		}
339 
340 		flock.l_type = (Canchange == CHG_BOTH) ? F_WRLCK : F_RDLCK;
341 		flock.l_whence = SEEK_SET;
342 		flock.l_start = 0;
343 		flock.l_len = 1;
344 		if (fcntl(Conffd, F_SETLKW, &flock) < 0)
345 			err(EF_SYS, "flock on %s", Confname);
346 
347 		/* wait until after file is locked to get filesize */
348 		if (fstat(Conffd, &stbuf1) < 0)
349 			err(EF_SYS, "fstat on %s", Confname);
350 
351 		/* verify that we've got a lock on the active file */
352 		if (stat(Confname, &stbuf2) < 0 ||
353 		    !(stbuf2.st_dev == stbuf1.st_dev &&
354 		    stbuf2.st_ino == stbuf1.st_ino)) {
355 			/* wrong config file, try again */
356 			(void) close(Conffd);
357 			Conffd = -1;
358 		}
359 	}
360 
361 	while (!Singlefile && Timesfd == -1) {
362 		if ((Timesfd = open(Timesname, O_CREAT|O_RDWR, 0644)) < 0) {
363 			if (Changing != CHG_NONE)
364 				err(EF_SYS, "open %s", Timesname);
365 			Canchange = CHG_NONE;
366 			if ((Timesfd = open(Timesname, O_RDONLY)) < 0)
367 				err(EF_SYS, "open %s", Timesname);
368 		}
369 
370 		flock.l_type = (Canchange != CHG_NONE) ? F_WRLCK : F_RDLCK;
371 		flock.l_whence = SEEK_SET;
372 		flock.l_start = 0;
373 		flock.l_len = 1;
374 		if (fcntl(Timesfd, F_SETLKW, &flock) < 0)
375 			err(EF_SYS, "flock on %s", Timesname);
376 
377 		/* wait until after file is locked to get filesize */
378 		if (fstat(Timesfd, &stbuf2) < 0)
379 			err(EF_SYS, "fstat on %s", Timesname);
380 
381 		/* verify that we've got a lock on the active file */
382 		if (stat(Timesname, &stbuf3) < 0 ||
383 		    !(stbuf2.st_dev == stbuf3.st_dev &&
384 		    stbuf2.st_ino == stbuf3.st_ino)) {
385 			/* wrong timestamp file, try again */
386 			(void) close(Timesfd);
387 			Timesfd = -1;
388 			continue;
389 		}
390 
391 		/* check that Timesname isn't an alias for Confname */
392 		if (stbuf2.st_dev == stbuf1.st_dev &&
393 		    stbuf2.st_ino == stbuf1.st_ino)
394 			err(0, "Timestamp file %s can't refer to "
395 			    "Configuration file %s", Timesname, Confname);
396 	}
397 
398 	Conflen = stbuf1.st_size;
399 	Timeslen = stbuf2.st_size;
400 
401 	if (Conflen == 0)
402 		return (1);	/* empty file, don't bother parsing it */
403 
404 	if ((Confbuf = (char *)mmap(0, Conflen,
405 	    PROT_READ | PROT_WRITE, MAP_PRIVATE, Conffd, 0)) == (char *)-1)
406 		err(EF_SYS, "mmap on %s", Confname);
407 
408 	ret = conf_scan(Confname, Confbuf, Conflen, 0, cliopts);
409 	if (ret == 3 && !Singlefile && Canchange == CHG_BOTH) {
410 		/*
411 		 * arrange to transfer any timestamps
412 		 * from conf_file to timestamps_file
413 		 */
414 		Changing = Changed = CHG_BOTH;
415 	}
416 
417 	if (Timesfd != -1 && Timeslen != 0) {
418 		if ((Timesbuf = (char *)mmap(0, Timeslen,
419 		    PROT_READ | PROT_WRITE, MAP_PRIVATE,
420 		    Timesfd, 0)) == (char *)-1)
421 			err(EF_SYS, "mmap on %s", Timesname);
422 		ret &= conf_scan(Timesname, Timesbuf, Timeslen, 1, cliopts);
423 	}
424 
425 	/*
426 	 * possible future enhancement:  go through and mark any entries:
427 	 * 		logfile -P <date>
428 	 * as DELETED if the logfile doesn't exist
429 	 */
430 
431 	return (ret);
432 }
433 
434 /*
435  * conf_close -- close the configuration file
436  */
437 void
438 conf_close(struct opts *opts)
439 {
440 	char cuname[PATH_MAX], tuname[PATH_MAX];
441 	int cfd, tfd;
442 	FILE *cfp = NULL, *tfp = NULL;
443 	boolean_t safe_update = B_TRUE;
444 
445 	if (Changed == CHG_NONE || opts_count(opts, "n") != 0) {
446 		if (opts_count(opts, "v"))
447 			(void) out("# %s and %s unchanged\n",
448 			    Confname, Timesname);
449 		goto cleanup;
450 	}
451 
452 	if (Debug > 1) {
453 		(void) fprintf(stderr, "conf_close, saving logadm context:\n");
454 		conf_print(stderr, NULL);
455 	}
456 
457 	cuname[0] = tuname[0] = '\0';
458 	LOCAL_ERR_BEGIN {
459 		if (SETJMP) {
460 			safe_update = B_FALSE;
461 			LOCAL_ERR_BREAK;
462 		}
463 		if (Changed == CHG_BOTH) {
464 			if (Canchange != CHG_BOTH)
465 				err(EF_JMP, "internal error: attempting "
466 				    "to update %s without locking", Confname);
467 			(void) snprintf(cuname, sizeof (cuname), "%sXXXXXX",
468 			    Confname);
469 			if ((cfd = mkstemp(cuname)) == -1)
470 				err(EF_SYS|EF_JMP, "open %s replacement",
471 				    Confname);
472 			if (opts_count(opts, "v"))
473 				(void) out("# writing changes to %s\n", cuname);
474 			if (fchmod(cfd, 0644) == -1)
475 				err(EF_SYS|EF_JMP, "chmod %s", cuname);
476 			if ((cfp = fdopen(cfd, "w")) == NULL)
477 				err(EF_SYS|EF_JMP, "fdopen on %s", cuname);
478 		} else {
479 			/* just toss away the configuration data */
480 			cfp = fopen("/dev/null", "w");
481 		}
482 		if (!Singlefile) {
483 			if (Canchange == CHG_NONE)
484 				err(EF_JMP, "internal error: attempting "
485 				    "to update %s without locking", Timesname);
486 			(void) snprintf(tuname, sizeof (tuname), "%sXXXXXX",
487 			    Timesname);
488 			if ((tfd = mkstemp(tuname)) == -1)
489 				err(EF_SYS|EF_JMP, "open %s replacement",
490 				    Timesname);
491 			if (opts_count(opts, "v"))
492 				(void) out("# writing changes to %s\n", tuname);
493 			if (fchmod(tfd, 0644) == -1)
494 				err(EF_SYS|EF_JMP, "chmod %s", tuname);
495 			if ((tfp = fdopen(tfd, "w")) == NULL)
496 				err(EF_SYS|EF_JMP, "fdopen on %s", tuname);
497 		}
498 
499 		conf_print(cfp, tfp);
500 		if (fclose(cfp) < 0)
501 			err(EF_SYS|EF_JMP, "fclose on %s", Confname);
502 		if (tfp != NULL && fclose(tfp) < 0)
503 			err(EF_SYS|EF_JMP, "fclose on %s", Timesname);
504 	LOCAL_ERR_END }
505 
506 	if (!safe_update) {
507 		if (cuname[0] != 0)
508 			(void) unlink(cuname);
509 		if (tuname[0] != 0)
510 			(void) unlink(tuname);
511 		err(EF_JMP, "unsafe to update configuration file "
512 		    "or timestamps");
513 		return;
514 	}
515 
516 	/* rename updated files into place */
517 	if (cuname[0] != '\0')
518 		if (rename(cuname, Confname) < 0)
519 			err(EF_SYS, "rename %s to %s", cuname, Confname);
520 	if (tuname[0] != '\0')
521 		if (rename(tuname, Timesname) < 0)
522 			err(EF_SYS, "rename %s to %s", tuname, Timesname);
523 	Changed = CHG_NONE;
524 
525 cleanup:
526 	if (Conffd != -1) {
527 		(void) close(Conffd);
528 		Conffd = -1;
529 	}
530 	if (Timesfd != -1) {
531 		(void) close(Timesfd);
532 		Timesfd = -1;
533 	}
534 	if (Conflut) {
535 		lut_free(Conflut, free);
536 		Conflut = NULL;
537 	}
538 	if (Confentries) {
539 		fn_list_free(Confentries);
540 		Confentries = NULL;
541 	}
542 }
543 
544 /*
545  * conf_lookup -- lookup an entry in the config file
546  */
547 void *
548 conf_lookup(const char *lhs)
549 {
550 	struct confinfo *cp = lut_lookup(Conflut, lhs);
551 
552 	if (cp != NULL)
553 		err_fileline(Confname, cp->cf_lineno);
554 	return (cp);
555 }
556 
557 /*
558  * conf_opts -- return the parsed opts for an entry
559  */
560 struct opts *
561 conf_opts(const char *lhs)
562 {
563 	struct confinfo *cp = lut_lookup(Conflut, lhs);
564 
565 	if (cp != NULL)
566 		return (cp->cf_opts);
567 	return (opts_parse(NULL, NULL, OPTF_CONF));
568 }
569 
570 /*
571  * conf_replace -- replace an entry in the config file
572  */
573 void
574 conf_replace(const char *lhs, struct opts *newopts)
575 {
576 	struct confinfo *cp = lut_lookup(Conflut, lhs);
577 
578 	if (Conffd == -1)
579 		return;
580 
581 	if (cp != NULL) {
582 		cp->cf_opts = newopts;
583 		/* cp->cf_args = NULL; */
584 		if (newopts == NULL)
585 			cp->cf_flags |= CONFF_DELETED;
586 	} else
587 		fillconflist(0, lhs, newopts, NULL, 0);
588 
589 	Changed = CHG_BOTH;
590 }
591 
592 /*
593  * conf_set -- set options for an entry in the config file
594  */
595 void
596 conf_set(const char *entry, char *o, const char *optarg)
597 {
598 	struct confinfo *cp = lut_lookup(Conflut, entry);
599 
600 	if (Conffd == -1)
601 		return;
602 
603 	if (cp != NULL) {
604 		cp->cf_flags &= ~CONFF_DELETED;
605 	} else {
606 		fillconflist(0, STRDUP(entry),
607 		    opts_parse(NULL, NULL, OPTF_CONF), NULL, 0);
608 		if ((cp = lut_lookup(Conflut, entry)) == NULL)
609 			err(0, "conf_set internal error");
610 	}
611 	(void) opts_set(cp->cf_opts, o, optarg);
612 	if (strcmp(o, "P") == 0)
613 		Changed |= CHG_TIMES;
614 	else
615 		Changed = CHG_BOTH;
616 }
617 
618 /*
619  * conf_entries -- list all the entry names
620  */
621 struct fn_list *
622 conf_entries(void)
623 {
624 	return (Confentries);
625 }
626 
627 /* print the config file */
628 static void
629 conf_print(FILE *cstream, FILE *tstream)
630 {
631 	struct confinfo *cp;
632 	char *exclude_opts = "PFfhnrvVw";
633 	const char *timestamp;
634 
635 	if (tstream == NULL) {
636 		exclude_opts++;		/* -P option goes to config file */
637 	} else {
638 		(void) fprintf(tstream, gettext(
639 		    "# This file holds internal data for logadm(1M).\n"
640 		    "# Do not edit.\n"));
641 	}
642 	for (cp = Confinfo; cp; cp = cp->cf_next) {
643 		if (cp->cf_flags & CONFF_DELETED)
644 			continue;
645 		if (cp->cf_entry) {
646 			opts_printword(cp->cf_entry, cstream);
647 			if (cp->cf_opts)
648 				opts_print(cp->cf_opts, cstream, exclude_opts);
649 			/* output timestamps to tstream */
650 			if (tstream != NULL && (timestamp =
651 			    opts_optarg(cp->cf_opts, "P")) != NULL) {
652 				opts_printword(cp->cf_entry, tstream);
653 				(void) fprintf(tstream, " -P ");
654 				opts_printword(timestamp, tstream);
655 				(void) fprintf(tstream, "\n");
656 			}
657 		}
658 		if (cp->cf_com) {
659 			if (cp->cf_entry)
660 				(void) fprintf(cstream, " ");
661 			(void) fprintf(cstream, "#%s", cp->cf_com);
662 		}
663 		(void) fprintf(cstream, "\n");
664 	}
665 }
666 
667 #ifdef	TESTMODULE
668 
669 /*
670  * test main for conf module, usage: a.out conffile
671  */
672 int
673 main(int argc, char *argv[])
674 {
675 	struct opts *opts;
676 
677 	err_init(argv[0]);
678 	setbuf(stdout, NULL);
679 	opts_init(Opttable, Opttable_cnt);
680 
681 	opts = opts_parse(NULL, NULL, 0);
682 
683 	if (argc != 2)
684 		err(EF_RAW, "usage: %s conffile\n", argv[0]);
685 
686 	conf_open(argv[1], argv[1], opts);
687 
688 	printf("conffile <%s>:\n", argv[1]);
689 	conf_print(stdout, NULL);
690 
691 	conf_close(opts);
692 
693 	err_done(0);
694 	/* NOTREACHED */
695 	return (0);
696 }
697 
698 #endif	/* TESTMODULE */
699