/* $Id: cyclades.c,v 1.1.2.8 2005/05/20 19:43:33 blaschke Exp $ */
/*
 * Stonith module for Cyclades AlterPath PM
 * Bases off the SSH plugin
 *
 * Copyright (c) 2004 Cyclades corp.
 *
 * Author: Jon Taylor <jon.taylor@cyclades.com>
 *
 * Rewritten from scratch using baytech.c structure and code 
 * and currently maintained by
 *       Marcelo Tosatti  <marcelo.tosatti@cyclades.com>
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 * 
 * This library 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
 * Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 *
 */

#include <portability.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <errno.h>
#include <syslog.h>
#include <libintl.h>
#include <sys/wait.h>
#include <glib.h>

#include <stonith/stonith.h>
#include "stonith_signal.h"

#define PIL_PLUGINTYPE          STONITH_TYPE
#define PIL_PLUGINTYPE_S        STONITH_TYPE_S
#define PIL_PLUGIN              cyclades 
#define PIL_PLUGIN_S            "cyclades"
#define PIL_PLUGINLICENSE 	LICENSE_LGPL
#define PIL_PLUGINLICENSEURL 	URL_LGPL
#include <pils/plugin.h>

/*
 * cycladesclose is called as part of unloading the cyclades STONITH plugin.
 * If there was any global data allocated, or file descriptors opened, etc.
 * which is associated with the plugin, and not a single interface
 * in particular, here's our chance to clean it up.
 */

static void
cycladesclosepi(PILPlugin*pi)
{
}


/*
 * cycladescloseintf called as part of shutting down the cyclades STONITH
 * interface.  If there was any global data allocated, or file descriptors
 * opened, etc.  which is associated with the ssh implementation,
 * here's our chance to clean it up.
 */
static PIL_rc
cycladescloseintf(PILInterface* pi, void* pd)
{
	return PIL_OK;
}

static void *		cyclades_new(void);
static void		cyclades_destroy(Stonith *);
static int		cyclades_set_config_file(Stonith *, const char * cfgname);
static int		cyclades_set_config_info(Stonith *, const char * info);
static const char *	cyclades_getinfo(Stonith * s, int InfoType);
static int		cyclades_status(Stonith * );
static int		cyclades_reset_req(Stonith * s, int request, const char * host);
static char **		cyclades_hostlist(Stonith  *);
static void		cyclades_free_hostlist(char **);



static struct stonith_ops cycladesOps ={
	cyclades_new,			/* Create new STONITH object	*/
	cyclades_destroy,		/* Destroy STONITH object	*/
	cyclades_set_config_file,	/* set configuration from file	*/
	cyclades_set_config_info,	/* Get configuration from file	*/
	cyclades_getinfo,		/* Return STONITH info string	*/
	cyclades_status,		/* Return STONITH device status	*/
	cyclades_reset_req,		/* Request a reset */
	cyclades_hostlist,		/* Return list of supported hosts */
	cyclades_free_hostlist		/* free above list */
};

PIL_PLUGIN_BOILERPLATE("1.0", Debug, cycladesclosepi);
static const PILPluginImports*  PluginImports;
static PILPlugin*               OurPlugin;
static PILInterface*		OurInterface;
static StonithImports*		OurImports;
static void*			interfprivate;

#define LOG		PluginImports->log
#define MALLOC		PluginImports->alloc
#define STRDUP  	PluginImports->mstrdup
#define FREE		PluginImports->mfree
#define EXPECT_TOK	OurImports->ExpectToken
#define STARTPROC	OurImports->StartProcess

PIL_rc
PIL_PLUGIN_INIT(PILPlugin*us, const PILPluginImports* imports);

PIL_rc
PIL_PLUGIN_INIT(PILPlugin*us, const PILPluginImports* imports)
{
	/* Force the compiler to do a little type checking */
	(void)(PILPluginInitFun)PIL_PLUGIN_INIT;

	PluginImports = imports;
	OurPlugin = us;

	/* Register ourself as a plugin */
	imports->register_plugin(us, &OurPIExports);  

	/*  Register our interface implementation */
 	return imports->register_interface(us, PIL_PLUGINTYPE_S
	,	PIL_PLUGIN_S
	,	&cycladesOps
	,	cycladescloseintf		/*close */
	,	&OurInterface
	,	(void*)&OurImports
	,	&interfprivate); 
}

