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