/*
 * Copyright (C) 2009 Andrew Beekhof <andrew@beekhof.net>
 *
 * 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 software 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 library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
 */

#include <crm_internal.h>

#include <sys/param.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <sys/utsname.h>

#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <fcntl.h>

#include <crm/crm.h>
#include <crm/msg_xml.h>
#include <crm/common/ipc.h>
#include <crm/cluster/internal.h>
#include <crm/common/mainloop.h>

#include <crm/stonith-ng.h>
#include <crm/cib.h>
#include <crm/pengine/status.h>

#include <crm/common/xml.h>


/* *INDENT-OFF* */
static struct crm_option long_options[] = {
    {   "help", no_argument, NULL, '?',
        "\tDisplay this text and exit."
    },
    {   "version", no_argument, NULL, '$',
        "\tDisplay version information and exit."
    },
    {   "verbose", no_argument, NULL, 'V',
        "\tIncrease debug output (may be specified multiple times)."
    },
    {   "quiet", no_argument, NULL, 'q',
        "\tBe less descriptive in output."
    },

    {   "-spacer-", no_argument, NULL, '-', "\nDevice definition commands:" },

    {   "register", required_argument, NULL, 'R',
        "Register the named stonith device. Requires: --agent.\n"
        "\t\t\tOptional: any number of --option and/or --env entries."
    },
    {   "deregister", required_argument, NULL, 'D',
        "De-register the named stonith device."
    },
    {   "register-level", required_argument, NULL, 'r',
        "Register a stonith level for the named target,\n"
        "\t\t\tspecified as one of NAME, @PATTERN, or ATTR=VALUE.\n"
        "\t\t\tRequires: --index and one or more --device entries."
    },
    {   "deregister-level", required_argument, NULL, 'd',
        "Unregister a stonith level for the named target,\n"
        "\t\t\tspecified as for --register-level. Requires: --index."
    },

    {   "-spacer-", no_argument, NULL, '-', "\nQueries:" },

    {   "list", required_argument, NULL, 'l',
        "List devices that can terminate the specified host.\n"
        "\t\t\tOptional: --timeout."
    },
    {   "list-registered", no_argument, NULL, 'L',
        "List all registered devices. Optional: --timeout."
    },
    {   "list-installed", no_argument, NULL, 'I',
        "List all installed devices. Optional: --timeout."
    },
    {   "list-targets", required_argument, NULL, 's',
        "List the targets that can be fenced by the\n"
        "\t\t\tnamed device. Optional: --timeout."
    },
    {   "metadata", no_argument, NULL, 'M',
        "\tShow agent metadata. Requires: --agent.\n"
        "\t\t\tOptional: --timeout."
    },
    {   "query", required_argument, NULL, 'Q',
        "Check the named device's status. Optional: --timeout."
    },

    {   "-spacer-", no_argument, NULL, '-', "\nFencing Commands:" },

    {   "fence", required_argument, NULL, 'F',
        "Fence named host. Optional: --timeout, --tolerance."
    },
    {   "unfence", required_argument, NULL, 'U',
        "Unfence named host. Optional: --timeout, --tolerance."
    },
    {   "reboot", required_argument, NULL, 'B',
        "Reboot named host. Optional: --timeout, --tolerance."
    },
    {   "confirm", required_argument, NULL, 'C',
        "Tell cluster that named host is now safely down."
    },
    {   "history", required_argument, NULL, 'H',
        "Show last successful fencing operation for named node\n"
        "\t\t\t(or '*' for all nodes). Optional: --timeout, --quiet\n"
        "\t\t\t(show only the operation's epoch timestamp),\n"
        "\t\t\t--verbose (show all recorded and pending operations)."
    },
    {   "last", required_argument, NULL, 'h',
        "Indicate when the named node was last fenced.\n"
        "\t\t\tOptional: --as-node-id."
    },

    {   "-spacer-", no_argument, NULL, '-', "\nAdditional Options:" },