#define	DEVICE	"Cyclades PM"
#define WHITESPACE	" \t\n\r\f"

/*
 *    Cyclades STONITH device
 *
 */

struct cycladesDevice {
	const char *	cycladesid;
	char *		device;
	char *		user;

	int config;
	int serial_port;

	/* pid of ssh client process and its in/out file descriptors */
	pid_t		pid; 
	int 		rdfd, wrfd;		
};

static struct Etoken StatusOutput[] = { 
	{ "Outlet\t\tName\t\tStatus\t\tUsers\t\tInterval (s)", 0, 0},
	{ NULL, 0, 0} 
};

static struct Etoken CRNL[] =           { {"\n",0,0},{"\r\n",0,0},{NULL,0,0}};


/* Commands of PM devices */
static char status_all[] = "status all";
static char cycle[] = "cycle";

static int CYC_robust_cmd(struct cycladesDevice *, char *);
static void CYCkillcomm(struct cycladesDevice *);
static int CYCScanLine(struct cycladesDevice*, int, char *, int);

static const char * cycladesid = "CycladesDevice-Stonith";
static const char * NOTcycladesID = "Cyclades device has been destroyed";

#define MAX_OUTLETS	128

#define	ISCYCLADESDEV(i)	(((i)!= NULL && (i)->pinfo != NULL)	\
	&& ((struct cycladesDevice *)(i->pinfo))->cycladesid == cycladesid)


#ifndef MALLOCT
#	define     MALLOCT(t)      ((t *)(MALLOC(sizeof(t)))) 
#endif

#define N_(text)	(text)
#define _(text)		dgettext(ST_TEXTDOMAIN, text)

#define EXPECT(p,t,b,l)     {						\
                        	if (LookFor(sd, p, t, b, l) < 0)	\
                                	return(errno == ETIMEDOUT	\
                        	?       S_TIMEOUT : S_OOPS);		\
                        }

#define NULLEXPECT(p,t,b,l) {						\
                                if (LookFor(sd, p, t, b, l) < 0)	\
                                        return(NULL);			\
                        }

#define ZEROEXPECT(p,t,b,l) {						\
                                if (LookFor(sd, p, t, b, l) < 0)	\
                                        return(0);			\
                        }

#define RESETEXPECT(p,t,b,l)     {					\
                        	if (LookFor(sd, p, t, b, l) < 0) {      \
					FREE(outletstr);		\
	                                return(errno == ETIMEDOUT       \
        	                ?       S_RESETFAIL : S_OOPS);          \
				}					\
                        }

static int
CYCScanLine(struct cycladesDevice* sd, int timeout, char * buf, int max)
{
        if (EXPECT_TOK(sd->rdfd, CRNL, timeout, buf, max) < 0) {
                CYCkillcomm(sd);
                sd->pid = -1;
                return(S_OOPS);
        }
        return(S_OK);
}


#define MAXSAVE 512
static int
LookFor(struct cycladesDevice* sd, struct Etoken * tlist, int timeout, char *buf,
		int max)
{
        int     rc;
                                                                                     
        if ((rc = EXPECT_TOK(sd->rdfd, tlist, timeout, buf, max)) < 0){
		if (tlist == StatusOutput) {
			/* if status all returns nothing, unknown device */
			syslog(LOG_ERR, _("Unknown " DEVICE " device %s")
			,	sd->device);
		}else{
	                syslog(LOG_ERR, _("Did not find string: '%s' from "
				DEVICE "."), tlist[0].string);
                	syslog(LOG_ERR, _("Got '%s' from " DEVICE " instead.")
	                ,       buf);
		}
                CYCkillcomm(sd);
                return(-1);
        }
#if 0
        syslog(LOG_INFO
        ,       "Cyclades: Expecting [%s] Received [%s]"
        ,       tlist[0].string
        ,       buf);
#endif
        return(rc);
}

