root/sbin/svc/svc.c
/*
 * Copyright (c) 2014 The DragonFly Project.  All rights reserved.
 *
 * This code is derived from software contributed to The DragonFly Project
 * by Matthew Dillon <dillon@backplane.com>
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 *
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in
 *    the documentation and/or other materials provided with the
 *    distribution.
 * 3. Neither the name of The DragonFly Project nor the names of its
 *    contributors may be used to endorse or promote products derived
 *    from this software without specific, prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 * ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
 * FOR A PARTICULAR PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL THE
 * COPYRIGHT HOLDERS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
 * INCIDENTAL, SPECIAL, EXEMPLARY OR CONSEQUENTIAL DAMAGES (INCLUDING,
 * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
 * AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
 * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
 * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
 * SUCH DAMAGE.
 */
/*
 * SERVICE MANAGER
 *
 * This program builds an environment to run a service in and provides
 * numerous options for naming, tracking, and management.  It uses
 * reapctl(2) to corral the processes under management.
 */

#include "svc.h"

static int execute_remote(command_t *cmd, int (*func)(command_t *cmd));
static int process_jailspec(command_t *cmd, const char *spec);

int
main(int ac, char **av)
{
        command_t cmd;
        int rc;

        signal(SIGPIPE, SIG_IGN);

        rc = process_cmd(&cmd, stdout, ac, av);
        cmd.cmdline = 1;        /* commanded from front-end */
        cmd.commanded = 1;      /* commanded action (vs automatic) */
        if (rc == 0)
                rc = execute_cmd(&cmd);
        free_cmd(&cmd);

        return rc;
}

int
process_cmd(command_t *cmd, FILE *fp, int ac, char **av)
{
        const char *optstr = "dfhp:r:R:xst:u:g:G:l:c:mj:k:T:F:";
        struct group *grent;
        struct passwd *pwent;
        char *sub;
        char *cpy;
        int rc = 1;
        int ch;
        int i;

        bzero(cmd, sizeof(*cmd));
        cmd->fp = fp;                           /* error and output reporting */
        cmd->logfd = -1;
        sreplace(&cmd->piddir, "/var/run");     /* must not be NULL */
        cmd->termkill_timo = -1;                /* will use default value */
        cmd->orig_ac = ac;
        cmd->orig_av = av;
        cmd->empty_label = 1;

        optind = 1;
        opterr = 1;
        optreset = 1;

        while ((ch = getopt(ac, av, optstr)) != -1) {
                switch(ch) {
                case 'd':
                        cmd->debug = 1;
                        cmd->foreground = 1;
                        break;
                case 'f':
                        cmd->foreground = 1;
                        break;
                case 'h':
                        execute_help(cmd);
                        exit(0);
                        break;
                case 'p':
                        sreplace(&cmd->piddir, optarg);
                        break;
                case 'r':
                        cmd->restart_some = 1;
                        cmd->restart_all = 0;
                        cmd->restart_timo = strtol(optarg, NULL, 0);
                        break;
                case 'R':
                        cmd->restart_some = 0;
                        cmd->restart_all = 1;
                        cmd->restart_timo = strtol(optarg, NULL, 0);
                        break;
                case 'x':
                        cmd->exit_mode = 1;
                        break;
                case 's':
                        cmd->sync_mode = 1;
                        break;
                case 't':
                        cmd->termkill_timo = strtoul(optarg, NULL, 0);
                        break;
                case 'u':
                        if (isdigit(optarg[0])) {
                                pwent = getpwuid(strtol(optarg, NULL, 0));
                        } else {
                                pwent = getpwnam(optarg);
                        }
                        if (pwent == NULL) {
                                fprintf(fp, "Cannot find user %s: %s\n",
                                        optarg,
                                        strerror(errno));
                                goto failed;
                        }
                        cmd->uid_mode = 1;
                        sfree(&cmd->pwent.pw_name);
                        sfree(&cmd->pwent.pw_passwd);
                        sfree(&cmd->pwent.pw_class);
                        sfree(&cmd->pwent.pw_gecos);
                        sfree(&cmd->pwent.pw_dir);
                        sfree(&cmd->pwent.pw_shell);
                        cmd->pwent = *pwent;
                        sdup(&cmd->pwent.pw_name);
                        sdup(&cmd->pwent.pw_passwd);
                        sdup(&cmd->pwent.pw_class);
                        sdup(&cmd->pwent.pw_gecos);
                        sdup(&cmd->pwent.pw_dir);
                        sdup(&cmd->pwent.pw_shell);
                        break;
                case 'g':
                        setgroupent(1);
                        if (isdigit(optarg[0])) {
                                grent = getgrgid(strtol(optarg, NULL, 0));
                        } else {
                                grent = getgrnam(optarg);
                        }
                        if (grent == NULL) {
                                fprintf(fp, "Cannot find group %s: %s\n",
                                        optarg,
                                        strerror(errno));
                                goto failed;
                        }
                        cmd->gid_mode = 1;
                        sfree(&cmd->grent.gr_name);
                        sfree(&cmd->grent.gr_passwd);
                        afree(&cmd->grent.gr_mem);
                        cmd->grent = *grent;
                        sdup(&cmd->grent.gr_name);
                        sdup(&cmd->grent.gr_passwd);
                        adup(&cmd->grent.gr_mem);
                        break;
                case 'G':
                        setgroupent(1);
                        cpy = strdup(optarg);
                        sub = strtok(cpy, ",");
                        i = 0;
                        while (sub) {
                                if (isdigit(sub[0])) {
                                        grent = getgrgid(strtol(sub, NULL, 0));
                                } else {
                                        grent = getgrnam(sub);
                                }
                                if (grent == NULL) {
                                        fprintf(fp,
                                                "Cannot find group %s: %s\n",
                                                sub, strerror(errno));
                                        i = -1;
                                }
                                if (i == NGROUPS) {
                                        fprintf(fp,
                                                "Too many groups specified, "
                                                "max %d\n", NGROUPS);
                                        i = -1;
                                }
                                if (i >= 0)
                                        cmd->groups[i++] = grent->gr_gid;
                                sub = strtok(NULL, ",");
                        }
                        free(cpy);
                        if (i < 0)
                                goto failed;
                        cmd->ngroups = i;
                        break;
                case 'l':
                        sreplace(&cmd->logfile, optarg);
                        break;
                case 'c':
                        sreplace(&cmd->rootdir, optarg);
                        break;
                case 'm':
                        cmd->mountdev = 1;
                        break;
                case 'j':
                        sreplace(&cmd->jaildir, optarg);
                        break;
                case 'k':
                        rc = process_jailspec(cmd, optarg);
                        if (rc)
                                goto failed;
                        break;
                case 'T':
                        sreplace(&cmd->proctitle, optarg);
                        break;
                case 'F':
                        cmd->restart_per = 60;
                        if (sscanf(optarg, "%d:%d",
                                   &cmd->restart_count,
                                   &cmd->restart_per) < 1) {
                                fprintf(fp, "bad restart specification: %s\n",
                                        optarg);
                                goto failed;
                        }
                        break;
                default:
                        fprintf(fp, "Unknown option %c\n", ch);
                        goto failed;
                }
        }

        /*
         * directive [label] [...additional args]
         *
         * If 'all' is specified the label field is left NULL (ensure that
         * it is NULL), and empty_label is still cleared so safety code works.
         */
        i = optind;
        if (av[i]) {
                cmd->directive = strdup(av[i]);
                ++i;
                if (av[i]) {
                        cmd->empty_label = 0;
                        if (strcmp(av[i], "all") == 0)
                                sfree(&cmd->label);
                        else
                                cmd->label = strdup(av[i]);
                        ++i;
                        cmd->ext_av = av + i;
                        cmd->ext_ac = ac - i;
                        adup(&cmd->ext_av);
                }
        } else {
                fprintf(fp, "No directive specified\n");
                goto failed;
        }
        rc = 0;
failed:
        endgrent();
        endpwent();

        return rc;
}