    {   "agent", required_argument, NULL, 'a',
        "The agent to use (for example, fence_xvm;\n"
        "\t\t\twith --register, --metadata)."
    },
    {   "option", required_argument, NULL, 'o',
        "Specify a device configuration parameter as NAME=VALUE\n"
        "\t\t\t(with --register)."
    },
    {   "env-option", required_argument, NULL, 'e',
        "Specify a device configuration parameter with the\n"
        "\t\t\tspecified name, using the value of the\n"
        "\t\t\tenvironment variable of the same name prefixed with\n"
        "\t\t\tOCF_RESKEY_ (with --register)."
    },
    {   "tag", required_argument, NULL, 'T',
        "Identify fencing operations in logs with the specified\n"
        "\t\t\ttag; useful when multiple entities might invoke\n"
        "\t\t\tstonith_admin (used with most commands)."
    },
    {   "device", required_argument, NULL, 'v',
        "A device to associate with a given host and\n"
        "\t\t\tstonith level (with --register-level)."
    },
    {   "index", required_argument, NULL, 'i',
        "The stonith level (1-9) (with --register-level,\n"
        "\t\t\t--deregister-level)."
    },
    {   "timeout", required_argument, NULL, 't',
        "Operation timeout in seconds (default 120;\n"
        "\t\t\tused with most commands)."
    },
    {   "as-node-id", no_argument, NULL, 'n',
        "(Advanced) The supplied node is the corosync node ID\n"
        "\t\t\t(with --last)."
    },
    {   "tolerance", required_argument, NULL,   0,
        "(Advanced) Do nothing if an equivalent --fence request\n"
        "\t\t\tsucceeded less than this many seconds earlier\n"
        "\t\t\t(with --fence, --unfence, --reboot)."
    },

    {   "list-all", no_argument, NULL, 'L', NULL, pcmk_option_hidden },
    { 0, 0, 0, 0 }
};
/* *INDENT-ON* */

int st_opts = st_opt_sync_call | st_opt_allow_suicide;

GMainLoop *mainloop = NULL;
struct {
    stonith_t *st;
    const char *target;
    const char *action;
    char *name;
    int timeout;
    int tolerance;
    int rc;
} async_fence_data;

static int
try_mainloop_connect(void)
{
    stonith_t *st = async_fence_data.st;
    int tries = 10;
    int i = 0;
    int rc = 0;

    for (i = 0; i < tries; i++) {
        crm_debug("Connecting as %s", async_fence_data.name);
        rc = st->cmds->connect(st, async_fence_data.name, NULL);

        if (!rc) {
            crm_debug("stonith client connection established");
            return 0;
        } else {
            crm_debug("stonith client connection failed");
        }
        sleep(1);
    }

    crm_err("Could not connect to stonithd.");
    return -1;
}

static void
notify_callback(stonith_t * st, stonith_event_t * e)
{
    if (e->result != pcmk_ok) {
        return;
    }

    if (safe_str_eq(async_fence_data.target, e->target) &&
        safe_str_eq(async_fence_data.action, e->action)) {

        async_fence_data.rc = e->result;
        g_main_loop_quit(mainloop);
    }
}

static void
fence_callback(stonith_t * stonith, stonith_callback_data_t * data)
{
    async_fence_data.rc = data->rc;

    g_main_loop_quit(mainloop);
}

static gboolean
async_fence_helper(gpointer user_data)
{
    stonith_t *st = async_fence_data.st;
    int call_id = 0;

    if (try_mainloop_connect()) {
        g_main_loop_quit(mainloop);
        return TRUE;
    }

    st->cmds->register_notification(st, T_STONITH_NOTIFY_FENCE, notify_callback);

    call_id = st->cmds->fence(st,
                              st_opt_allow_suicide,
                              async_fence_data.target,
                              async_fence_data.action,
                              async_fence_data.timeout, async_fence_data.tolerance);

    if (call_id < 0) {
        g_main_loop_quit(mainloop);
        return TRUE;
    }

    st->cmds->register_callback(st,
                                call_id,
                                async_fence_data.timeout,
                                st_opt_timeout_updates, NULL, "callback", fence_callback);

    return TRUE;
}