static int
cyclades_status(Stonith  *s)
{
	struct cycladesDevice *sd = s->pinfo;
	char *cmd = status_all;
	char    savebuf[MAXSAVE];
        savebuf[MAXSAVE-1] = EOS;
        savebuf[0] = EOS;

	if (!ISCYCLADESDEV(s)) {
		syslog(LOG_ERR, "invalid argument to cyclades_status");
		return(S_OOPS);
	}

	if (CYC_robust_cmd(sd, cmd) != S_OK) {
		syslog(LOG_ERR, "can't run status all command");
		return(S_OOPS);
	}

	EXPECT(StatusOutput, 50, savebuf, sizeof(savebuf));

	return (S_OK);
}

static void CYCkillcomm(struct cycladesDevice *sd)
{
	if (sd->pid > 0) {
		STONITH_KILL(sd->pid, SIGKILL);
                (void)waitpid(sd->pid, NULL, 0);
                sd->pid = -1;
	}
}

static int CYC_run_command(struct cycladesDevice *sd, char *cmd)
{
	char	SshCommand[MAX_OUTLETS*4];

	snprintf(SshCommand, sizeof(SshCommand),
			"exec ssh -q %s@%s /bin/pmCommand %d %s 2>/dev/null", 
			sd->user, sd->device, sd->serial_port, cmd);

	sd->pid = STARTPROC(SshCommand, &sd->rdfd, &sd->wrfd);

	if (sd->pid <= 0)
		return(S_OOPS);

	return(S_OK);
}

static int 
CYC_robust_cmd(struct cycladesDevice *sd, char *cmd)
{
	int rc = S_OOPS;
	int i;

	for (i=0; i < 20 && rc != S_OK; i++) {

		if (sd->pid > 0)
			CYCkillcomm(sd);

		if (CYC_run_command(sd, cmd) != S_OK) {
			CYCkillcomm(sd);
			continue;
		} 
		rc = S_OK;
	}

	return rc;
}

static int CYCNametoOutlet(struct cycladesDevice *sd, const char *host, int *outlets, int maxoutlet)
{
	char *cmd = status_all;
	char	savebuf[MAXSAVE];
	int err;
	int outlet, numoutlet = 0;
	char name[10], locked[10], on[4];

	if (CYC_robust_cmd(sd, cmd) != S_OK) {
		syslog(LOG_ERR, "can't run status all command");
		return 0;
	}

	ZEROEXPECT(StatusOutput, 50, savebuf, sizeof(savebuf));

	ZEROEXPECT(CRNL, 50, savebuf, sizeof(savebuf));

	do {

		memset(savebuf, 0, sizeof(savebuf));
		memset(name, 0, sizeof(name));
		memset(locked, 0, sizeof(locked));
		memset(on, 0, sizeof(on));

		err = CYCScanLine(sd, 2, savebuf, sizeof(savebuf));

		if ((err == S_OK) &&
		    (sscanf(savebuf,"%3d %10s %10s %3s", &outlet, 
			name, locked, on) > 0)) {
			g_strdown(name);
			if (strstr(locked, "ocked") && !strcmp(name, host)) {
				if (numoutlet >= maxoutlet) {
					syslog(LOG_ERR, "too many outlets");
					return 0;
				}
				outlets[numoutlet++] = outlet;
			}
		}

	} while (err == S_OK);

	return numoutlet;
}


/*
 *	Return the list of hosts configured for this Cyclades device
 */

