/* dummy-ups.c - NUT simulation and device repeater driver Copyright (C) 2005 - 2010 Arnaud Quette 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 02111-1307 USA */ /* TODO list: * - separate the code between dummy and repeater/meta * - for repeater/meta: * * add support for instant commands and setvar * - for dummy: * * variable/value enforcement using cmdvartab for testing * the variable existance, and possible values * * allow variable creation on the fly (using upsrw) * * poll the "port" file for change */ #include #include #include #include #include "main.h" #include "parseconf.h" #include "upsclient.h" #include "dummy-ups.h" #define DRIVER_NAME "Device simulation and repeater driver" #define DRIVER_VERSION "0.13" /* driver description structure */ upsdrv_info_t upsdrv_info = { DRIVER_NAME, DRIVER_VERSION, "Arnaud Quette ", DRV_STABLE, { NULL } }; #define MODE_NONE 0 #define MODE_DUMMY 1 /* use the embedded defintion or a definition file */ #define MODE_REPEATER 2 /* use libupsclient to repeat an UPS */ #define MODE_META 3 /* consolidate data from several UPSs (TBS) */ int mode=MODE_NONE; /* parseconf context, for dummy mode using a file */ PCONF_CTX_t *ctx=NULL; time_t next_update = -1; #define MAX_STRING_SIZE 128 static int setvar(const char *varname, const char *val); static int instcmd(const char *cmdname, const char *extra); static int parse_data_file(int upsfd); static dummy_info_t *find_info(const char *varname); static int is_valid_data(const char* varname); static int is_valid_value(const char* varname, const char *value); /* libupsclient update */ static int upsclient_update_vars(void); /* connection information */ static char *client_upsname = NULL, *hostname = NULL; static UPSCONN_t *ups = NULL; static int port; /* Driver functions */ void upsdrv_initinfo(void) { dummy_info_t *item; switch (mode) { case MODE_DUMMY: /* Initialise basic essential variables */ for ( item = nut_data ; item->info_type != NULL ; item++ ) { if (item->drv_flags & DU_FLAG_INIT) { dstate_setinfo(item->info_type, "%s", item->default_value); dstate_setflags(item->info_type, item->info_flags); /* Set max length for strings, if needed */ if (item->info_flags & ST_FLAG_STRING) dstate_setaux(item->info_type, item->info_len); } } /* Now get user's defined variables */ if (parse_data_file(upsfd) < 0) upslogx(LOG_NOTICE, "Unable to parse the definition file %s", device_path); /* Initialize handler */ upsh.setvar = setvar; dstate_dataok(); break; case MODE_META: case MODE_REPEATER: /* Obtain the target name */ if (upscli_splitname(device_path, &client_upsname, &hostname, &port) != 0) { fatalx(EXIT_FAILURE, "Error: invalid UPS definition.\nRequired format: upsname[@hostname[:port]]"); } /* Connect to the target */ ups = xmalloc(sizeof(*ups)); if (upscli_connect(ups, hostname, port, UPSCLI_CONN_TRYSSL) < 0) { fatalx(EXIT_FAILURE, "Error: %s", upscli_strerror(ups)); } else { upsdebugx(1, "Connected to %s@%s", client_upsname, hostname); } if (upsclient_update_vars() < 0) { /* check for an old upsd */ if (upscli_upserror(ups) == UPSCLI_ERR_UNKCOMMAND) { fatalx(EXIT_FAILURE, "Error: upsd is too old to support this query"); } fatalx(EXIT_FAILURE, "Error: %s", upscli_strerror(ups)); } /* FIXME: commands and settable variable! */ break; default: case MODE_NONE: fatalx(EXIT_FAILURE, "no suitable definition found!"); break; } upsh.instcmd = instcmd; dstate_addcmd("load.off"); } void upsdrv_updateinfo(void) { upsdebugx(1, "upsdrv_updateinfo..."); sleep(1); switch (mode) { case MODE_DUMMY: /* Now get user's defined variables */ if (parse_data_file(upsfd) >= 0) dstate_dataok(); break; case MODE_META: case MODE_REPEATER: if (upsclient_update_vars() > 0) { dstate_dataok(); } else { /* try to reconnect */ upscli_disconnect(ups); if (upscli_connect(ups, hostname, port, UPSCLI_CONN_TRYSSL) < 0) { upsdebugx(1, "Error reconnecting: %s", upscli_strerror(ups)); } else { upsdebugx(1, "Reconnected"); } } break; case MODE_NONE: default: break; } } void upsdrv_shutdown(void) { fatalx(EXIT_FAILURE, "shutdown not supported"); } static int instcmd(const char *cmdname, const char *extra) { /* if (!strcasecmp(cmdname, "test.battery.stop")) { ser_send_buf(upsfd, ...); return STAT_INSTCMD_HANDLED; } */ /* FIXME: the below is only valid if (mode == MODE_DUMMY) * if (mode == MODE_REPEATER) => forward * if (mode == MODE_META) => ? */ upslogx(LOG_NOTICE, "instcmd: unknown command [%s]", cmdname); return STAT_INSTCMD_UNKNOWN; } void upsdrv_help(void) { } void upsdrv_makevartable(void) { } void upsdrv_initups(void) { /* check the running mode... */ if (strchr(device_path, '@')) { upsdebugx(1, "Repeater mode"); mode = MODE_REPEATER; dstate_setinfo("driver.parameter.mode", "repeater"); /* FIXME: if there is at least one more => MODE_META... */ } else { upsdebugx(1, "Dummy (simulation) mode"); mode = MODE_DUMMY; dstate_setinfo("driver.parameter.mode", "dummy"); } } void upsdrv_cleanup(void) { if ( (mode == MODE_META) || (mode == MODE_REPEATER) ) { if (ups) { upscli_disconnect(ups); } if (ctx) { pconf_finish(ctx); free(ctx); } free(client_upsname); free(hostname); free(ups); } } static int setvar(const char *varname, const char *val) { dummy_info_t *item; upsdebugx(2, "entering setvar(%s, %s)", varname, val); /* FIXME: the below is only valid if (mode == MODE_DUMMY) * if (mode == MODE_REPEATER) => forward * if (mode == MODE_META) => ? */ if (!strncmp(varname, "ups.status", 10)) { status_init(); /* FIXME: split and check values (support multiple values), à la usbhid-ups */ status_set(val); status_commit(); return STAT_SET_HANDLED; } /* Check variable validity */ if (!is_valid_data(varname)) { upsdebugx(2, "setvar: invalid variable name (%s)", varname); return STAT_SET_UNKNOWN; } /* Check value validity */ if (!is_valid_value(varname, val)) { upsdebugx(2, "setvar: invalid value (%s) for variable (%s)", val, varname); return STAT_SET_UNKNOWN; } /* If value is empty, remove the variable (FIXME: do we need * a magic word?) */ if (strlen(val) == 0) { dstate_delinfo(varname); } else { dstate_setinfo(varname, "%s", val); if ( (item = find_info(varname)) != NULL) { dstate_setflags(item->info_type, item->info_flags); /* Set max length for strings, if needed */ if (item->info_flags & ST_FLAG_STRING) dstate_setaux(item->info_type, item->info_len); } } return STAT_SET_HANDLED; } /*************************************************/ /* Support functions */ /*************************************************/ static int upsclient_update_vars(void) { int ret; unsigned int numq, numa; const char *query[4]; char **answer; query[0] = "VAR"; query[1] = client_upsname; numq = 2; ret = upscli_list_start(ups, numq, query); if (ret < 0) { upsdebugx(1, "Error: %s (%i)", upscli_strerror(ups), upscli_upserror(ups)); return ret; } while (upscli_list_next(ups, numq, query, &numa, &answer) == 1) { /* VAR */ if (numa < 4) { upsdebugx(1, "Error: insufficient data (got %d args, need at least 4)", numa); } upsdebugx(5, "Received: %s %s %s %s", answer[0], answer[1], answer[2], answer[3]); /* do not override the driver collection */ if (strncmp(answer[2], "driver.", 7)) setvar(answer[2], answer[3]); } return 1; } /* find info element definition in info array */ static dummy_info_t *find_info(const char *varname) { dummy_info_t *item; for ( item = nut_data ; item->info_type != NULL ; item++ ) { if (!strcasecmp(item->info_type, varname)) return item; } upsdebugx(2, "find_info: unknown variable: %s\n", varname); return NULL; } /* check if data exists in our data table */ static int is_valid_data(const char* varname) { dummy_info_t *item; if ( (item = find_info(varname)) != NULL) { return 1; } /* FIXME: we need to have the full data set before * enforcing controls! We also need a way to automate * the update / sync process (with cmdvartab?!) */ upsdebugx(1, "Unknown data. Committing anyway..."); return 1; /* return 0;*/ } /* check if data's value validity */ static int is_valid_value(const char* varname, const char *value) { dummy_info_t *item; if ( (item = find_info(varname)) != NULL) { /* FIXME: test enum or bound against value */ return 1; } /* FIXME: we need to have the full data set before * enforcing controls! We also need a way to automate * the update / sync process (with cmdvartab?) */ upsdebugx(1, "Unknown data. Committing value anyway..."); return 1; /* return 0;*/ } /* called for fatal errors in parseconf like malloc failures */ static void upsconf_err(const char *errmsg) { upslogx(LOG_ERR, "Fatal error in parseconf(ups.conf): %s", errmsg); } /* for dummy mode * parse the definition file and process its content */ static int parse_data_file(int upsfd) { char fn[SMALLBUF]; char *ptr, var_value[MAX_STRING_SIZE]; int value_args = 0, counter; time_t now; time(&now); upsdebugx(1, "entering parse_data_file()"); if (now < next_update) { upsdebugx(1, "leaving (paused)..."); return 1; } /* initialise everything, to loop back at the beginning of the file */ if (ctx == NULL) { ctx = (PCONF_CTX_t *)xmalloc(sizeof(PCONF_CTX_t)); if (device_path[0] == '/') snprintf(fn, sizeof(fn), "%s", device_path); else snprintf(fn, sizeof(fn), "%s/%s", confpath(), device_path); pconf_init(ctx, upsconf_err); if (!pconf_file_begin(ctx, fn)) fatalx(EXIT_FAILURE, "Can't open dummy-ups definition file %s: %s", fn, ctx->errmsg); } /* Reset the next call time, so that we can loop back on the file * if there is no blocking action (ie TIMER) until the end of the file */ next_update = -1; /* Now start or continue parsing... */ while (pconf_file_next(ctx)) { if (pconf_parse_error(ctx)) { upsdebugx(2, "Parse error: %s:%d: %s", fn, ctx->linenum, ctx->errmsg); continue; } /* Check if we have something to process */ if (ctx->numargs < 1) continue; /* Process actions (only "TIMER" ATM) */ if (!strncmp(ctx->arglist[0], "TIMER", 5)) { /* TIMER will wait "seconds" before * continuing the parsing */ int delay = atoi (ctx->arglist[1]); time(&next_update); next_update += delay; upsdebugx(1, "suspending execution for %i seconds...", delay); break; } /* Remove ":" suffix, after the variable name */ if ((ptr = strchr(ctx->arglist[0], ':')) != NULL) *ptr = '\0'; upsdebugx(3, "parse_data_file: variable \"%s\" with %d args", ctx->arglist[0], (int)ctx->numargs); /* Skip the driver.* collection data */ if (!strncmp(ctx->arglist[0], "driver.", 7)) { upsdebugx(2, "parse_data_file: skipping %s", ctx->arglist[0]); continue; } /* From there, we get varname in arg[0], and values in other arg[1...x] */ /* special handler for status */ if (!strncmp( ctx->arglist[0], "ups.status", 10)) { status_init(); for (counter = 1, value_args = ctx->numargs ; counter < value_args ; counter++) { status_set(ctx->arglist[counter]); } status_commit(); } else { for (counter = 1, value_args = ctx->numargs ; counter < value_args ; counter++) { if (counter == 1) /* don't append the first space separator */ snprintf(var_value, sizeof(var_value), "%s", ctx->arglist[counter]); else snprintfcat(var_value, sizeof(var_value), " %s", ctx->arglist[counter]); } if (setvar(ctx->arglist[0], var_value) == STAT_SET_UNKNOWN) { upsdebugx(2, "parse_data_file: can't add \"%s\" with value \"%s\"\nError: %s", ctx->arglist[0], var_value, ctx->errmsg); } else { upsdebugx(3, "parse_data_file: added \"%s\" with value \"%s\"", ctx->arglist[0], var_value); } } } /* Cleanup parseconf if there is no pending action */ if (next_update == -1) { pconf_finish(ctx); free(ctx); ctx=NULL; } return 1; }