static int
mainloop_fencing(stonith_t * st, const char *target, const char *action, int timeout, int tolerance)
{
    crm_trigger_t *trig;

    async_fence_data.st = st;
    async_fence_data.target = target;
    async_fence_data.action = action;
    async_fence_data.timeout = timeout;
    async_fence_data.tolerance = tolerance;
    async_fence_data.rc = -1;

    trig = mainloop_add_trigger(G_PRIORITY_HIGH, async_fence_helper, NULL);
    mainloop_set_trigger(trig);

    mainloop = g_main_new(FALSE);
    g_main_run(mainloop);

    return async_fence_data.rc;
}

static int
handle_level(stonith_t *st, char *target, int fence_level,
             stonith_key_value_t *devices, bool added)
{
    char *node = NULL;
    char *pattern = NULL;
    char *name = NULL;
    char *value = strchr(target, '=');

    /* Determine if targeting by attribute, node name pattern or node name */
    if (value != NULL)  {
        name = target;
        *value++ = '\0';
    } else if (*target == '@') {
        pattern = target + 1;
    } else {
        node = target;
    }

    /* Register or unregister level as appropriate */
    if (added) {
        return st->cmds->register_level_full(st, st_opts, node, pattern,
                                             name, value, fence_level,
                                             devices);
    }
    return st->cmds->remove_level_full(st, st_opts, node, pattern,
                                       name, value, fence_level);
}

static char *
fence_action_str(const char *action)
{
    char *str = NULL;

    if (action == NULL) {
        str = strdup("unknown");
    } else if (action[0] == 'o') { // on, off
        str = crm_concat("turn", action, ' ');
    } else {
        str = strdup(action);
    }
    return str;
}

static void
print_fence_event(stonith_history_t *event)
{
    char *action_s = fence_action_str(event->action);
    time_t complete = event->completed;

    printf("%s was able to %s node %s on behalf of %s from %s at %s\n",
           (event->delegate? event->delegate : "This node"), action_s,
           event->target, event->client, event->origin, ctime(&complete));
    free(action_s);
}

static int
show_history(stonith_t *st, const char *target, int timeout, int quiet,
             int verbose)
{
    stonith_history_t *history, *hp, *latest = NULL;
    int rc = 0;

    rc = st->cmds->history(st, st_opts,
                           (safe_str_eq(target, "*")? NULL : target),
                           &history, timeout);
    for (hp = history; hp; hp = hp->next) {
        char *action_s = NULL;
        time_t complete = hp->completed;

        if (hp->state == st_done) {
            latest = hp;
        }

        if (quiet || !verbose) {
            continue;
        }

        if (hp->state == st_failed) {
            action_s = fence_action_str(hp->action);
            printf("%s failed to %s node %s on behalf of %s from %s at %s\n",
                   hp->delegate ? hp->delegate : "We", action_s, hp->target,
                   hp->client, hp->origin, ctime(&complete));

        } else if (hp->state == st_done) {
            print_fence_event(latest);

        } else {
            /* ocf:pacemaker:controld depends on "wishes to" being
             * in this output, when used with older versions of DLM
             * that don't report stateful_merge_wait
             */
            action_s = fence_action_str(hp->action);
            printf("%s at %s wishes to %s node %s - %d %d\n",
                   hp->client, hp->origin, action_s, hp->target, hp->state, hp->completed);
        }

        free(action_s);
    }

    if (latest) {
        if (quiet) {
            printf("%d\n", latest->completed);
        } else if (!verbose) { // already printed if verbose
            print_fence_event(latest);
        }
    }
    return rc;
}