static char **
cyclades_hostlist(Stonith  *s)
{
	struct cycladesDevice*	sd;
	char *cmd = status_all;
	char    savebuf[MAXSAVE];
	int err, i;
	int outlet;
	int numnames = 0;
	char name[10], locked[10], on[4];
	char *NameList[MAX_OUTLETS];
	char **ret = NULL;

	if (!ISCYCLADESDEV(s)) {
		syslog(LOG_ERR, "invalid argument to cyclades_hostlist");
		return(NULL);
	}

	sd = (struct cycladesDevice*) s->pinfo;

	if (CYC_robust_cmd(sd, cmd) != S_OK) {
		syslog(LOG_ERR, "can't run status all command");
		return (NULL);
	}

	memset(savebuf, 0, sizeof(savebuf));

	NULLEXPECT(StatusOutput, 50, savebuf, sizeof(savebuf));

	NULLEXPECT(CRNL, 50, savebuf, sizeof(savebuf));

	do {
		char *nm;

		memset(savebuf, 0, sizeof(savebuf));
		memset(name, 0, sizeof(name));
		memset(locked, 0, sizeof(locked));
		memset(on, 0, sizeof(on));

		err = CYCScanLine(sd, 2, savebuf, sizeof(savebuf));

		if ((err == S_OK) &&
		    (sscanf(savebuf,"%3d %10s %10s %3s", &outlet, 
			name, locked, on) > 0)) {
			if (strstr(locked, "ocked")) {
				nm = (char *) STRDUP (name);
				if (!nm) {
					syslog(LOG_ERR, "out of memory");
					goto out_of_memory;
				}
				g_strdown(nm);
				NameList[numnames] = nm;
				numnames++;
				NameList[numnames] = NULL;
			}
		}

	} while (err == S_OK);

	if (numnames) {

		ret = (char **)MALLOC((numnames+1)*sizeof(char*));
		if (ret == NULL) {
			syslog(LOG_ERR, "out of memory");
			goto out_of_memory;
		} else {
			memcpy(ret, NameList, (numnames+1)*sizeof(char*));
		}
		return (ret);
	}

out_of_memory:
	for (i=0; i<numnames; i++)
		free(NameList[i]);

	return (NULL);
}

static void
cyclades_free_hostlist (char ** hlist)
{
	char ** hl = hlist;
	if (hl == NULL) {
		return;
	}

	while (*hl) {
		FREE(*hl);
		*hl = NULL;
		++hl;
	}

	FREE(hlist);
	hlist = NULL;
}

/*
 *	Parse the config information, and stash it away...
 */

static int
cyclades_parse_config_info(struct cycladesDevice* sd, const char * info)
{
	static char dev[1024];
	static char user[1024];
	int serial_port;

	if (sd->config)
		return(S_OOPS);

	/* sscanf is dangerous ? */

	if (sscanf(info, "%s %s %d", dev, user, &serial_port) == 3) {

		if ((sd->device = STRDUP(dev)) == NULL) {
			syslog(LOG_ERR, "out of memory");
			return (S_OOPS);
		}

		if ((sd->user = STRDUP(user)) == NULL) {
			FREE(sd->device);
			sd->device = NULL;
			syslog(LOG_ERR, "out of memory");
			return (S_OOPS);
		}

		sd->serial_port = serial_port;
				
		sd->config = 1;

  		return(S_OK);
	}
	return(S_BADCONFIG);
}


static char *cyclades_outletstr(int *outlet, int numoutlet)
{
	int i, len;
	char *ret;

	/* maximum length per outlet is currently four (outlet is one to
	 * three digits, followed by either a comma or null), so add one
	 * for good measure */
	len = numoutlet * 5 * sizeof(char);
	if ((ret = MALLOC(len)) != NULL) {
		snprintf(ret, len, "%d", outlet[0]);
		for (i = 1; i < numoutlet; i++) {
			char buf[5];
			snprintf(buf, sizeof(buf), ",%d", outlet[i]);
			strcat(ret, buf);
		}
	}
	return(ret);
}


