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, Version 1.0 only 6 * (the "License"). You may not use this file except in compliance 7 * with the License. 8 * 9 * You can obtain a copy of the license at usr/src/OPENSOLARIS.LICENSE 10 * or http://www.opensolaris.org/os/licensing. 11 * See the License for the specific language governing permissions 12 * and limitations under the License. 13 * 14 * When distributing Covered Code, include this CDDL HEADER in each 15 * file and include the License file at usr/src/OPENSOLARIS.LICENSE. 16 * If applicable, add the following below this CDDL HEADER, with the 17 * fields enclosed by brackets "[]" replaced with your own identifying 18 * information: Portions Copyright [yyyy] [name of copyright owner] 19 * 20 * CDDL HEADER END 21 */ 22 /* 23 * Copyright 2004 Sun Microsystems, Inc. All rights reserved. 24 * Use is subject to license terms. 25 * 26 * logadm/main.c -- main routines for logadm 27 * 28 * this program is 90% argument processing, 10% actions... 29 */ 30 31 #pragma ident "%Z%%M% %I% %E% SMI" 32 33 #include <stdio.h> 34 #include <stdlib.h> 35 #include <unistd.h> 36 #include <strings.h> 37 #include <libintl.h> 38 #include <locale.h> 39 #include <sys/types.h> 40 #include <sys/stat.h> 41 #include <sys/wait.h> 42 #include <sys/filio.h> 43 #include <time.h> 44 #include "err.h" 45 #include "lut.h" 46 #include "fn.h" 47 #include "opts.h" 48 #include "conf.h" 49 #include "glob.h" 50 #include "kw.h" 51 52 /* forward declarations for functions in this file */ 53 static void usage(const char *msg); 54 static void commajoin(const char *lhs, void *rhs, void *arg); 55 static void doaftercmd(const char *lhs, void *rhs, void *arg); 56 static void dologname(struct fn *fnp, struct opts *clopts); 57 static boolean_t rotatelog(struct fn *fnp, struct opts *opts); 58 static void rotateto(struct fn *fnp, struct opts *opts, int n, 59 struct fn *recentlog, boolean_t isgz); 60 static void expirefiles(struct fn *fnp, struct opts *opts); 61 static void dorm(struct opts *opts, const char *msg, struct fn *fnp); 62 static void docmd(struct opts *opts, const char *msg, const char *cmd, 63 const char *arg1, const char *arg2, const char *arg3); 64 65 /* our configuration file, unless otherwise specified by -f */ 66 static char *Default_conffile = "/etc/logadm.conf"; 67 68 /* default pathnames to the commands we invoke */ 69 static char *Sh = "/bin/sh"; 70 static char *Mv = "/bin/mv"; 71 static char *Cp = "/bin/cp"; 72 static char *Rm = "/bin/rm"; 73 static char *Touch = "/bin/touch"; 74 static char *Chmod = "/bin/chmod"; 75 static char *Chown = "/bin/chown"; 76 static char *Gzip = "/bin/gzip"; 77 static char *Mkdir = "/bin/mkdir"; 78 79 /* return from time(0), gathered early on to avoid slewed timestamps */ 80 time_t Now; 81 82 /* list of before commands that have been executed */ 83 static struct lut *Beforecmds; 84 85 /* list of after commands to execute before exiting */ 86 static struct lut *Aftercmds; 87 88 /* list of conffile entry names that are considered "done" */ 89 static struct lut *Donenames; 90 91 /* table that drives argument parsing */ 92 static struct optinfo Opttable[] = { 93 { "e", OPTTYPE_STRING, NULL, OPTF_CLI|OPTF_CONF }, 94 { "f", OPTTYPE_STRING, NULL, OPTF_CLI }, 95 { "h", OPTTYPE_BOOLEAN, NULL, OPTF_CLI }, 96 { "N", OPTTYPE_BOOLEAN, NULL, OPTF_CLI|OPTF_CONF }, 97 { "n", OPTTYPE_BOOLEAN, NULL, OPTF_CLI }, 98 { "r", OPTTYPE_BOOLEAN, NULL, OPTF_CLI }, 99 { "V", OPTTYPE_BOOLEAN, NULL, OPTF_CLI }, 100 { "v", OPTTYPE_BOOLEAN, NULL, OPTF_CLI }, 101 { "w", OPTTYPE_STRING, NULL, OPTF_CLI }, 102 { "p", OPTTYPE_INT, opts_parse_seconds, OPTF_CLI|OPTF_CONF }, 103 { "P", OPTTYPE_INT, opts_parse_ctime, OPTF_CLI|OPTF_CONF }, 104 { "s", OPTTYPE_INT, opts_parse_bytes, OPTF_CLI|OPTF_CONF }, 105 { "a", OPTTYPE_STRING, NULL, OPTF_CLI|OPTF_CONF }, 106 { "b", OPTTYPE_STRING, NULL, OPTF_CLI|OPTF_CONF }, 107 { "c", OPTTYPE_BOOLEAN, NULL, OPTF_CLI|OPTF_CONF }, 108 { "g", OPTTYPE_STRING, NULL, OPTF_CLI|OPTF_CONF }, 109 { "m", OPTTYPE_INT, opts_parse_atopi, OPTF_CLI|OPTF_CONF }, 110 { "M", OPTTYPE_STRING, NULL, OPTF_CLI|OPTF_CONF }, 111 { "o", OPTTYPE_STRING, NULL, OPTF_CLI|OPTF_CONF }, 112 { "R", OPTTYPE_STRING, NULL, OPTF_CLI|OPTF_CONF }, 113 { "t", OPTTYPE_STRING, NULL, OPTF_CLI|OPTF_CONF }, 114 { "z", OPTTYPE_INT, opts_parse_atopi, OPTF_CLI|OPTF_CONF }, 115 { "A", OPTTYPE_INT, opts_parse_seconds, OPTF_CLI|OPTF_CONF }, 116 { "C", OPTTYPE_INT, opts_parse_atopi, OPTF_CLI|OPTF_CONF }, 117 { "E", OPTTYPE_STRING, NULL, OPTF_CLI|OPTF_CONF }, 118 { "S", OPTTYPE_INT, opts_parse_bytes, OPTF_CLI|OPTF_CONF }, 119 { "T", OPTTYPE_STRING, NULL, OPTF_CLI|OPTF_CONF }, 120 }; 121 122 /* 123 * only the "fhnVv" options are allowed in the first form of this command, 124 * so this defines the list of options that are an error in they appear 125 * in the first form. In other words, it is not allowed to run logadm 126 * with any of these options unless at least one logname is also provided. 127 */ 128 #define OPTIONS_NOT_FIRST_FORM "eNrwpPsabcgmoRtzACEST" 129 130 /* text that we spew with the -h flag */ 131 #define HELP1 \ 132 "Usage: logadm [options]\n"\ 133 " (processes all entries in /etc/logadm.conf or conffile given by -f)\n"\ 134 " or: logadm [options] logname...\n"\ 135 " (processes the given lognames)\n"\ 136 "\n"\ 137 "General options:\n"\ 138 " -e mailaddr mail errors to given address\n"\ 139 " -f conffile use conffile instead of /etc/logadm.conf\n"\ 140 " -h display help\n"\ 141 " -N not an error if log file nonexistent\n"\ 142 " -n show actions, don't perform them\n"\ 143 " -r remove logname entry from conffile\n"\ 144 " -V ensure conffile entries exist, correct\n"\ 145 " -v print info about actions happening\n"\ 146 " -w entryname write entry to config file\n"\ 147 "\n"\ 148 "Options which control when a logfile is rotated:\n"\ 149 "(default is: -s1b -p1w if no -s or -p)\n"\ 150 " -p period only rotate if period passed since last rotate\n"\ 151 " -P timestamp used to store rotation date in conffile\n"\ 152 " -s size only rotate if given size or greater\n"\ 153 "\n" 154 #define HELP2 \ 155 "Options which control how a logfile is rotated:\n"\ 156 "(default is: -t '$file.$n', owner/group/mode taken from log file)\n"\ 157 " -a cmd execute cmd after taking actions\n"\ 158 " -b cmd execute cmd before taking actions\n"\ 159 " -c copy & truncate logfile, don't rename\n"\ 160 " -g group new empty log file group\n"\ 161 " -m mode new empty log file mode\n"\ 162 " -M cmd execute cmd to rotate the log file\n"\ 163 " -o owner new empty log file owner\n"\ 164 " -R cmd run cmd on file after rotate\n"\ 165 " -t template template for naming old logs\n"\ 166 " -z count gzip old logs except most recent count\n"\ 167 "\n"\ 168 "Options which control the expiration of old logfiles:\n"\ 169 "(default is: -C10 if no -A, -C, or -S)\n"\ 170 " -A age expire logs older than age\n"\ 171 " -C count expire old logs until count remain\n"\ 172 " -E cmd run cmd on file to expire\n"\ 173 " -S size expire until space used is below size \n"\ 174 " -T pattern pattern for finding old logs\n" 175 176 /* 177 * main -- where it all begins 178 */ 179 /*ARGSUSED*/ 180 int 181 main(int argc, char *argv[]) 182 { 183 struct opts *clopts; /* from parsing command line */ 184 const char *conffile; /* our configuration file */ 185 struct fn_list *lognames; /* list of lognames we're processing */ 186 struct fn *fnp; 187 char *val; 188 189 (void) setlocale(LC_ALL, ""); 190 191 #if !defined(TEXT_DOMAIN) 192 #define TEXT_DOMAIN "SYS_TEST" /* only used if Makefiles don't define it */ 193 #endif 194 195 (void) textdomain(TEXT_DOMAIN); 196 197 /* we only print times into the conffile, so make them uniform */ 198 (void) setlocale(LC_TIME, "C"); 199 200 /* give our name to error routines & skip it for arg parsing */ 201 err_init(*argv++); 202 (void) setlinebuf(stdout); 203 204 if (putenv("PATH=/bin")) 205 err(EF_SYS, "putenv PATH"); 206 if (putenv("TZ=UTC")) 207 err(EF_SYS, "putenv TZ"); 208 tzset(); 209 210 (void) umask(0); 211 212 Now = time(0); 213 214 /* check for (undocumented) debugging environment variables */ 215 if (val = getenv("_LOGADM_DEFAULT_CONFFILE")) 216 Default_conffile = val; 217 if (val = getenv("_LOGADM_DEBUG")) 218 Debug = atoi(val); 219 if (val = getenv("_LOGADM_SH")) 220 Sh = val; 221 if (val = getenv("_LOGADM_MV")) 222 Mv = val; 223 if (val = getenv("_LOGADM_CP")) 224 Cp = val; 225 if (val = getenv("_LOGADM_RM")) 226 Rm = val; 227 if (val = getenv("_LOGADM_TOUCH")) 228 Touch = val; 229 if (val = getenv("_LOGADM_CHMOD")) 230 Chmod = val; 231 if (val = getenv("_LOGADM_CHOWN")) 232 Chown = val; 233 if (val = getenv("_LOGADM_GZIP")) 234 Gzip = val; 235 if (val = getenv("_LOGADM_MKDIR")) 236 Mkdir = val; 237 238 opts_init(Opttable, sizeof (Opttable) / sizeof (struct optinfo)); 239 240 /* parse command line arguments */ 241 if (SETJMP) 242 usage("bailing out due to command line errors"); 243 else 244 clopts = opts_parse(argv, OPTF_CLI); 245 246 if (Debug) { 247 (void) fprintf(stderr, "command line opts:"); 248 opts_print(clopts, stderr, NULL); 249 (void) fprintf(stderr, "\n"); 250 } 251 252 /* 253 * There are many moods of logadm: 254 * 255 * 1. "-h" for help was given. We spew a canned help 256 * message and exit, regardless of any other options given. 257 * 258 * 2. "-r" or "-w" asking us to write to the conffile. Lots 259 * of argument checking, then we make the change to conffile 260 * and exit. (-r processing actually happens in dologname().) 261 * 262 * 3. "-V" to search/verify the conffile was given. We do 263 * the appropriate run through the conffile and exit. 264 * (-V processing actually happens in dologname().) 265 * 266 * 4. No lognames were given, so we're being asked to go through 267 * every entry in conffile. We verify that only the options 268 * that make sense for this form of the command are present 269 * and fall into the main processing loop below. 270 * 271 * 5. lognames were given, so we fall into the main processing 272 * loop below to work our way through them. 273 * 274 * The last two cases are where the option processing gets more 275 * complex. Each time around the main processing loop, we're 276 * in one of these cases: 277 * 278 * A. No cmdargs were found (we're in case 4), the entry 279 * in conffile supplies no log file names, so the entry 280 * name itself is the logfile name (or names, if it globs 281 * to multiple file names). 282 * 283 * B. No cmdargs were found (we're in case 4), the entry 284 * in conffile gives log file names that we then loop 285 * through and rotate/expire. In this case, the entry 286 * name is specifically NOT one of the log file names. 287 * 288 * C. We're going through the cmdargs (we're in case 5), 289 * the entry in conffile either doesn't exist or it exists 290 * but supplies no log file names, so the cmdarg itself 291 * is the log file name. 292 * 293 * D. We're going through the cmdargs (we're in case 5), 294 * a matching entry in conffile supplies log file names 295 * that we then loop through and rotate/expire. In this 296 * case the entry name is specifically NOT one of the log 297 * file names. 298 * 299 * As we're doing all this, any options given on the command line 300 * override any found in the conffile, and we apply the defaults 301 * for rotation conditions and expiration conditions, etc. at the 302 * last opportunity, when we're sure they haven't been overridden 303 * by an option somewhere along the way. 304 * 305 */ 306 307 /* help option overrides anything else */ 308 if (opts_count(clopts, "h")) { 309 (void) fputs(HELP1, stderr); 310 (void) fputs(HELP2, stderr); 311 err_done(0); 312 /*NOTREACHED*/ 313 } 314 315 /* detect illegal option combinations */ 316 if (opts_count(clopts, "rwV") > 1) 317 usage("Only one of -r, -w, or -V may be used at a time."); 318 if (opts_count(clopts, "cM") > 1) 319 usage("Only one of -c or -M may be used at a time."); 320 321 /* arrange for error output to be mailed if clopts includes -e */ 322 if (opts_count(clopts, "e")) 323 err_mailto(opts_optarg(clopts, "e")); 324 325 /* this implements the default conffile */ 326 if ((conffile = opts_optarg(clopts, "f")) == NULL) 327 conffile = Default_conffile; 328 if (opts_count(clopts, "v")) 329 (void) out("# loading %s\n", conffile); 330 conf_open(conffile, opts_count(clopts, "Vn") == 0); 331 332 /* handle conffile write option */ 333 if (opts_count(clopts, "w")) { 334 if (Debug) 335 (void) fprintf(stderr, 336 "main: add/replace conffile entry: <%s>\n", 337 opts_optarg(clopts, "w")); 338 conf_replace(opts_optarg(clopts, "w"), clopts); 339 conf_close(clopts); 340 err_done(0); 341 /*NOTREACHED*/ 342 } 343 344 /* 345 * lognames is either a list supplied on the command line, 346 * or every entry in the conffile if none were supplied. 347 */ 348 lognames = opts_cmdargs(clopts); 349 if (fn_list_empty(lognames)) { 350 /* 351 * being asked to do all entries in conffile 352 * 353 * check to see if any options were given that only 354 * make sense when lognames are given specifically 355 * on the command line. 356 */ 357 if (opts_count(clopts, OPTIONS_NOT_FIRST_FORM)) 358 usage("some options require logname argument"); 359 if (Debug) 360 (void) fprintf(stderr, 361 "main: run all entries in conffile\n"); 362 lognames = conf_entries(); 363 } 364 365 /* foreach logname... */ 366 fn_list_rewind(lognames); 367 while ((fnp = fn_list_next(lognames)) != NULL) { 368 if (lut_lookup(Donenames, fn_s(fnp)) != NULL) { 369 if (Debug) 370 (void) fprintf(stderr, 371 "main: logname already done: <%s>\n", 372 fn_s(fnp)); 373 continue; 374 } 375 if (SETJMP) 376 err(EF_FILE, "bailing out on logname \"%s\" " 377 "due to errors", fn_s(fnp)); 378 else 379 dologname(fnp, clopts); 380 } 381 382 /* execute any after commands */ 383 lut_walk(Aftercmds, doaftercmd, clopts); 384 385 /* write out any conffile changes */ 386 conf_close(clopts); 387 388 err_done(0); 389 /*NOTREACHED*/ 390 return (0); /* for lint's little mind */ 391 } 392 393 /* spew a message, then a usage message, then exit */ 394 static void 395 usage(const char *msg) 396 { 397 if (msg) 398 err(0, "%s\nUse \"logadm -h\" for help.", msg); 399 else 400 err(EF_RAW, "Use \"logadm -h\" for help.\n"); 401 } 402 403 /* helper function used by doaftercmd() to join mail addrs with commas */ 404 /*ARGSUSED1*/ 405 static void 406 commajoin(const char *lhs, void *rhs, void *arg) 407 { 408 struct fn *fnp = (struct fn *)arg; 409 410 if (*fn_s(fnp)) 411 fn_putc(fnp, ','); 412 fn_puts(fnp, lhs); 413 } 414 415 /* helper function used by main() to run "after" commands */ 416 static void 417 doaftercmd(const char *lhs, void *rhs, void *arg) 418 { 419 struct opts *opts = (struct opts *)arg; 420 struct lut *addrs = (struct lut *)rhs; 421 422 if (addrs) { 423 struct fn *fnp = fn_new(NULL); 424 425 /* 426 * addrs contains list of email addrs that should get 427 * the error output when this after command is executed. 428 */ 429 lut_walk(addrs, commajoin, fnp); 430 err_mailto(fn_s(fnp)); 431 } 432 433 docmd(opts, "-a cmd", Sh, "-c", lhs, NULL); 434 } 435 436 /* main logname processing */ 437 static void 438 dologname(struct fn *fnp, struct opts *clopts) 439 { 440 const char *logname = fn_s(fnp); 441 struct opts *cfopts; 442 struct opts *allopts; 443 struct fn_list *logfiles; 444 struct fn_list *globbedfiles; 445 struct fn *nextfnp; 446 447 /* look up options set by config file */ 448 cfopts = conf_opts(logname); 449 450 if (opts_count(clopts, "v")) 451 (void) out("# processing logname: %s\n", logname); 452 453 if (Debug) { 454 (void) fprintf(stderr, "dologname: logname <%s>\n", logname); 455 (void) fprintf(stderr, "conffile opts:"); 456 opts_print(cfopts, stderr, NULL); 457 (void) fprintf(stderr, "\n"); 458 } 459 460 /* handle conffile lookup option */ 461 if (opts_count(clopts, "V")) { 462 /* lookup an entry in conffile */ 463 if (Debug) 464 (void) fprintf(stderr, 465 "dologname: lookup conffile entry\n"); 466 if (conf_lookup(logname)) { 467 opts_printword(logname, stdout); 468 opts_print(cfopts, stdout, NULL); 469 (void) out("\n"); 470 } else 471 err_exitcode(1); 472 return; 473 } 474 475 /* handle conffile removal option */ 476 if (opts_count(clopts, "r")) { 477 if (Debug) 478 (void) fprintf(stderr, 479 "dologname: remove conffile entry\n"); 480 if (conf_lookup(logname)) 481 conf_replace(logname, NULL); 482 else 483 err_exitcode(1); 484 return; 485 } 486 487 /* generate combined options */ 488 allopts = opts_merge(cfopts, clopts); 489 490 /* arrange for error output to be mailed if allopts includes -e */ 491 if (opts_count(allopts, "e")) 492 err_mailto(opts_optarg(allopts, "e")); 493 else 494 err_mailto(NULL); 495 496 /* this implements the default rotation rules */ 497 if (opts_count(allopts, "sp") == 0) { 498 if (opts_count(clopts, "v")) 499 (void) out( 500 "# using default rotate rules: -s1b -p1w\n"); 501 (void) opts_set(allopts, "s", "1b"); 502 (void) opts_set(allopts, "p", "1w"); 503 } 504 505 /* this implements the default expiration rules */ 506 if (opts_count(allopts, "ACS") == 0) { 507 if (opts_count(clopts, "v")) 508 (void) out("# using default expire rule: -C10\n"); 509 (void) opts_set(allopts, "C", "10"); 510 } 511 512 /* this implements the default template */ 513 if (opts_count(allopts, "t") == 0) { 514 if (opts_count(clopts, "v")) 515 (void) out("# using default template: $file.$n\n"); 516 (void) opts_set(allopts, "t", "$file.$n"); 517 } 518 519 if (Debug) { 520 (void) fprintf(stderr, "merged opts:"); 521 opts_print(allopts, stderr, NULL); 522 (void) fprintf(stderr, "\n"); 523 } 524 525 /* 526 * if the conffile entry supplied log file names, then 527 * logname is NOT one of the log file names (it was just 528 * the entry name in conffile). 529 */ 530 logfiles = opts_cmdargs(cfopts); 531 if (Debug) { 532 (void) fprintf(stderr, "dologname: logfiles from cfopts:\n"); 533 fn_list_rewind(logfiles); 534 while ((nextfnp = fn_list_next(logfiles)) != NULL) 535 (void) fprintf(stderr, " <%s>\n", fn_s(nextfnp)); 536 } 537 if (fn_list_empty(logfiles)) 538 globbedfiles = glob_glob(fnp); 539 else 540 globbedfiles = glob_glob_list(logfiles); 541 542 /* go through the list produced by glob expansion */ 543 fn_list_rewind(globbedfiles); 544 while ((nextfnp = fn_list_next(globbedfiles)) != NULL) 545 if (rotatelog(nextfnp, allopts)) 546 expirefiles(nextfnp, allopts); 547 548 fn_list_free(globbedfiles); 549 opts_free(allopts); 550 } 551 552 553 /* absurdly long buffer lengths for holding user/group/mode strings */ 554 #define TIMESTRMAX 100 555 #define MAXATTR 100 556 557 /* rotate a log file if necessary, returns true if ok to go on to expire step */ 558 static boolean_t 559 rotatelog(struct fn *fnp, struct opts *opts) 560 { 561 char *fname = fn_s(fnp); 562 struct stat stbuf; 563 char nowstr[TIMESTRMAX]; 564 struct fn *recentlog = fn_new(NULL); /* for -R cmd */ 565 char ownerbuf[MAXATTR]; 566 char groupbuf[MAXATTR]; 567 char modebuf[MAXATTR]; 568 const char *owner; 569 const char *group; 570 const char *mode; 571 572 if (Debug) 573 (void) fprintf(stderr, "rotatelog: fname <%s>\n", fname); 574 575 if (opts_count(opts, "p") && opts_optarg_int(opts, "p") == OPTP_NEVER) 576 return (B_TRUE); /* "-p never" forced no rotate */ 577 578 /* prepare the keywords */ 579 kw_init(fnp, NULL); 580 if (Debug > 1) { 581 (void) fprintf(stderr, "rotatelog keywords:\n"); 582 kw_print(stderr); 583 } 584 585 if (lstat(fname, &stbuf) < 0) { 586 if (opts_count(opts, "N")) 587 return (1); 588 err(EF_WARN|EF_SYS, "%s", fname); 589 return (B_FALSE); 590 } 591 592 if ((stbuf.st_mode & S_IFMT) == S_IFLNK) { 593 err(EF_WARN, "%s is a symlink", fname); 594 return (B_FALSE); 595 } 596 597 if ((stbuf.st_mode & S_IFMT) != S_IFREG) { 598 err(EF_WARN, "%s is not a regular file", fname); 599 return (B_FALSE); 600 } 601 602 /* see if size condition is present, and return if not met */ 603 if (opts_count(opts, "s") && stbuf.st_size < opts_optarg_int(opts, "s")) 604 return (B_TRUE); 605 606 /* see if age condition is present, and return if not met */ 607 if (opts_count(opts, "p")) { 608 int when = opts_optarg_int(opts, "p"); 609 struct opts *cfopts; 610 611 /* unless rotate forced by "-p now", see if period has passed */ 612 if (when != OPTP_NOW) { 613 /* 614 * "when" holds the number of seconds that must have 615 * passed since the last time this log was rotated. 616 * of course, running logadm can take a little time 617 * (typically a second or two, but longer if the 618 * conffile has lots of stuff in it) and that amount 619 * of time is variable, depending on system load, etc. 620 * so we want to allow a little "slop" in the value of 621 * "when". this way, if a log should be rotated every 622 * week, and the number of seconds passed is really a 623 * few seconds short of a week, we'll go ahead and 624 * rotate the log as expected. 625 * 626 */ 627 if (when >= 60 * 60) 628 when -= 59; 629 630 /* 631 * last rotation is recorded as argument to -P, 632 * but if logname isn't the same as log file name 633 * then the timestamp would be recorded on a 634 * separate line in the conf file. so if we 635 * haven't seen a -P already, we check to see if 636 * it is part of a specific entry for the log 637 * file name. this handles the case where the 638 * logname is "apache", it supplies a log file 639 * name like "/var/apache/logs/[a-z]*_log", 640 * which expands to multiple file names. if one 641 * of the file names is "/var/apache/logs/access_log" 642 * the the -P will be attached to a line with that 643 * logname in the conf file. 644 */ 645 if (opts_count(opts, "P")) { 646 int last = opts_optarg_int(opts, "P"); 647 648 /* return if not enough time has passed */ 649 if (Now - last < when) 650 return (B_TRUE); 651 } else if ((cfopts = conf_opts(fname)) != NULL && 652 opts_count(cfopts, "P")) { 653 int last = opts_optarg_int(cfopts, "P"); 654 655 /* 656 * just checking this means this entry 657 * is now "done" if we're going through 658 * the entire conffile 659 */ 660 Donenames = lut_add(Donenames, fname, "1"); 661 662 /* return if not enough time has passed */ 663 if (Now - last < when) 664 return (B_TRUE); 665 } 666 } 667 } 668 669 if (Debug) 670 (void) fprintf(stderr, "rotatelog: conditions met\n"); 671 672 /* rename the log file */ 673 rotateto(fnp, opts, 0, recentlog, B_FALSE); 674 675 /* determine owner, group, mode for empty log file */ 676 if (opts_count(opts, "o")) 677 (void) strlcpy(ownerbuf, opts_optarg(opts, "o"), MAXATTR); 678 else { 679 (void) snprintf(ownerbuf, MAXATTR, "%ld", stbuf.st_uid); 680 } 681 owner = ownerbuf; 682 if (opts_count(opts, "g")) 683 group = opts_optarg(opts, "g"); 684 else { 685 (void) snprintf(groupbuf, MAXATTR, "%ld", stbuf.st_gid); 686 group = groupbuf; 687 } 688 (void) strlcat(ownerbuf, ":", MAXATTR - strlen(ownerbuf)); 689 (void) strlcat(ownerbuf, group, MAXATTR - strlen(ownerbuf)); 690 if (opts_count(opts, "m")) 691 mode = opts_optarg(opts, "m"); 692 else { 693 (void) snprintf(modebuf, MAXATTR, 694 "%03lo", stbuf.st_mode & 0777); 695 mode = modebuf; 696 } 697 698 /* create the empty log file */ 699 docmd(opts, NULL, Touch, fname, NULL, NULL); 700 docmd(opts, NULL, Chown, owner, fname, NULL); 701 docmd(opts, NULL, Chmod, mode, fname, NULL); 702 703 /* execute post-rotation command */ 704 if (opts_count(opts, "R")) { 705 struct fn *rawcmd = fn_new(opts_optarg(opts, "R")); 706 struct fn *cmd = fn_new(NULL); 707 708 kw_init(recentlog, NULL); 709 (void) kw_expand(rawcmd, cmd, 0, B_FALSE); 710 docmd(opts, "-R cmd", Sh, "-c", fn_s(cmd), NULL); 711 fn_free(rawcmd); 712 fn_free(cmd); 713 } 714 fn_free(recentlog); 715 716 /* 717 * add "after" command to list of after commands. we also record 718 * the email address, if any, where the error output of the after 719 * command should be sent. if the after command is already on 720 * our list, add the email addr to the list the email addrs for 721 * that command (the after command will only be executed once, 722 * so the error output gets mailed to every address we've come 723 * across associated with this command). 724 */ 725 if (opts_count(opts, "a")) { 726 const char *cmd = opts_optarg(opts, "a"); 727 struct lut *addrs = (struct lut *)lut_lookup(Aftercmds, cmd); 728 if (opts_count(opts, "e")) 729 addrs = lut_add(addrs, opts_optarg(opts, "e"), NULL); 730 Aftercmds = lut_add(Aftercmds, opts_optarg(opts, "a"), addrs); 731 } 732 733 /* record the rotation date */ 734 (void) strftime(nowstr, sizeof (nowstr), 735 "%a %b %e %T %Y", gmtime(&Now)); 736 if (opts_count(opts, "v")) 737 (void) out("# recording rotation date %s for %s\n", 738 nowstr, fname); 739 conf_set(fname, "P", STRDUP(nowstr)); 740 Donenames = lut_add(Donenames, fname, "1"); 741 return (B_TRUE); 742 } 743 744 /* rotate files "up" according to current template */ 745 static void 746 rotateto(struct fn *fnp, struct opts *opts, int n, struct fn *recentlog, 747 boolean_t isgz) 748 { 749 struct fn *template = fn_new(opts_optarg(opts, "t")); 750 struct fn *newfile = fn_new(NULL); 751 struct fn *dirname; 752 int hasn; 753 struct stat stbuf; 754 755 /* expand template to figure out new filename */ 756 hasn = kw_expand(template, newfile, n, isgz); 757 758 if (Debug) 759 (void) fprintf(stderr, "rotateto: %s -> %s (%d)\n", fn_s(fnp), 760 fn_s(newfile), n); 761 762 /* if filename is there already, rotate "up" */ 763 if (hasn && lstat(fn_s(newfile), &stbuf) != -1) 764 rotateto(newfile, opts, n + 1, recentlog, isgz); 765 else if (hasn && opts_count(opts, "z")) { 766 struct fn *gzfnp = fn_dup(newfile); 767 /* 768 * since we're compressing old files, see if we 769 * about to rotate into one. 770 */ 771 fn_puts(gzfnp, ".gz"); 772 if (lstat(fn_s(gzfnp), &stbuf) != -1) 773 rotateto(gzfnp, opts, n + 1, recentlog, B_TRUE); 774 fn_free(gzfnp); 775 } 776 777 /* first time through run "before" cmd if not run already */ 778 if (n == 0 && opts_count(opts, "b")) { 779 const char *cmd = opts_optarg(opts, "b"); 780 781 if (lut_lookup(Beforecmds, cmd) == NULL) { 782 docmd(opts, "-b cmd", Sh, "-c", cmd, NULL); 783 Beforecmds = lut_add(Beforecmds, cmd, "1"); 784 } 785 } 786 787 /* ensure destination directory exists */ 788 dirname = fn_dirname(newfile); 789 docmd(opts, "verify directory exists", Mkdir, "-p", 790 fn_s(dirname), NULL); 791 fn_free(dirname); 792 793 /* do the rename */ 794 if (opts_count(opts, "c")) { 795 docmd(opts, "rotate log file via copy (-c flag)", 796 Cp, "-fp", fn_s(fnp), fn_s(newfile)); 797 docmd(opts, "truncate log file (-c flag)", 798 Cp, "-f", "/dev/null", fn_s(fnp)); 799 } else if (n == 0 && opts_count(opts, "M")) { 800 struct fn *rawcmd = fn_new(opts_optarg(opts, "M")); 801 struct fn *cmd = fn_new(NULL); 802 803 /* use specified command to mv the log file */ 804 kw_init(fnp, newfile); 805 (void) kw_expand(rawcmd, cmd, 0, B_FALSE); 806 docmd(opts, "-M cmd", Sh, "-c", fn_s(cmd), NULL); 807 fn_free(rawcmd); 808 fn_free(cmd); 809 } else 810 /* common case: we call "mv" to handle the actual rename */ 811 docmd(opts, "rotate log file", Mv, "-f", 812 fn_s(fnp), fn_s(newfile)); 813 814 /* gzip the old log file according to -z count */ 815 if (!isgz && opts_count(opts, "z")) { 816 int count = opts_optarg_int(opts, "z"); 817 818 if (Debug) 819 (void) fprintf(stderr, "rotateto z count %d\n", count); 820 821 if (count <= n) { 822 docmd(opts, "compress old log (-z flag)", Gzip, 823 "-f", fn_s(newfile), NULL); 824 fn_puts(newfile, ".gz"); 825 } 826 } 827 828 /* first time through, gather interesting info for caller */ 829 if (n == 0) 830 fn_renew(recentlog, fn_s(newfile)); 831 } 832 833 /* expire phase of logname processing */ 834 static void 835 expirefiles(struct fn *fnp, struct opts *opts) 836 { 837 char *fname = fn_s(fnp); 838 struct fn *template; 839 struct fn *pattern; 840 struct fn_list *files; 841 struct fn *nextfnp; 842 int count; 843 size_t size; 844 845 if (Debug) 846 (void) fprintf(stderr, "expirefiles: fname <%s>\n", fname); 847 848 /* return if no potential expire conditions */ 849 if (opts_count(opts, "AS") == 0 && opts_optarg_int(opts, "C") == 0) 850 return; 851 852 kw_init(fnp, NULL); 853 if (Debug > 1) { 854 (void) fprintf(stderr, "expirefiles keywords:\n"); 855 kw_print(stderr); 856 } 857 858 /* see if pattern was supplied by user */ 859 if (opts_count(opts, "T")) { 860 template = fn_new(opts_optarg(opts, "T")); 861 pattern = glob_to_reglob(template); 862 } else { 863 /* nope, generate pattern based on rotation template */ 864 template = fn_new(opts_optarg(opts, "t")); 865 pattern = fn_new(NULL); 866 (void) kw_expand(template, pattern, -1, 867 opts_count(opts, "z") != 0); 868 } 869 870 /* match all old log files (hopefully not any others as well!) */ 871 files = glob_reglob(pattern); 872 873 if (Debug) { 874 (void) fprintf(stderr, "expirefiles: pattern <%s>\n", 875 fn_s(pattern)); 876 fn_list_rewind(files); 877 while ((nextfnp = fn_list_next(files)) != NULL) 878 (void) fprintf(stderr, " <%s>\n", fn_s(nextfnp)); 879 } 880 881 /* see if count causes expiration */ 882 if ((count = opts_optarg_int(opts, "C")) > 0) { 883 int needexpire = fn_list_count(files) - count; 884 885 if (Debug) 886 (void) fprintf(stderr, "expirefiles: needexpire %d\n", 887 needexpire); 888 889 while (needexpire > 0 && 890 ((nextfnp = fn_list_popoldest(files)) != NULL)) { 891 dorm(opts, "expire by count rule", nextfnp); 892 fn_free(nextfnp); 893 needexpire--; 894 } 895 } 896 897 /* see if total size causes expiration */ 898 if (opts_count(opts, "S") && (size = opts_optarg_int(opts, "S")) > 0) { 899 while (fn_list_totalsize(files) > size && 900 ((nextfnp = fn_list_popoldest(files)) != NULL)) { 901 dorm(opts, "expire by size rule", nextfnp); 902 fn_free(nextfnp); 903 } 904 } 905 906 /* see if age causes expiration */ 907 if (opts_count(opts, "A")) { 908 int mtime = (int)time(0) - opts_optarg_int(opts, "A"); 909 910 while ((nextfnp = fn_list_popoldest(files)) != NULL) 911 if (fn_getstat(nextfnp)->st_mtime < mtime) { 912 dorm(opts, "expire by age rule", nextfnp); 913 fn_free(nextfnp); 914 } else { 915 fn_free(nextfnp); 916 break; 917 } 918 } 919 920 fn_free(template); 921 fn_list_free(files); 922 } 923 924 /* execute a command to remove an expired log file */ 925 static void 926 dorm(struct opts *opts, const char *msg, struct fn *fnp) 927 { 928 if (opts_count(opts, "E")) { 929 struct fn *rawcmd = fn_new(opts_optarg(opts, "E")); 930 struct fn *cmd = fn_new(NULL); 931 932 /* user supplied cmd, expand $file */ 933 kw_init(fnp, NULL); 934 (void) kw_expand(rawcmd, cmd, 0, B_FALSE); 935 docmd(opts, msg, Sh, "-c", fn_s(cmd), NULL); 936 fn_free(rawcmd); 937 fn_free(cmd); 938 } else 939 docmd(opts, msg, Rm, "-f", fn_s(fnp), NULL); 940 } 941 942 /* execute a command, producing -n and -v output as necessary */ 943 static void 944 docmd(struct opts *opts, const char *msg, const char *cmd, 945 const char *arg1, const char *arg2, const char *arg3) 946 { 947 int pid; 948 int errpipe[2]; 949 950 /* print info about command if necessary */ 951 if (opts_count(opts, "vn")) { 952 const char *simplecmd; 953 954 if ((simplecmd = strrchr(cmd, '/')) == NULL) 955 simplecmd = cmd; 956 else 957 simplecmd++; 958 (void) out("%s", simplecmd); 959 if (arg1) 960 (void) out(" %s", arg1); 961 if (arg2) 962 (void) out(" %s", arg2); 963 if (arg3) 964 (void) out(" %s", arg3); 965 if (msg) 966 (void) out(" # %s", msg); 967 (void) out("\n"); 968 } 969 970 if (opts_count(opts, "n")) 971 return; /* -n means don't really do it */ 972 973 /* 974 * run the cmd and see if it failed. this function is *not* a 975 * generic command runner -- we depend on some knowledge we 976 * have about the commands we run. first of all, we expect 977 * errors to spew something to stderr, and that something is 978 * typically short enough to fit into a pipe so we can wait() 979 * for the command to complete and then fetch the error text 980 * from the pipe. we also expect the exit codes to make sense. 981 * notice also that we only allow a command name which is an 982 * absolute pathname, and two args must be supplied (the 983 * second may be NULL, or they may both be NULL). 984 */ 985 if (pipe(errpipe) < 0) 986 err(EF_SYS, "pipe"); 987 988 if ((pid = fork()) < 0) 989 err(EF_SYS, "fork"); 990 else if (pid) { 991 int wstat; 992 int count; 993 994 /* parent */ 995 (void) close(errpipe[1]); 996 if (waitpid(pid, &wstat, 0) < 0) 997 err(EF_SYS, "waitpid"); 998 999 /* check for stderr output */ 1000 if (ioctl(errpipe[0], FIONREAD, &count) >= 0 && count) { 1001 err(EF_WARN, "command failed: %s%s%s%s%s%s%s", 1002 cmd, 1003 (arg1) ? " " : "", 1004 (arg1) ? arg1 : "", 1005 (arg2) ? " " : "", 1006 (arg2) ? arg2 : "", 1007 (arg3) ? " " : "", 1008 (arg3) ? arg3 : ""); 1009 err_fromfd(errpipe[0]); 1010 } else if (WIFSIGNALED(wstat)) 1011 err(EF_WARN, 1012 "command died, signal %d: %s%s%s%s%s%s%s", 1013 WTERMSIG(wstat), 1014 cmd, 1015 (arg1) ? " " : "", 1016 (arg1) ? arg1 : "", 1017 (arg2) ? " " : "", 1018 (arg2) ? arg2 : "", 1019 (arg3) ? " " : "", 1020 (arg3) ? arg3 : ""); 1021 else if (WIFEXITED(wstat) && WEXITSTATUS(wstat)) 1022 err(EF_WARN, 1023 "command error, exit %d: %s%s%s%s%s%s%s", 1024 WEXITSTATUS(wstat), 1025 cmd, 1026 (arg1) ? " " : "", 1027 (arg1) ? arg1 : "", 1028 (arg2) ? " " : "", 1029 (arg2) ? arg2 : "", 1030 (arg3) ? " " : "", 1031 (arg3) ? arg3 : ""); 1032 1033 (void) close(errpipe[0]); 1034 } else { 1035 /* child */ 1036 (void) dup2(errpipe[1], fileno(stderr)); 1037 (void) close(errpipe[0]); 1038 (void) execl(cmd, cmd, arg1, arg2, arg3, 0); 1039 perror(cmd); 1040 _exit(1); 1041 } 1042 } 1043