xref: /illumos-gate/usr/src/cmd/logadm/conf.c (revision 48edc7cf07b5dccc3ad84bf2dafe4150bd666d60)
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 
266 		/*
267 		 * If there is no next token on the line, make sure that
268 		 * we get a non-NULL Args array.
269 		 */
270 		if (Args == NULL)
271 			fillargs(NULL);
272 
273 		Args[ArgsI] = NULL;
274 
275 		LOCAL_ERR_BEGIN {
276 			if (SETJMP) {
277 				err(EF_FILE, "cannot process invalid entry %s",
278 				    entry);
279 				ret = 0;
280 				LOCAL_ERR_BREAK;
281 			}
282 
283 			if (timescan) {
284 				/* append to config options */
285 				cp = lut_lookup(Conflut, entry);
286 				if (cp != NULL) {
287 					opts = cp->cf_opts;
288 				}
289 			}
290 			opts = opts_parse(opts, Args, OPTF_CONF);
291 			if (!timescan || cp == NULL) {
292 				/*
293 				 * If we're not doing timescan, we track this
294 				 * entry.  If we are doing timescan and have
295 				 * what looks like an orphaned entry (cp ==
296 				 * NULL) then we also have to track. See the
297 				 * comment in rotatelog. We need to allow for
298 				 * the case where the logname is not the same as
299 				 * the log file name.
300 				 */
301 				fillconflist(lineno, entry, opts, comment, 0);
302 			}
303 		LOCAL_ERR_END }
304 
305 		if (ret == 1 && opts && opts_optarg(opts, "P") != NULL)
306 			ret = 3;
307 	}
308 
309 	err_fileline(NULL, 0);
310 	return (ret);
311 }
312 
313 /*
314  * conf_open -- open the configuration file, lock it if we have write perms
315  */
316 int
317 conf_open(const char *cfname, const char *tfname, struct opts *cliopts)
318 {
319 	struct stat stbuf1, stbuf2, stbuf3;
320 	struct flock	flock;
321 	int ret;
322 
323 	Confname = cfname;
324 	Timesname = tfname;
325 	Confentries = fn_list_new(NULL);
326 	Changed = CHG_NONE;
327 
328 	Changing = CHG_TIMES;
329 	if (opts_count(cliopts, "Vn") != 0)
330 		Changing = CHG_NONE;
331 	else if (opts_count(cliopts, "rw") != 0)
332 		Changing = CHG_BOTH;
333 
334 	Singlefile = strcmp(Confname, Timesname) == 0;
335 	if (Singlefile && Changing == CHG_TIMES)
336 		Changing = CHG_BOTH;
337 
338 	/* special case this so we don't even try locking the file */
339 	if (strcmp(Confname, "/dev/null") == 0)
340 		return (0);
341 
342 	while (Conffd == -1) {
343 		Canchange = CHG_BOTH;
344 		if ((Conffd = open(Confname, O_RDWR)) < 0) {
345 			if (Changing == CHG_BOTH)
346 				err(EF_SYS, "open %s", Confname);
347 			Canchange = CHG_TIMES;
348 			if ((Conffd = open(Confname, O_RDONLY)) < 0)
349 				err(EF_SYS, "open %s", Confname);
350 		}
351 
352 		flock.l_type = (Canchange == CHG_BOTH) ? F_WRLCK : F_RDLCK;
353 		flock.l_whence = SEEK_SET;
354 		flock.l_start = 0;
355 		flock.l_len = 1;
356 		if (fcntl(Conffd, F_SETLKW, &flock) < 0)
357 			err(EF_SYS, "flock on %s", Confname);
358 
359 		/* wait until after file is locked to get filesize */
360 		if (fstat(Conffd, &stbuf1) < 0)
361 			err(EF_SYS, "fstat on %s", Confname);
362 
363 		/* verify that we've got a lock on the active file */
364 		if (stat(Confname, &stbuf2) < 0 ||
365 		    !(stbuf2.st_dev == stbuf1.st_dev &&
366 		    stbuf2.st_ino == stbuf1.st_ino)) {
367 			/* wrong config file, try again */
368 			(void) close(Conffd);
369 			Conffd = -1;
370 		}
371 	}
372 
373 	while (!Singlefile && Timesfd == -1) {
374 		if ((Timesfd = open(Timesname, O_CREAT|O_RDWR, 0644)) < 0) {
375 			if (Changing != CHG_NONE)
376 				err(EF_SYS, "open %s", Timesname);
377 			Canchange = CHG_NONE;
378 			if ((Timesfd = open(Timesname, O_RDONLY)) < 0)
379 				err(EF_SYS, "open %s", Timesname);
380 		}
381 
382 		flock.l_type = (Canchange != CHG_NONE) ? F_WRLCK : F_RDLCK;
383 		flock.l_whence = SEEK_SET;
384 		flock.l_start = 0;
385 		flock.l_len = 1;
386 		if (fcntl(Timesfd, F_SETLKW, &flock) < 0)
387 			err(EF_SYS, "flock on %s", Timesname);
388 
389 		/* wait until after file is locked to get filesize */
390 		if (fstat(Timesfd, &stbuf2) < 0)
391 			err(EF_SYS, "fstat on %s", Timesname);
392 
393 		/* verify that we've got a lock on the active file */
394 		if (stat(Timesname, &stbuf3) < 0 ||
395 		    !(stbuf2.st_dev == stbuf3.st_dev &&
396 		    stbuf2.st_ino == stbuf3.st_ino)) {
397 			/* wrong timestamp file, try again */
398 			(void) close(Timesfd);
399 			Timesfd = -1;
400 			continue;
401 		}
402 
403 		/* check that Timesname isn't an alias for Confname */
404 		if (stbuf2.st_dev == stbuf1.st_dev &&
405 		    stbuf2.st_ino == stbuf1.st_ino)
406 			err(0, "Timestamp file %s can't refer to "
407 			    "Configuration file %s", Timesname, Confname);
408 	}
409 
410 	Conflen = stbuf1.st_size;
411 	Timeslen = stbuf2.st_size;
412 
413 	if (Conflen == 0)
414 		return (1);	/* empty file, don't bother parsing it */
415 
416 	if ((Confbuf = (char *)mmap(0, Conflen,
417 	    PROT_READ | PROT_WRITE, MAP_PRIVATE, Conffd, 0)) == (char *)-1)
418 		err(EF_SYS, "mmap on %s", Confname);
419 
420 	ret = conf_scan(Confname, Confbuf, Conflen, 0);
421 	if (ret == 3 && !Singlefile && Canchange == CHG_BOTH) {
422 		/*
423 		 * arrange to transfer any timestamps
424 		 * from conf_file to timestamps_file
425 		 */
426 		Changing = Changed = CHG_BOTH;
427 	}
428 
429 	if (Timesfd != -1 && Timeslen != 0) {
430 		if ((Timesbuf = (char *)mmap(0, Timeslen,
431 		    PROT_READ | PROT_WRITE, MAP_PRIVATE,
432 		    Timesfd, 0)) == (char *)-1)
433 			err(EF_SYS, "mmap on %s", Timesname);
434 		ret &= conf_scan(Timesname, Timesbuf, Timeslen, 1);
435 	}
436 
437 	/*
438 	 * possible future enhancement:  go through and mark any entries:
439 	 * 		logfile -P <date>
440 	 * as DELETED if the logfile doesn't exist
441 	 */
442 
443 	return (ret);
444 }
445 
446 /*
447  * conf_close -- close the configuration file
448  */
449 void
450 conf_close(struct opts *opts)
451 {
452 	char cuname[PATH_MAX], tuname[PATH_MAX];
453 	int cfd, tfd;
454 	FILE *cfp = NULL, *tfp = NULL;
455 	boolean_t safe_update = B_TRUE;
456 
457 	if (Changed == CHG_NONE || opts_count(opts, "n") != 0) {
458 		if (opts_count(opts, "v"))
459 			(void) out("# %s and %s unchanged\n",
460 			    Confname, Timesname);
461 		goto cleanup;
462 	}
463 
464 	if (Debug > 1) {
465 		(void) fprintf(stderr, "conf_close, saving logadm context:\n");
466 		conf_print(stderr, NULL);
467 	}
468 
469 	cuname[0] = tuname[0] = '\0';
470 	LOCAL_ERR_BEGIN {
471 		if (SETJMP) {
472 			safe_update = B_FALSE;
473 			LOCAL_ERR_BREAK;
474 		}
475 		if (Changed == CHG_BOTH) {
476 			if (Canchange != CHG_BOTH)
477 				err(EF_JMP, "internal error: attempting "
478 				    "to update %s without locking", Confname);
479 			(void) snprintf(cuname, sizeof (cuname), "%sXXXXXX",
480 			    Confname);
481 			if ((cfd = mkstemp(cuname)) == -1)
482 				err(EF_SYS|EF_JMP, "open %s replacement",
483 				    Confname);
484 			if (opts_count(opts, "v"))
485 				(void) out("# writing changes to %s\n", cuname);
486 			if (fchmod(cfd, 0644) == -1)
487 				err(EF_SYS|EF_JMP, "chmod %s", cuname);
488 			if ((cfp = fdopen(cfd, "w")) == NULL)
489 				err(EF_SYS|EF_JMP, "fdopen on %s", cuname);
490 		} else {
491 			/* just toss away the configuration data */
492 			cfp = fopen("/dev/null", "w");
493 		}
494 		if (!Singlefile) {
495 			if (Canchange == CHG_NONE)
496 				err(EF_JMP, "internal error: attempting "
497 				    "to update %s without locking", Timesname);
498 			(void) snprintf(tuname, sizeof (tuname), "%sXXXXXX",
499 			    Timesname);
500 			if ((tfd = mkstemp(tuname)) == -1)
501 				err(EF_SYS|EF_JMP, "open %s replacement",
502 				    Timesname);
503 			if (opts_count(opts, "v"))
504 				(void) out("# writing changes to %s\n", tuname);
505 			if (fchmod(tfd, 0644) == -1)
506 				err(EF_SYS|EF_JMP, "chmod %s", tuname);
507 			if ((tfp = fdopen(tfd, "w")) == NULL)
508 				err(EF_SYS|EF_JMP, "fdopen on %s", tuname);
509 		}
510 
511 		conf_print(cfp, tfp);
512 		if (fclose(cfp) < 0)
513 			err(EF_SYS|EF_JMP, "fclose on %s", Confname);
514 		if (tfp != NULL && fclose(tfp) < 0)
515 			err(EF_SYS|EF_JMP, "fclose on %s", Timesname);
516 	LOCAL_ERR_END }
517 
518 	if (!safe_update) {
519 		if (cuname[0] != 0)
520 			(void) unlink(cuname);
521 		if (tuname[0] != 0)
522 			(void) unlink(tuname);
523 		err(EF_JMP, "unsafe to update configuration file "
524 		    "or timestamps");
525 		return;
526 	}
527 
528 	/* rename updated files into place */
529 	if (cuname[0] != '\0')
530 		if (rename(cuname, Confname) < 0)
531 			err(EF_SYS, "rename %s to %s", cuname, Confname);
532 	if (tuname[0] != '\0')
533 		if (rename(tuname, Timesname) < 0)
534 			err(EF_SYS, "rename %s to %s", tuname, Timesname);
535 	Changed = CHG_NONE;
536 
537 cleanup:
538 	if (Conffd != -1) {
539 		(void) close(Conffd);
540 		Conffd = -1;
541 	}
542 	if (Timesfd != -1) {
543 		(void) close(Timesfd);
544 		Timesfd = -1;
545 	}
546 	if (Conflut) {
547 		lut_free(Conflut, free);
548 		Conflut = NULL;
549 	}
550 	if (Confentries) {
551 		fn_list_free(Confentries);
552 		Confentries = NULL;
553 	}
554 }
555 
556 /*
557  * conf_lookup -- lookup an entry in the config file
558  */
559 void *
560 conf_lookup(const char *lhs)
561 {
562 	struct confinfo *cp = lut_lookup(Conflut, lhs);
563 
564 	if (cp != NULL)
565 		err_fileline(Confname, cp->cf_lineno);
566 	return (cp);
567 }
568 
569 /*
570  * conf_opts -- return the parsed opts for an entry
571  */
572 struct opts *
573 conf_opts(const char *lhs)
574 {
575 	struct confinfo *cp = lut_lookup(Conflut, lhs);
576 
577 	if (cp != NULL)
578 		return (cp->cf_opts);
579 	return (opts_parse(NULL, NULL, OPTF_CONF));
580 }
581 
582 /*
583  * conf_replace -- replace an entry in the config file
584  */
585 void
586 conf_replace(const char *lhs, struct opts *newopts)
587 {
588 	struct confinfo *cp = lut_lookup(Conflut, lhs);
589 
590 	if (Conffd == -1)
591 		return;
592 
593 	if (cp != NULL) {
594 		cp->cf_opts = newopts;
595 		/* cp->cf_args = NULL; */
596 		if (newopts == NULL)
597 			cp->cf_flags |= CONFF_DELETED;
598 	} else
599 		fillconflist(0, lhs, newopts, NULL, 0);
600 
601 	Changed = CHG_BOTH;
602 }
603 
604 /*
605  * conf_set -- set options for an entry in the config file
606  */
607 void
608 conf_set(const char *entry, char *o, const char *optarg)
609 {
610 	struct confinfo *cp = lut_lookup(Conflut, entry);
611 
612 	if (Conffd == -1)
613 		return;
614 
615 	if (cp != NULL) {
616 		cp->cf_flags &= ~CONFF_DELETED;
617 	} else {
618 		fillconflist(0, STRDUP(entry),
619 		    opts_parse(NULL, NULL, OPTF_CONF), NULL, 0);
620 		if ((cp = lut_lookup(Conflut, entry)) == NULL)
621 			err(0, "conf_set internal error");
622 	}
623 	(void) opts_set(cp->cf_opts, o, optarg);
624 	if (strcmp(o, "P") == 0)
625 		Changed |= CHG_TIMES;
626 	else
627 		Changed = CHG_BOTH;
628 }
629 
630 /*
631  * conf_entries -- list all the entry names
632  */
633 struct fn_list *
634 conf_entries(void)
635 {
636 	return (Confentries);
637 }
638 
639 /* print the config file */
640 static void
641 conf_print(FILE *cstream, FILE *tstream)
642 {
643 	struct confinfo *cp;
644 	char *exclude_opts = "PFfhnrvVw";
645 	const char *timestamp;
646 
647 	if (tstream == NULL) {
648 		exclude_opts++;		/* -P option goes to config file */
649 	} else {
650 		(void) fprintf(tstream, gettext(
651 		    "# This file holds internal data for logadm(1M).\n"
652 		    "# Do not edit.\n"));
653 	}
654 	for (cp = Confinfo; cp; cp = cp->cf_next) {
655 		if (cp->cf_flags & CONFF_DELETED)
656 			continue;
657 		if (cp->cf_entry) {
658 			opts_printword(cp->cf_entry, cstream);
659 			if (cp->cf_opts)
660 				opts_print(cp->cf_opts, cstream, exclude_opts);
661 			/* output timestamps to tstream */
662 			if (tstream != NULL && (timestamp =
663 			    opts_optarg(cp->cf_opts, "P")) != NULL) {
664 				opts_printword(cp->cf_entry, tstream);
665 				(void) fprintf(tstream, " -P ");
666 				opts_printword(timestamp, tstream);
667 				(void) fprintf(tstream, "\n");
668 			}
669 		}
670 		if (cp->cf_com) {
671 			if (cp->cf_entry)
672 				(void) fprintf(cstream, " ");
673 			(void) fprintf(cstream, "#%s", cp->cf_com);
674 		}
675 		(void) fprintf(cstream, "\n");
676 	}
677 }
678 
679 #ifdef	TESTMODULE
680 
681 /*
682  * test main for conf module, usage: a.out conffile
683  */
684 int
685 main(int argc, char *argv[])
686 {
687 	struct opts *opts;
688 
689 	err_init(argv[0]);
690 	setbuf(stdout, NULL);
691 	opts_init(Opttable, Opttable_cnt);
692 
693 	opts = opts_parse(NULL, NULL, 0);
694 
695 	if (argc != 2)
696 		err(EF_RAW, "usage: %s conffile\n", argv[0]);
697 
698 	conf_open(argv[1], argv[1], opts);
699 
700 	printf("conffile <%s>:\n", argv[1]);
701 	conf_print(stdout, NULL);
702 
703 	conf_close(opts);
704 
705 	err_done(0);
706 	/* NOTREACHED */
707 	return (0);
708 }
709 
710 #endif	/* TESTMODULE */
711