/*
* CDDL HEADER START
*
* The contents of this file are subject to the terms of the
* Common Development and Distribution License (the "License").
* You may not use this file except in compliance with the License.
*
* You can obtain a copy of the license at usr/src/OPENSOLARIS.LICENSE
* See the License for the specific language governing permissions
* and limitations under the License.
*
* When distributing Covered Code, include this CDDL HEADER in each
* file and include the License file at usr/src/OPENSOLARIS.LICENSE.
* If applicable, add the following below this CDDL HEADER, with the
* fields enclosed by brackets "[]" replaced with your own identifying
* information: Portions Copyright [yyyy] [name of copyright owner]
*
* CDDL HEADER END
*/
/*
* Copyright (c) 2013, Joyent, Inc. All rights reserved.
*
*
* this program is 90% argument processing, 10% actions...
*/
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <strings.h>
#include <libintl.h>
#include <locale.h>
#include <fcntl.h>
#include <sys/sysmacros.h>
#include <time.h>
#include <utime.h>
#include "err.h"
#include "lut.h"
#include "fn.h"
#include "opts.h"
#include "conf.h"
#include "glob.h"
#include "kw.h"
/* forward declarations for functions in this file */
const char *file_copy);
/* our configuration file, unless otherwise specified by -f */
/* our timestamps file, unless otherwise specified by -F */
/* default pathnames to the commands we invoke */
/* return from time(0), gathered early on to avoid slewed timestamps */
/* list of before commands that have been executed */
/* list of after commands to execute before exiting */
/* list of conffile entry names that are considered "done" */
/* A list of names of files to be gzipped */
/*
* only the "FfhnVv" options are allowed in the first form of this command,
* so this defines the list of options that are an error in they appear
* in the first form. In other words, it is not allowed to run logadm
* with any of these options unless at least one logname is also provided.
*/
/* text that we spew with the -h flag */
#define HELP1 \
"Usage: logadm [options]\n"\
" (processes all entries in /etc/logadm.conf or conffile given by -f)\n"\
" or: logadm [options] logname...\n"\
" (processes the given lognames)\n"\
"\n"\
"General options:\n"\
" -e mailaddr mail errors to given address\n"\
" -F timestamps use timestamps instead of /var/logadm/timestamps\n"\
" -f conffile use conffile instead of /etc/logadm.conf\n"\
" -h display help\n"\
" -N not an error if log file nonexistent\n"\
" -n show actions, don't perform them\n"\
" -r remove logname entry from conffile\n"\
" -V ensure conffile entries exist, correct\n"\
" -v print info about actions happening\n"\
" -w entryname write entry to config file\n"\
"\n"\
"Options which control when a logfile is rotated:\n"\
"(default is: -s1b -p1w if no -s or -p)\n"\
" -p period only rotate if period passed since last rotate\n"\
" -P timestamp used to store rotation date in conffile\n"\
" -s size only rotate if given size or greater\n"\
"\n"
#define HELP2 \
"Options which control how a logfile is rotated:\n"\
" -a cmd execute cmd after taking actions\n"\
" -b cmd execute cmd before taking actions\n"\
" -c copy & truncate logfile, don't rename\n"\
" -g group new empty log file group\n"\
" -l rotate log file with local time rather than UTC\n"\
" -m mode new empty log file mode\n"\
" -M cmd execute cmd to rotate the log file\n"\
" -o owner new empty log file owner\n"\
" -R cmd run cmd on file after rotate\n"\
" -t template template for naming old logs\n"\
" -z count gzip old logs except most recent count\n"\
"\n"\
"Options which control the expiration of old logfiles:\n"\
"(default is: -C10 if no -A, -C, or -S)\n"\
" -A age expire logs older than age\n"\
" -C count expire old logs until count remain\n"\
" -E cmd run cmd on file to expire\n"\
" -S size expire until space used is below size \n"\
" -T pattern pattern for finding old logs\n"
/*
* main -- where it all begins
*/
/*ARGSUSED*/
int
{
char *val;
char *buf;
int status;
#if !defined(TEXT_DOMAIN)
#endif
(void) textdomain(TEXT_DOMAIN);
/* we only print times into the timestamps file, so make them uniform */
/* give our name to error routines & skip it for arg parsing */
(void) setlinebuf(stdout);
if (putenv("PATH=/bin"))
if (putenv("TZ=UTC"))
tzset();
(void) umask(0);
/* check for (undocumented) debugging environment variables */
/* parse command line arguments */
if (SETJMP)
usage("bailing out due to command line errors");
else
if (Debug) {
}
/*
* There are many moods of logadm:
*
* 1. "-h" for help was given. We spew a canned help
* message and exit, regardless of any other options given.
*
* 2. "-r" or "-w" asking us to write to the conffile. Lots
* of argument checking, then we make the change to conffile
* and exit. (-r processing actually happens in dologname().)
*
* the appropriate run through the conffile and exit.
* (-V processing actually happens in dologname().)
*
* 4. No lognames were given, so we're being asked to go through
* every entry in conffile. We verify that only the options
* that make sense for this form of the command are present
* and fall into the main processing loop below.
*
* 5. lognames were given, so we fall into the main processing
* loop below to work our way through them.
*
* The last two cases are where the option processing gets more
* complex. Each time around the main processing loop, we're
* in one of these cases:
*
* A. No cmdargs were found (we're in case 4), the entry
* in conffile supplies no log file names, so the entry
* name itself is the logfile name (or names, if it globs
* to multiple file names).
*
* B. No cmdargs were found (we're in case 4), the entry
* in conffile gives log file names that we then loop
* name is specifically NOT one of the log file names.
*
* C. We're going through the cmdargs (we're in case 5),
* the entry in conffile either doesn't exist or it exists
* but supplies no log file names, so the cmdarg itself
* is the log file name.
*
* D. We're going through the cmdargs (we're in case 5),
* a matching entry in conffile supplies log file names
* case the entry name is specifically NOT one of the log
* file names.
*
* As we're doing all this, any options given on the command line
* override any found in the conffile, and we apply the defaults
* for rotation conditions and expiration conditions, etc. at the
* last opportunity, when we're sure they haven't been overridden
* by an option somewhere along the way.
*
*/
/* help option overrides anything else */
err_done(0);
/*NOTREACHED*/
}
/* detect illegal option combinations */
usage("Only one of -r, -w, or -V may be used at a time.");
usage("Only one of -c or -M may be used at a time.");
/* arrange for error output to be mailed if clopts includes -e */
/* this implements the default conffile and timestamps */
err_done(0);
/* handle conffile write option */
if (Debug)
err_done(0);
/*NOTREACHED*/
}
/*
* lognames is either a list supplied on the command line,
* or every entry in the conffile if none were supplied.
*/
if (fn_list_empty(lognames)) {
/*
* being asked to do all entries in conffile
*
* check to see if any options were given that only
* make sense when lognames are given specifically
* on the command line.
*/
usage("some options require logname argument");
if (Debug)
"main: run all entries in conffile\n");
lognames = conf_entries();
}
/* foreach logname... */
if (Debug)
"main: logname already done: <%s>\n",
buf);
continue;
}
"due to errors", buf);
else
}
/* execute any after commands */
/* execute any gzip commands */
/* write out any conffile changes */
err_done(0);
/*NOTREACHED*/
return (0); /* for lint's little mind */
}
/* spew a message, then a usage message, then exit */
static void
{
if (msg)
else
}
/* helper function used by doaftercmd() to join mail addrs with commas */
/*ARGSUSED1*/
static void
{
char *buf;
}
/* helper function used by main() to run "after" commands */
static void
{
if (addrs) {
/*
* addrs contains list of email addrs that should get
* the error output when this after command is executed.
*/
}
}
/* perform delayed gzip */
static void
{
if (Debug) {
"expired file <%s>\n", lhs);
}
return;
}
}
/* main logname processing */
static void
{
/* look up options set by config file */
if (Debug) {
logname);
}
/* handle conffile lookup option */
/* lookup an entry in conffile */
if (Debug)
"dologname: lookup conffile entry\n");
if (conf_lookup(logname)) {
(void) out("\n");
} else
err_exitcode(1);
return;
}
/* handle conffile removal option */
if (Debug)
"dologname: remove conffile entry\n");
if (conf_lookup(logname))
else
err_exitcode(1);
return;
}
/* generate combined options */
/* arrange for error output to be mailed if allopts includes -e */
else
/* this implements the default rotation rules */
(void) out(
"# using default rotate rules: -s1b -p1w\n");
}
/* this implements the default expiration rules */
(void) out("# using default expire rule: -C10\n");
}
/* this implements the default template */
(void) out("# using default template: $file.$n\n");
}
if (Debug) {
}
/*
* if the conffile entry supplied log file names, then
* logname is NOT one of the log file names (it was just
* the entry name in conffile).
*/
if (Debug) {
char *buf;
}
if (fn_list_empty(logfiles))
else
/* go through the list produced by glob expansion */
}
/* rotate a log file if necessary, returns true if ok to go on to expire step */
static boolean_t
{
const char *owner;
const char *group;
const char *mode;
return (B_TRUE); /* "-p never" forced no rotate */
/* prepare the keywords */
if (Debug > 1) {
}
return (1);
return (B_FALSE);
}
return (B_FALSE);
}
return (B_FALSE);
}
/* even if size condition is not met, this entry is "done" */
return (B_TRUE);
}
/* see if age condition is present, and return if not met */
/* unless rotate forced by "-p now", see if period has passed */
/*
* "when" holds the number of seconds that must have
* passed since the last time this log was rotated.
* of course, running logadm can take a little time
* (typically a second or two, but longer if the
* conffile has lots of stuff in it) and that amount
* of time is variable, depending on system load, etc.
* so we want to allow a little "slop" in the value of
* "when". this way, if a log should be rotated every
* week, and the number of seconds passed is really a
* few seconds short of a week, we'll go ahead and
* rotate the log as expected.
*
*/
when -= 59;
/*
* last rotation is recorded as argument to -P,
* but if logname isn't the same as log file name
* then the timestamp would be recorded on a
* separate line in the timestamp file. so if we
* haven't seen a -P already, we check to see if
* it is part of a specific entry for the log
* file name. this handles the case where the
* logname is "apache", it supplies a log file
* which expands to multiple file names. if one
* of the file names is "/var/apache/logs/access_log"
* the the -P will be attached to a line with that
* logname in the timestamp file.
*/
/* return if not enough time has passed */
return (B_TRUE);
/*
* just checking this means this entry
* is now "done" if we're going through
* the entire conffile
*/
/* return if not enough time has passed */
return (B_TRUE);
}
}
}
if (Debug)
/* Change the time zone to local time zone */
if (putenv("TZ="))
tzset();
/* rename the log file */
/* Change the time zone to UTC */
if (putenv("TZ=UTC"))
tzset();
} else {
/* rename the log file */
}
/* determine owner, group, mode for empty log file */
else {
}
else {
}
else {
}
/* create the empty log file */
/* execute post-rotation command */
}
/*
* add "after" command to list of after commands. we also record
* the email address, if any, where the error output of the after
* command should be sent. if the after command is already on
* our list, add the email addr to the list the email addrs for
* that command (the after command will only be executed once,
* so the error output gets mailed to every address we've come
* across associated with this command).
*/
}
/* record the rotation date */
(void) out("# recording rotation date %s for %s\n",
return (B_TRUE);
}
/* rotate files "up" according to current template */
static void
{
int hasn;
char *buf1;
char *buf2;
/* expand template to figure out new filename */
if (Debug)
}
/* if filename is there already, rotate "up" */
/*
* since we're compressing old files, see if we
* about to rotate into one.
*/
}
/* first time through run "before" cmd if not run already */
}
}
/* ensure destination directory exists */
/* do the rename */
/* use specified command to mv the log file */
} else
/* common case: we call "mv" to handle the actual rename */
/* first time through, gather interesting info for caller */
if (n == 0)
}
/* expire phase of logname processing */
static void
{
/* return if no potential expire conditions */
return;
if (Debug > 1) {
}
/* see if pattern was supplied by user */
} else {
/* nope, generate pattern based on rotation template */
}
/* match all old log files (hopefully not any others as well!) */
if (Debug) {
char *buf;
buf);
}
}
/* see if count causes expiration */
if (Debug)
while (needexpire > 0 &&
needexpire--;
}
}
/* see if total size causes expiration */
}
}
/* see if age causes expiration */
} else {
break;
}
}
}
/* record old log files to be gzip'ed according to -z count */
/*
* Don't gzip the old log file yet -
* it takes too long. Just remember that we
* need to gzip.
*/
if (Debug) {
"will compress %s count %d\n",
}
}
fcount--;
}
}
}
/* execute a command to remove an expired log file */
static void
{
/* user supplied cmd, expand $file */
} else
}
/* execute a command, producing -n and -v output as necessary */
static void
{
int pid;
/* print info about command if necessary */
const char *simplecmd;
else
simplecmd++;
if (arg1)
if (arg2)
if (arg3)
if (msg)
(void) out("\n");
}
return; /* -n means don't really do it */
/*
* run the cmd and see if it failed. this function is *not* a
* generic command runner -- we depend on some knowledge we
* have about the commands we run. first of all, we expect
* errors to spew something to stderr, and that something is
* typically short enough to fit into a pipe so we can wait()
* for the command to complete and then fetch the error text
* from the pipe. we also expect the exit codes to make sense.
* notice also that we only allow a command name which is an
* absolute pathname, and two args must be supplied (the
* second may be NULL, or they may both be NULL).
*/
else if (pid) {
int wstat;
int count;
/* parent */
/* check for stderr output */
cmd,
err_fromfd(errpipe[0]);
} else if (WIFSIGNALED(wstat))
"command died, signal %d: %s%s%s%s%s%s%s",
cmd,
"command error, exit %d: %s%s%s%s%s%s%s",
cmd,
} else {
/* child */
_exit(1);
}
}
/* do internal atomic file copy and truncation */
static void
{
struct stat s;
/* print info if necessary */
(void) out("# log rotation via atomic copy and truncation"
" (-c flag):\n");
}
return; /* -n means don't really do it */
/* open log file to be rotated and remember its chmod mask */
return;
}
return;
}
/* create new file for copy destination with correct attributes */
return;
}
/*
* Now we'll loop, reading the log file and writing it to our copy
* until the bytes remaining are beneath our atomicity threshold -- at
* which point we'll lock the file and copy the remainder atomically.
* The body of this loop is non-atomic with respect to writers, the
* rationale being that total atomicity (that is, locking the file for
* the entire duration of the copy) comes at too great a cost for a
* large log file, as the writer (i.e., the daemon whose log is being
* rolled) can be blocked for an unacceptable duration. (For one
* particularly loquacious daemon, this period was observed to be
* several minutes in length -- a time so long that it induced
* additional failures in dependent components.) Note that this means
* that if the log file is not always appended to -- if it is opened
* without O_APPEND or otherwise truncated outside of logadm -- this
* will result in our log snapshot being incorrect. But of course, in
* either of these cases, the use of logadm at all is itself
* suspect...
*/
do {
return;
}
if (rem >= 0)
break;
/*
* If the file became smaller, something fishy is going
* on; we'll truncate our copy, reset our seek offset
* and break into the atomic copy.
*/
break;
}
/*
* We're falling behind -- this file is getting bigger
* faster than we're able to write it; break out and
* lock the file to block the writer.
*/
break;
}
while (rem > 0) {
break;
continue;
}
return;
}
} while (len >= 0);
/* lock log file so that nobody can write into it before we are done */
/* do atomic copy and truncation */
return;
}
/* unlock log file */
/* keep times from original file */
}