int
execute_cmd(command_t *cmd)
{
        const char *directive;
        int rc;

        directive = cmd->directive;

        /*
         * Safely, require a label for directives that do not match
         * this list, or 'all'.  Do not default to all if no label
         * is specified.  e.g. things like 'kill' or 'exit' could
         * blow up the system.
         */
        if (cmd->empty_label) {
                if (strcmp(directive, "status") != 0 &&
                    strcmp(directive, "list") != 0 &&
                    strcmp(directive, "log") != 0 &&
                    strcmp(directive, "logf") != 0 &&
                    strcmp(directive, "help") != 0 &&
                    strcmp(directive, "tailf") != 0)  {
                        fprintf(cmd->fp,
                                "Directive requires a label or 'all': %s\n",
                                directive);
                        rc = 1;
                        return rc;
                }
        }

        /*
         * Process directives.  If we are on the remote already the
         * execute_remote() function will simply chain to the passed-in
         * function.
         */
        if (strcmp(directive, "init") == 0) {
                rc = execute_init(cmd);
        } else if (strcmp(directive, "help") == 0) {
                rc = execute_help(cmd);
        } else if (strcmp(directive, "start") == 0) {
                rc = execute_remote(cmd, execute_start);
        } else if (strcmp(directive, "stop") == 0) {
                rc = execute_remote(cmd, execute_stop);
        } else if (strcmp(directive, "stopall") == 0) {
                cmd->restart_some = 0;
                cmd->restart_all = 1;
                rc = execute_remote(cmd, execute_stop);
        } else if (strcmp(directive, "restart") == 0) {
                rc = execute_remote(cmd, execute_restart);
        } else if (strcmp(directive, "exit") == 0) {
                cmd->restart_some = 0;
                cmd->restart_all = 1;           /* stop everything */
                cmd->force_remove_files = 1;
                rc = execute_remote(cmd, execute_exit);
        } else if (strcmp(directive, "kill") == 0) {
                cmd->restart_some = 0;
                cmd->restart_all = 1;           /* stop everything */
                cmd->termkill_timo = 0;         /* force immediate SIGKILL */
                cmd->force_remove_files = 1;
                rc = execute_remote(cmd, execute_exit);
        } else if (strcmp(directive, "list") == 0) {
                rc = execute_remote(cmd, execute_list);
        } else if (strcmp(directive, "status") == 0) {
                rc = execute_remote(cmd, execute_status);
        } else if (strcmp(directive, "log") == 0) {
                rc = execute_remote(cmd, execute_log);
        } else if (strcmp(directive, "logf") == 0) {
                cmd->tail_mode = 1;
                rc = execute_remote(cmd, execute_log);
        } else if (strcmp(directive, "tailf") == 0) {
                cmd->tail_mode = 2;
                rc = execute_remote(cmd, execute_log);
        } else if (strcmp(directive, "logfile") == 0) {
                rc = execute_remote(cmd, execute_logfile);
        } else {
                fprintf(cmd->fp, "Unknown directive: %s\n", directive);
                rc = 1;
        }
        return rc;
}