int
main(int argc, char **argv)
{
    int flag;
    int rc = 0;
    int quiet = 0;
    int verbose = 0;
    int argerr = 0;
    int timeout = 120;
    int option_index = 0;
    int fence_level = 0;
    int no_connect = 0;
    int tolerance = 0;
    int as_nodeid = FALSE;

    char *name = NULL;
    char *value = NULL;
    char *target = NULL;
    char *lists = NULL;
    const char *agent = NULL;
    const char *device = NULL;
    const char *longname = NULL;

    char action = 0;
    stonith_t *st = NULL;
    stonith_key_value_t *params = NULL;
    stonith_key_value_t *devices = NULL;
    stonith_key_value_t *dIter = NULL;

    crm_log_cli_init("stonith_admin");
    crm_set_options(NULL, "<command> [<options>]", long_options,
                    "access the Pacemaker fencing API");

    async_fence_data.name = strdup(crm_system_name);

    while (1) {
        flag = crm_get_option_long(argc, argv, &option_index, &longname);
        if (flag == -1)
            break;

        switch (flag) {
            case 'V':
                verbose = 1;
                crm_bump_log_level(argc, argv);
                break;
            case '$':
            case '?':
                crm_help(flag, EX_OK);
                break;
            case 'I':
                no_connect = 1;
                /* fall through */
            case 'L':
                action = flag;
                break;
            case 'q':
                quiet = 1;
                break;
            case 'Q':
            case 'R':
            case 'D':
            case 's':
                action = flag;
                device = optarg;
                break;
            case 'T':
                free(async_fence_data.name);
                async_fence_data.name = crm_strdup_printf("%s.%s", crm_system_name, optarg);
                break;
            case 'a':
                agent = optarg;
                break;
            case 'l':
                target = optarg;
                action = 'L';
                break;
            case 'M':
                no_connect = 1;
                action = flag;
                break;
            case 't':
                timeout = crm_atoi(optarg, NULL);
                break;
            case 'B':
            case 'F':
            case 'U':
                /* using mainloop here */
                no_connect = 1;
                /* fall through */
            case 'C':
                /* Always log the input arguments */
                crm_log_args(argc, argv);
                target = optarg;
                action = flag;
                break;
            case 'n':
                as_nodeid = TRUE;
                break;
            case 'h':
            case 'H':
            case 'r':
            case 'd':
                target = optarg;
                action = flag;
                break;
            case 'i':
                fence_level = crm_atoi(optarg, NULL);
                break;
            case 'v':
                devices = stonith_key_value_add(devices, NULL, optarg);
                break;
            case 'o':
                crm_info("Scanning: -o %s", optarg);
                rc = sscanf(optarg, "%m[^=]=%m[^=]", &name, &value);
                if (rc != 2) {
                    crm_err("Invalid option: -o %s", optarg);
                    ++argerr;
                } else {
                    crm_info("Got: '%s'='%s'", name, value);
                    params = stonith_key_value_add(params, name, value);
                }
                free(value); value = NULL;
                free(name); name = NULL;
                break;
            case 'e':
                {
                    char *key = crm_concat("OCF_RESKEY", optarg, '_');
                    const char *env = getenv(key);

                    if (env == NULL) {
                        crm_err("Invalid option: -e %s", optarg);
                        ++argerr;
                    } else {
                        crm_info("Got: '%s'='%s'", optarg, env);
                        params = stonith_key_value_add(params, optarg, env);
                    }
                    free(key);
                }
                break;
            case 0:
                if (safe_str_eq("tolerance", longname)) {
                    tolerance = crm_get_msec(optarg) / 1000;    /* Send in seconds */
                }
                break;
            default:
                ++argerr;
                break;
        }
    }

    if (optind > argc) {
        ++argerr;
    }

    if (argerr) {
        crm_help('?', EX_USAGE);
    }

    st = stonith_api_new();

    if (!no_connect) {
        rc = st->cmds->connect(st, async_fence_data.name, NULL);
        if (rc < 0) {
            goto done;
        }
    }

    switch (action) {
        case 'I':
            rc = st->cmds->list_agents(st, st_opt_sync_call, NULL, &devices, timeout);
            for (dIter = devices; dIter; dIter = dIter->next) {
                fprintf(stdout, " %s\n", dIter->value);
            }
            if (rc == 0) {
                fprintf(stderr, "No devices found\n");

            } else if (rc > 0) {
                fprintf(stderr, "%d devices found\n", rc);
                rc = 0;
            }
            stonith_key_value_freeall(devices, 1, 1);
            break;
        case 'L':
            rc = st->cmds->query(st, st_opts, target, &devices, timeout);
            for (dIter = devices; dIter; dIter = dIter->next) {
                fprintf(stdout, " %s\n", dIter->value);
            }
            if (rc == 0) {
                fprintf(stderr, "No devices found\n");
            } else if (rc > 0) {
                fprintf(stderr, "%d devices found\n", rc);
                rc = 0;
            }
            stonith_key_value_freeall(devices, 1, 1);
            break;
        case 'Q':
            rc = st->cmds->monitor(st, st_opts, device, timeout);
            if (rc < 0) {
                rc = st->cmds->list(st, st_opts, device, NULL, timeout);
            }
            break;
        case 's':
            rc = st->cmds->list(st, st_opts, device, &lists, timeout);
            if (rc == 0) {
                if (lists) {
                    char *source = lists, *dest = lists; 

                    while (*dest) {
                        if ((*dest == '\\') && (*(dest+1) == 'n')) {
                            *source = '\n';
                            dest++;
                            dest++;
                            source++;
                        } else if ((*dest == ',') || (*dest == ';')) {
                            dest++;
                        } else {
                            *source = *dest;
                            dest++;
                            source++;
                        }

                        if (!(*dest)) {
                            *source = 0;
                        }
                    }
                    fprintf(stdout, "%s", lists);
                    free(lists);
                }
            } else {
                fprintf(stderr, "List command returned error. rc : %d\n", rc);
            }
            break;
        case 'R':
            rc = st->cmds->register_device(st, st_opts, device, "stonith-ng", agent, params);
            break;
        case 'D':
            rc = st->cmds->remove_device(st, st_opts, device);
            break;
        case 'd':
        case 'r':
            rc = handle_level(st, target, fence_level, devices, action == 'r');
            break;
        case 'M':
            if (agent == NULL) {
                printf("Please specify an agent to query using -a,--agent [value]\n");
                return -1;
            } else {
                char *buffer = NULL;

                rc = st->cmds->metadata(st, st_opt_sync_call, agent, NULL, &buffer, timeout);
                if (rc == pcmk_ok) {
                    printf("%s\n", buffer);
                }
                free(buffer);
            }
            break;
        case 'C':
            rc = st->cmds->confirm(st, st_opts, target);
            break;
        case 'B':
            rc = mainloop_fencing(st, target, "reboot", timeout, tolerance);
            break;
        case 'F':
            rc = mainloop_fencing(st, target, "off", timeout, tolerance);
            break;
        case 'U':
            rc = mainloop_fencing(st, target, "on", timeout, tolerance);
            break;
        case 'h':
            {
                time_t when = 0;

                if(as_nodeid) {
                    uint32_t nodeid = atol(target);
                    when = stonith_api_time(nodeid, NULL, FALSE);
                } else {
                    when = stonith_api_time(0, target, FALSE);
                }
                if(when) {
                    printf("Node %s last kicked at: %s\n", target, ctime(&when));
                } else {
                    printf("Node %s has never been kicked\n", target);
                }
            }
            break;
        case 'H':
            rc = show_history(st, target, timeout, quiet, verbose);
            break;
    }

  done:
    free(async_fence_data.name);
    crm_info("Command returned: %s (%d)", pcmk_strerror(rc), rc);

    stonith_key_value_freeall(params, 1, 1);
    st->cmds->disconnect(st);
    stonith_api_delete(st);

    return rc;
}
