blob: 8416d5a887b6c4c6c7d7035a6e2d55b0e20acb2f [file] [log] [blame]
/*
* cancd - CA NetConsole Daemon
*
* Simply collect netconsole messages.
*
* Author: Joel Becker <joel.becker@oracle.com>
* Copyright (C) 2005 Oracle. All rights reserved.
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public
* License as published by the Free Software Foundation; either
* version 2 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* General Public License for more details.
*
* You should have received a copy of the GNU General Public
* License along with this program; if not, write to the
* Free Software Foundation, Inc., 59 Temple Place - Suite 330,
* Boston, MA 021110-1307, USA.
*/
#define _GNU_SOURCE
#include <features.h>
#include <arpa/inet.h>
#include <assert.h>
#include <ctype.h>
#include <errno.h>
#include <fcntl.h>
#include <getopt.h>
#include <libgen.h>
#include <limits.h>
#include <netdb.h>
#include <netinet/in.h>
#include <pthread.h>
#include <semaphore.h>
#include <signal.h>
#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <syslog.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <sys/time.h>
#include <sys/types.h>
#include <time.h>
#include <unistd.h>
#include "kernel-list.h"
#include "btree.h"
#ifndef VERSION
#define VERSION "0.2.0"
#endif
#define PROGNAME "cancd"
/*
* Log prefix. This is the path all logs are placed under. Modified
* with the '-l' option.
*/
static char *log_prefix = "/var/crash";
/*
* Output file format. The format is a strftime(3) string with %Q (the
* only unused letter in strftime(3)) representing the IP address of
* the machine sending the netconsole message.
* The default of "%Q/%Y-%m-%d-%H:%M/log" Means that a machine
* 10.0.0.1 sending a message at 3:30pm on 2005/10/1 would output
* to 10.0.0.1/2005-10-01-15:30/log. The files are underneath
* the log_prefix. Modified with the '-o' option.
*/
static char *log_format = "%Q.log";
/*
* Port number to listen on. Modified with the '-p' option.
*/
static uint16_t log_port = 7075;
/* Socket we are using */
static int sock_fd;
/* Are we a daemon? */
static int daemonize = 1;
/* What signal did we catch? */
sig_atomic_t caught_sig = 0;
struct btree_head32 btree;
pthread_t dns_thread;
sem_t dns_sem;
struct source_ip {
const char *filename;
const char *tmpfilename;
int had_newline;
};
void handler(int signum)
{
caught_sig = signum;
}
static int setup_signals()
{
int rc = 0;
struct sigaction act;
act.sa_sigaction = NULL;
act.sa_restorer = NULL;
sigemptyset(&act.sa_mask);
act.sa_handler = handler;
#ifdef SA_INTERRUPT
act.sa_flags = SA_INTERRUPT;
#endif
rc += sigaction(SIGTERM, &act, NULL);
rc += sigaction(SIGINT, &act, NULL);
act.sa_handler = SIG_IGN;
rc += sigaction(SIGPIPE, &act, NULL); /* Get EPIPE instead */
return rc;
}
static char *get_path(const void *addr)
{
char ntop[INET6_ADDRSTRLEN + 1];
char path[PATH_MAX];
size_t ntop_len;
char *src, *dst;
int havep;
if (!inet_ntop(PF_INET, addr, ntop, sizeof(ntop))) {
syslog(LOG_ERR, "Unable to resolve address: %s", strerror(errno));
return NULL;
}
ntop_len = strlen(ntop);
for (havep = 0, src = log_format, dst = path; *src; src++) {
switch (*src) {
case '%':
if (havep) {
/* '%%' should be passed on to strftime(3) */
*dst++ = '%';
*dst++ = '%';
havep = 0;
} else
havep = 1;
break;
case 'Q':
if (havep) {
/* Copy our address in */
memmove(dst, ntop, ntop_len);
dst += ntop_len;
havep = 0;
break;
}
/* Fall Through */
default:
if (havep) {
*dst++ = '%';
havep = 0;
}
*dst++ = *src;
break;
}
}
*dst++ = 0;
return strdup(path);
}
struct dir_to_make {
struct list_head list;
char *path;
};
static int make_tree(const char *path, int mode)
{
struct stat stat_buf;
char *ptr, *tmp;
int rc;
LIST_HEAD(to_make);
struct list_head *p, *n;
struct dir_to_make *m;
if (!path) {
syslog(LOG_ERR, "Can\'t make empty path!");
return -EINVAL;
}
ptr = strdup(path);
if (!ptr) {
syslog(LOG_ERR, "Unable to allocate memory for path \"%s\"", path);
return -ENOMEM;
}
while (*ptr && strcmp(ptr, ".") && strcmp(ptr, "/")) {
rc = stat(ptr, &stat_buf);
if (rc) {
if (errno != ENOENT) {
rc = -errno;
syslog(LOG_ERR, "Unable to stat \"%s\" while creating \"%s\": %s", ptr, path, strerror(-rc));
goto out_error;
}
} else {
if (!S_ISDIR(stat_buf.st_mode)) {
rc = -ENOTDIR;
syslog(LOG_ERR, "Path \"%s\" is not a directory while creating \"%s\"\n", ptr, path);
goto out_error;
}
/* Found an existing parent, we're done */
break;
}
/* If we got here, there's a path component we need to make */
rc = -ENOMEM;
m = malloc(sizeof(struct dir_to_make));
if (!m) {
syslog(LOG_ERR, "Unable to allocate memory while creating path \"%s\"", path);
goto out_error;
}
list_add(&m->list, &to_make);
m->path = ptr;
/* Create a temporary copy for dirname(3) */
tmp = strdup(ptr);
if (!tmp) {
syslog(LOG_ERR, "Unable to allocate memory while creating path \"%s\"", path);
goto out_error;
}
ptr = dirname(tmp);
ptr = strdup(ptr); /* Because dirname could return static */
free(tmp);
if (!ptr) {
syslog(LOG_ERR, "Unable to allocate memory while creating path \"%s\"", path);
goto out_error;
}
}
list_for_each(p, &to_make) {
m = list_entry(p, struct dir_to_make, list);
rc = mkdir(m->path, mode);
if (rc) {
rc = -errno;
syslog(LOG_ERR, "Unable to create directory \"%s\" while creating path \"%s\": %s", m->path, path, strerror(-rc));
goto out_error;
}
}
rc = 0;
out_error:
free(ptr);
list_for_each_safe(p, n, &to_make) {
m = list_entry(p, struct dir_to_make, list);
list_del(&m->list);
if (m->path)
free(m->path);
free(m);
}
return rc;
}
static int open_socket()
{
int rc;
struct sockaddr_in servaddr = { 0, };
sock_fd = socket(PF_INET, SOCK_DGRAM, 0);
if (sock_fd < 0) {
rc = -errno;
syslog(LOG_ERR, "Unable to open socket: %s", strerror(-rc));
return rc;
}
servaddr.sin_family = PF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(log_port);
rc = bind(sock_fd, (struct sockaddr *)&servaddr, sizeof(struct sockaddr_in));
if (rc) {
rc = -errno;
syslog(LOG_ERR, "Unable to bind socket: %s", strerror(-rc));
close(sock_fd);
sock_fd = -1;
return rc;
}
return 0;
}
static int do_write(int fd, const void *buf, size_t count)
{
ssize_t ret;
do {
ret = write(fd, buf, count);
if (ret < 0 && errno == EINTR)
continue;
if (ret < 0)
return ret;
count -= ret;
} while (count);
return 0;
}
static int write_formatted(int fd, struct source_ip *sip, char *buf, int count)
{
const char *format = "%b %d %H:%M:%S ";
char *end;
char timestr[200];
size_t n, len, err;
time_t now = time(NULL);
struct tm *tm = localtime(&now);
/*
* buf must be NUL-terminated. We add 1 to count for the terminating
* byte. But we also abort the loop of only the terminating byte
* remains, so we don't print stray newlines
*/
count += 1;
while (count > 1) {
if (sip->had_newline) {
n = strftime(timestr, sizeof(timestr), format, tm);
err = do_write(fd, timestr, n);
if (err)
return err;
}
end = strchr(buf, 0xa);
if (!end) {
/* no newline, just write what we have */
do_write(fd, buf, count - 1);
sip->had_newline = 0;
break;
}
len = end - buf + 1;
err = do_write(fd, buf, len);
if (err)
return err;
buf += len;
count -= len;
sip->had_newline = 1;
}
return 0;
}
static struct source_ip *get_source_ip(struct sockaddr_in *addr)
{
u32 key = addr->sin_addr.s_addr;
struct source_ip *sip;
int err;
sip = btree_lookup32(&btree, key);
if (!sip) {
sip = calloc(1, sizeof(*sip));
sip->had_newline = 1;
sip->tmpfilename = get_path(&addr->sin_addr);
err = btree_insert32(&btree, key, sip);
assert(!err);
sem_post(&dns_sem);
}
return sip;
}
/*
* Basically just returns the filename, but with a twist. The until the DNS
* resolver gives us a decent filename, we use a temporary one based on the
* IP address. Once we have a decent filename, we copy everything from the
* temporary file to the final one. So hopefully there is just a short
* window with IP filenames around and never should we lose data.
*/
static const char *copy_tmpfile(struct source_ip *sip)
{
int fd, tmpfd;
const char *name, *tmpname;
name = sip->filename;
tmpname = sip->tmpfilename;
if (name && tmpname) {
char buf[4096];
ssize_t count;
fd = open(name, O_WRONLY | O_APPEND | O_CREAT, 0600);
if (fd < 0) {
syslog(LOG_ERR, "Unable to open \"%s\": %s", name,
strerror(errno));
goto out;
}
tmpfd = open(tmpname, O_RDONLY);
if (tmpfd < 0) {
syslog(LOG_ERR, "Unable to open \"%s\": %s", name,
strerror(errno));
close(fd);
goto out;
}
while ((count = read(tmpfd, buf, sizeof(buf))) > 0)
do_write(fd, buf, count);
close(tmpfd);
close(fd);
unlink(tmpname);
sip->tmpfilename = NULL;
free((void *)tmpname);
}
out:
if (sip->tmpfilename)
return sip->tmpfilename;
if (sip->filename)
return sip->filename;
return NULL;
}
static void do_output(char *buf, int len, struct sockaddr_in *addr, socklen_t socklen)
{
int fd;
const char *name;
struct source_ip *sip = get_source_ip(addr);
name = copy_tmpfile(sip);
if (!name)
return;
fd = open(name, O_WRONLY | O_APPEND | O_CREAT, 0600);
if (fd < 0)
syslog(LOG_ERR, "Unable to open \"%s\": %s", name, strerror(errno));
else {
write_formatted(fd, sip, buf, len);
close(fd);
}
}
static int set_blocking(int blocking)
{
int flags, rc;
flags = fcntl(sock_fd, F_GETFL, 0);
if (flags < 0) {
rc = -errno;
syslog(LOG_ERR, "Cannot get blocking state: %s", strerror(-rc));
return rc;
}
if (blocking)
flags &= ~O_NONBLOCK;
else
flags |= O_NONBLOCK;
rc = fcntl(sock_fd, F_SETFL, flags);
if (rc) {
rc = -errno;
syslog(LOG_ERR, "Cannot set blocking state: %s", strerror(-rc));
}
return rc;
}
/*
* There is some magic state here. To allow us to batch things
* if we want, we go O_NONBLOCK once data is available. Once we
* see that data isn't available anymore, we go back to blocking.
*/
static int do_recvfrom(int fd, void *buf, size_t bufsize, int flags, struct sockaddr_in *from, socklen_t * fromlen)
{
int rc;
static int block = 1;
rc = recvfrom(sock_fd, buf, bufsize, 0, (struct sockaddr *)from, fromlen);
if (rc < 0) {
rc = -errno;
if (rc == -EAGAIN) {
if (!block) {
rc = set_blocking(1);
if (!rc) {
block = 1;
rc = -EAGAIN;
} else
syslog(LOG_ERR, "Never blocking means eating CPU, goodbye");
}
} else if (rc != -EINTR)
syslog(LOG_ERR, "Error reading from socket: %s", strerror(-rc));
} else {
if (block) {
if (!set_blocking(0))
block = 0;
else
syslog(LOG_ERR, "Unable to set nonblocking");
}
}
return rc;
}
static int run()
{
int rc;
char *buf;
size_t bufsize = 0x100000;
struct sockaddr_in from;
socklen_t fromlen;
buf = malloc(bufsize);
if (!buf) {
syslog(LOG_ERR, "Unable to allocate memory for receive buffer: %s", strerror(errno));
return -ENOMEM;
}
syslog(LOG_INFO, "Logging to %s on port %d", log_prefix, log_port);
while (1) {
rc = do_recvfrom(sock_fd, buf, bufsize, 0, &from, &fromlen);
if (rc < 0) {
if (rc == -EAGAIN)
continue;
if (rc == -EINTR)
syslog(LOG_INFO, "Caught signal %d", caught_sig);
else
syslog(LOG_ERR, "Error reading from socket: %s", strerror(-rc));
break;
}
if (rc >= bufsize) {
rc = bufsize - 1;
syslog(LOG_ERR, "Receive buffer too small (%zd %d)", bufsize, rc);
}
buf[rc] = 0;
/* For now, we process one at a time */
do_output(buf, rc, &from, fromlen);
}
syslog(LOG_INFO, "Shutting down");
return 0;
}
static int init_dir()
{
int rc;
rc = make_tree(log_prefix, 0755);
if (!rc) {
rc = chdir(log_prefix);
if (rc) {
rc = -errno;
syslog(LOG_ERR, "Unable to change to \"%s\"; %s", log_prefix, strerror(-rc));
}
}
return rc;
}
static int init_self()
{
int rc;
openlog(PROGNAME, LOG_PERROR | LOG_PID, LOG_DAEMON);
rc = init_dir();
if (rc)
return rc;
if (daemonize) {
rc = daemon(1, 0);
if (rc) {
rc = -errno;
syslog(LOG_ERR, "daemon() failed: %s", strerror(-rc));
}
}
if (!rc) {
rc = setup_signals();
if (rc)
syslog(LOG_ERR, "Unable to set up signal handling");
}
return rc;
}
static int valid_format()
{
struct in_addr addr = { 0, };
char *name;
name = get_path(&addr);
free(name);
return !!name;
}
static void print_version(void)
{
fprintf(stdout, PROGNAME " version %s\n", VERSION);
exit(0);
}
static void print_usage(int rc)
{
FILE *output = rc ? stderr : stdout;
fprintf(output, "Usage: " PROGNAME " [-l <log_prefix>] [-o <log_name_format>] [-p <port>]\n" " " PROGNAME " -h\n" " " PROGNAME " -V\n");
exit(rc);
}
extern char *optarg;
extern int optopt;
extern int opterr;
static int parse_options(int argc, char *argv[])
{
int c;
opterr = 0;
while ((c = getopt(argc, argv, ":hVDl:o:p:-:")) != EOF) {
switch (c) {
case 'h':
print_usage(0);
break;
case 'V':
print_version();
break;
case '-':
if (!strcmp(optarg, "version"))
print_version();
else if (!strcmp(optarg, "help"))
print_usage(0);
else {
fprintf(stderr, PROGNAME ": Invalid option: \'--%s\'\n", optarg);
print_usage(-EINVAL);
}
break;
case 'l':
if (!optarg || !*optarg) {
fprintf(stderr, PROGNAME ": Invalid log prefix \"%s\"\n", optarg);
print_usage(-EINVAL);
}
log_prefix = optarg;
break;
case 'o':
log_format = optarg;
if (!log_format || !*log_format || !valid_format()) {
fprintf(stderr, PROGNAME ": Invalid log filename format \"%s\"\n", optarg);
print_usage(-EINVAL);
}
break;
case 'p':
log_port = (uint16_t) (atoi(optarg) & (uint16_t) - 1);
if (!log_port) {
fprintf(stderr, PROGNAME ": Invalid port: \"%s\"\n", optarg);
print_usage(-EINVAL);
}
break;
case 'D':
daemonize = 0;
break;
case '?':
fprintf(stderr, PROGNAME ": Invalid option: \'-%c\'\n", optopt);
print_usage(-EINVAL);
break;
case ':':
fprintf(stderr, PROGNAME ": Option \'-%c\' requires an argument\n", optopt);
print_usage(-EINVAL);
break;
default:
fprintf(stderr, PROGNAME ": Shouldn\'t get here\n");
exit(1);
break;
}
}
return 0;
}
static void dns_visitor(void *_sip, long unused, u32 ip, size_t unused2)
{
struct source_ip *sip = _sip;
struct hostent *he;
const char *old;
char *new, *c;
he = gethostbyaddr(&ip, 4, AF_INET);
if (!he)
return;
new = strdup(he->h_name);
if (!new)
return;
for (c = new; c; c++) {
/* normalize and shorten name */
*c = tolower(*c);
if (*c == '.') {
*c = 0;
break;
}
}
old = sip->filename;
sip->filename = new;
free((void *)old);
}
void *dns_thread_func(void *_arg)
{
struct timespec ts;
struct timeval tv;
for (;;) {
/* Wait for 60min or the main thread to wake us */
gettimeofday(&tv, NULL);
ts.tv_sec = tv.tv_sec + 3600;
ts.tv_nsec = 0;
sem_timedwait(&dns_sem, &ts);
/* Do DNS lookups for all hosts */
btree_visitor32(&btree, 0, dns_visitor);
}
}
int main(int argc, char *argv[])
{
int rc;
btree_init32(&btree);
rc = parse_options(argc, argv);
if (rc)
return rc;
rc = init_self();
if (rc)
goto out;
rc = open_socket();
if (rc)
goto out;
rc = pthread_create(&dns_thread, NULL, dns_thread_func, NULL);
if (rc)
goto out;
rc = run();
close(sock_fd);
out:
closelog();
return rc;
}