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