static
int
execute_remote(command_t *cmd, int (*func)(command_t *cmd))
{
        DIR *dir;
        struct dirent *den;
        const char *p1;
        const char *p2;
        char *plab;
        size_t cmdlen;
        size_t len;
        int rc;

        /*
         * If already on the remote service just execute the operation
         * as requested.
         */
        if (cmd->cmdline == 0) {
                return (func(cmd));
        }

        /*
         * Look for label(s).  If no exact match or label is NULL, scan
         * piddir for matches.
         */
        if ((dir = opendir(cmd->piddir)) == NULL) {
                fprintf(cmd->fp, "Unable to scan \"%s\"\n", cmd->piddir);
                return 1;
        }

        rc = 0;
        cmdlen = (cmd->label ? strlen(cmd->label) : 0);

        while ((den = readdir(dir)) != NULL) {
                /*
                 * service. prefix.
                 */
                if (strncmp(den->d_name, "service.", 8) != 0)
                        continue;

                /*
                 * .sk suffix
                 */
                p1 = den->d_name + 8;
                p2 = strrchr(p1, '.');
                if (p2 == NULL || p2 < p1 || strcmp(p2, ".sk") != 0)
                        continue;

                /*
                 * Extract the label from the service.<label>.sk name.
                 */
                len = p2 - p1;
                plab = strdup(p1);
                *strrchr(plab, '.') = 0;

                /*
                 * Start remote execution (in parallel) for all matching
                 * labels.  This will generally create some asynchronous
                 * threads.
                 */
                if (cmdlen == 0 ||
                    (cmdlen <= len && strncmp(cmd->label, plab, cmdlen) == 0)) {
                        remote_execute(cmd, plab);
                }
                free(plab);
        }
        closedir(dir);

        /*
         * Wait for completion of remote commands and dump output.
         */
        rc = remote_wait();

        return rc;
}

void
free_cmd(command_t *cmd)
{
        sfree(&cmd->piddir);

        sfree(&cmd->pwent.pw_name);
        sfree(&cmd->pwent.pw_passwd);
        sfree(&cmd->pwent.pw_class);
        sfree(&cmd->pwent.pw_gecos);
        sfree(&cmd->pwent.pw_dir);
        sfree(&cmd->pwent.pw_shell);

        sfree(&cmd->grent.gr_name);
        sfree(&cmd->grent.gr_passwd);
        afree(&cmd->grent.gr_mem);

        sfree(&cmd->logfile);
        sfree(&cmd->rootdir);
        sfree(&cmd->jaildir);
        sfree(&cmd->proctitle);
        sfree(&cmd->directive);
        sfree(&cmd->label);
        afree(&cmd->ext_av);

        if (cmd->logfd >= 0) {
                close(cmd->logfd);
                cmd->logfd = -1;
        }

        bzero(cmd, sizeof(*cmd));
}

static
int
process_jailspec(command_t *cmd, const char *spec)
{
        char *cpy = strdup(spec);
        char *ptr;
        int rc = 0;

        ptr = strtok(cpy, ",");
        while (ptr) {
                if (strcmp(ptr, "clean") == 0) {
                        cmd->jail_clean = 1;
                } else if (strncmp(ptr, "ip=", 3) == 0) {
                        assert(0); /* XXX TODO */
                } else {
                        fprintf(cmd->fp, "jail-spec '%s' not understood\n",
                                ptr);
                        rc = 1;
                }
                ptr = strtok(NULL, ",");
        }
        free(cpy);

        return rc;
}