static int cyclades_onoff(struct cycladesDevice *sd, int *outlet, int numoutlet, const char *unitid, int req)
{
	char savebuf[MAXSAVE];
	const char * onoff;
	char cmd[MAX_OUTLETS*4], expstring[64];
	struct Etoken exp[] = {{NULL, 0, 0}, {NULL, 0, 0}};
	char *outletstr;
	int i;
	
	onoff = (req == ST_POWERON ? "on" : "off");

	memset(cmd, 0, sizeof(cmd));

	outletstr = cyclades_outletstr(outlet, numoutlet);
	if (outletstr == NULL) {
		syslog(LOG_ERR, "out of memory");
		return(S_OOPS);
	}
	snprintf(cmd, sizeof(cmd), "%s %s", onoff, outletstr);

	if (CYC_robust_cmd(sd, cmd) != S_OK) {
		syslog(LOG_ERR, "can't run %s command", onoff);
		FREE(outletstr);
		return(S_OOPS);
	}

	EXPECT(CRNL, 50, savebuf, sizeof(savebuf));

	for (i = 0; i < numoutlet; i++) {
		memset(expstring, 0, sizeof(expstring));
		snprintf(expstring, sizeof(expstring), "%d: Outlet turned %s."
		,	outlet[i], onoff);

		exp[0].string = expstring;

		/* FIXME: should handle "already powered on/off" case and 
		   inform to syslog */

		EXPECT(exp, 50, savebuf, sizeof(savebuf)); 
	}
	
	syslog(LOG_NOTICE, _("Power to host %s turned %s."), unitid, onoff);

	FREE(outletstr);
	return (S_OK);
}

static int cyclades_reset(struct cycladesDevice *sd, int *outlet, int numoutlet, const char *unitid)
{
	char savebuf[MAXSAVE];

	char cmd[MAX_OUTLETS*4], expstring[64];
	struct Etoken exp[] = {{NULL, 0, 0}, {NULL, 0, 0}};
	char *outletstr;
	int i;

	memset(cmd, 0, sizeof(cmd));

	outletstr = cyclades_outletstr(outlet, numoutlet);
	if (outletstr == NULL) {
		syslog(LOG_ERR, "out of memory");
		return(S_OOPS);
	}
	snprintf(cmd, sizeof(cmd), "%s %s", cycle, outletstr);

	syslog(LOG_INFO, _("Host %s being rebooted."), unitid);

	if (CYC_robust_cmd(sd, cmd) != S_OK) {
		syslog(LOG_ERR, "can't run cycle command");
		FREE(outletstr);
		return(S_OOPS);
	}

	RESETEXPECT(CRNL, 50, savebuf, sizeof(savebuf));

	for (i = 0; i < numoutlet; i++) {
		memset(expstring, 0, sizeof(expstring));
		snprintf(expstring, sizeof(expstring), "%d: Outlet turned off."
		,	outlet[i]);

		exp[0].string = expstring;
		RESETEXPECT(exp, 50, savebuf, sizeof(savebuf)); 
	}
	
	for (i = 0; i < numoutlet; i++) {
		memset(expstring, 0, sizeof(expstring));
		snprintf(expstring, sizeof(expstring), "%d: Outlet turned on."
		,	outlet[i]);

		exp[0].string = expstring;
		RESETEXPECT(exp, 50, savebuf, sizeof(savebuf)); 
	}

	FREE(outletstr);
	return (S_OK);
}

/*
 *	Reset the given host on this Stonith device.
 */
static int
cyclades_reset_req(Stonith * s, int request, const char * host)
{
	struct cycladesDevice *sd;
	int rc = 0;
	int numoutlet, outlets[MAX_OUTLETS];

	if (!ISCYCLADESDEV(s)) {
		syslog (LOG_ERR, "invalid argument to cyclades_reset");
		return (S_OOPS);
	}

	sd = (struct cycladesDevice *)s->pinfo;

	numoutlet = CYCNametoOutlet(sd, host, outlets, MAX_OUTLETS);

	if (!numoutlet) {
		syslog (LOG_ERR, "Unknown host %s to " DEVICE, host);
		return (S_OOPS);
	}

		
	switch (request) {
	case ST_POWERON:
	case ST_POWEROFF:
		rc = cyclades_onoff(sd, outlets, numoutlet, host, request);
		break;

	case ST_GENERIC_RESET:
		rc = cyclades_reset(sd, outlets, numoutlet, host);
		break;
	default:
		rc = S_INVAL;
		break;
	}

	return rc;
}

/*
 *	Parse the information in the given configuration file,
 *	and stash it away...
 */
static int
cyclades_set_config_file(Stonith* s, const char * configname)
{
	FILE * cfgfile;
	char line[256];
	struct cycladesDevice*	sd;

	if (!ISCYCLADESDEV(s)) {
		syslog(LOG_ERR, "invalid argument to cyclades_set_config_file");
		return(S_OOPS);
	}

	sd = (struct cycladesDevice*) s->pinfo;

	if ((cfgfile = fopen(configname, "r")) == NULL)  {
		syslog(LOG_ERR, "Cannot open %s", configname);
		return(S_BADCONFIG);
	}

	while (fgets(line, sizeof(line), cfgfile) != NULL){
		if (*line == '#' || *line == '\n' || *line == EOS) {
			continue;
		}
		return(cyclades_parse_config_info(sd, line));
  	}

	return(S_BADCONFIG);
}

/*
 *	Parse the config information in the given string, and stash it away...
 */
static int
cyclades_set_config_info(Stonith* s, const char * info)
{
	struct cycladesDevice* sd;

	if (!ISCYCLADESDEV(s)) {
		syslog(LOG_ERR, "%s: invalid argument", __FUNCTION__);
		return(S_OOPS);
	}

	sd = (struct cycladesDevice *)s->pinfo;

	return(cyclades_parse_config_info(sd, info));
}

static const char *
cyclades_getinfo(Stonith * s, int reqtype)
{
	struct cycladesDevice * sd;
	const char * ret;

	if (!ISCYCLADESDEV(s)) {
		syslog(LOG_ERR, "cyclades_getinfo: invalid argument");
		return NULL;
	}

	/*
	 *	We look in the ST_TEXTDOMAIN catalog for our messages
	 */

	sd = (struct cycladesDevice *)s->pinfo;

	switch (reqtype) {
	case ST_DEVICEID:
		/* FIXME: could inform the exact PM model */
		ret = _("Cyclades AlterPath PM");
		break;

	case ST_CONF_INFO_SYNTAX:
		ret = _("IP-address login serial-port\n"
		"The IP address, login and serial-port are white-space delimited.  "
		"login is the username on the TS/ACS (usually root) and "
		"serial-port indicates in which port the PM is connected to.");
		break;

	case ST_CONF_FILE_SYNTAX:
		 ret = _("IP-address login serial-port\n"
		"The IP-address, login and serial-port are white-space delimited.  "
		"All three items must be on one line. "
		"Blank lines and lines beginning with # are ignored");
		break;

	case ST_DEVICEDESCR:		/* Description of device type */
		ret = _("Cyclades AlterPath PM "
			"series power switches (via TS/ACS/KVM).");
		break;

	case ST_DEVICEURL:
		ret = "http://www.cyclades.com/";
		break;

	default:
		ret = NULL;
		break;
	}
	return ret;
}

/*
 *	Cyclades Stonith destructor...
 */
static void
cyclades_destroy(Stonith *s)
{
	struct cycladesDevice* sd;

	if (!ISCYCLADESDEV(s)) {
		syslog(LOG_ERR, "%s: invalid argument", __FUNCTION__);
		return;
	}

	sd = (struct cycladesDevice *)s->pinfo;

	sd->cycladesid = NOTcycladesID;
	CYCkillcomm(sd);
	if (sd->device != NULL) {
		FREE(sd->device);
		sd->device = NULL;
	}
	if (sd->user != NULL) {
		FREE(sd->user);
		sd->user = NULL;
	}

	FREE(sd);
}

/* Create a new cyclades Stonith device */
static void *
cyclades_new(void)
{
	struct cycladesDevice*	sd = MALLOCT(struct cycladesDevice);

	if (sd == NULL) {
		syslog(LOG_ERR, "out of memory");
		return(NULL);
	}

	memset(sd, 0, sizeof(*sd));
	sd->cycladesid = cycladesid;
	sd->rdfd = -1;
	sd->wrfd = -1;
	return((void *)sd);
}


syntax highlighted by Code2HTML, v. 0.9.1