This introduces a new set of APIs in src/util/command.h to use for invoking commands. This is intended to replace all current usage of virRun and virExec variants, with a more flexible and less error prone API. --- src/Makefile.am | 1 + src/util/command.c | 782 ++++++++++++++++++++++++++++++++++++++++++ src/util/command.h | 197 +++++++++++ tests/Makefile.am | 14 +- tests/commanddata/test1.log | 12 + tests/commanddata/test10.log | 14 + tests/commanddata/test11.log | 12 + tests/commanddata/test12.log | 12 + tests/commanddata/test13.log | 12 + tests/commanddata/test2.log | 14 + tests/commanddata/test3.log | 12 + tests/commanddata/test4.log | 10 + tests/commanddata/test5.log | 6 + tests/commanddata/test6.log | 11 + tests/commanddata/test7.log | 7 + tests/commanddata/test8.log | 14 + tests/commanddata/test9.log | 14 + tests/commandhelper.c | 112 ++++++ tests/commandtest.c | 494 ++++++++++++++++++++++++++ 19 files changed, 1749 insertions(+), 1 deletions(-) create mode 100644 src/util/command.c create mode 100644 src/util/command.h create mode 100644 tests/commanddata/test1.log create mode 100644 tests/commanddata/test10.log create mode 100644 tests/commanddata/test11.log create mode 100644 tests/commanddata/test12.log create mode 100644 tests/commanddata/test13.log create mode 100644 tests/commanddata/test2.log create mode 100644 tests/commanddata/test3.log create mode 100644 tests/commanddata/test4.log create mode 100644 tests/commanddata/test5.log create mode 100644 tests/commanddata/test6.log create mode 100644 tests/commanddata/test7.log create mode 100644 tests/commanddata/test8.log create mode 100644 tests/commanddata/test9.log create mode 100644 tests/commandhelper.c create mode 100644 tests/commandtest.c
diff --git a/src/Makefile.am b/src/Makefile.am index 15bc8fc..704a3a9 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -52,6 +52,7 @@ UTIL_SOURCES = \ util/authhelper.c util/authhelper.h \ util/bridge.c util/bridge.h \ util/buf.c util/buf.h \ + util/command.c util/command.h \ util/conf.c util/conf.h \ util/cgroup.c util/cgroup.h \ util/event.c util/event.h \ diff --git a/src/util/command.c b/src/util/command.c new file mode 100644 index 0000000..36c6f61 --- /dev/null +++ b/src/util/command.c @@ -0,0 +1,782 @@ +/* + * command.c: Child command execution + * + * Copyright (C) 2010 Red Hat, Inc. + * + * 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 <config.h> + +#include "command.h" +#include "memory.h" +#include "virterror_internal.h" +#include "util.h" +#include "logging.h" + +#include <stdlib.h> +#include <poll.h> +#include <sys/wait.h> + +#define VIR_FROM_THIS VIR_FROM_NONE + +#define virCommandError(code, ...) \ + virReportErrorHelper(NULL, VIR_FROM_NONE, code, __FILE__, \ + __FUNCTION__, __LINE__, __VA_ARGS__) + +struct _virCommand { + int has_error; + + int nargs; + char **args; + + int nenv; + char **env; + + fd_set preserve; + unsigned int flags; + + const char *inbuf; + char **outbuf; + char **errbuf; + + int infd; + int inpipe; + int outfd; + int errfd; + int *outfdptr; + int *errfdptr; + + virExecHook hook; + void *opaque; + + pid_t pid; + char *pidfile; +}; + +/* + * Create a new command for named binary + */ +virCommandPtr virCommandNew(const char *binary) +{ + const char *const args[] = { binary, NULL }; + + return virCommandNewArgs(args); +} + +/* + * Create a new command with a NULL terminated + * set of args, taking binary from argv[0] + */ +virCommandPtr virCommandNewArgs(const char *const*args) +{ + virCommandPtr cmd; + + if (VIR_ALLOC(cmd) < 0) + return NULL; + + virCommandAddArgSet(cmd, args); + + if (cmd->has_error) { + virCommandFree(cmd); + return NULL; + } + + FD_ZERO(&cmd->preserve); + cmd->infd = cmd->outfd = cmd->errfd = -1; + cmd->inpipe = -1; + cmd->pid = -1; + + return cmd; +} + + +/* + * Preserve the specified file descriptor + * in the child, instead of closing it + */ +void virCommandPreserveFD(virCommandPtr cmd, + int fd) +{ + if (!cmd || cmd->has_error) + return; + + FD_SET(fd, &cmd->preserve); +} + +/* + * Save the child PID in a pidfile + */ +void virCommandSetPidFile(virCommandPtr cmd, + const char *pidfile) +{ + if (!cmd || cmd->has_error) + return; + + VIR_FREE(cmd->pidfile); + if (!(cmd->pidfile = strdup(pidfile))) + cmd->has_error = 1; +} + +/* + * Remove all capabilities from the child + */ +void virCommandClearCaps(virCommandPtr cmd) +{ + if (!cmd || cmd->has_error) + return; + + cmd->flags |= VIR_EXEC_CLEAR_CAPS; +} + + +/* + * Re-allow a specific capability + */ +void virCommandAllowCap(virCommandPtr cmd, + int capability ATTRIBUTE_UNUSED) +{ + if (!cmd || cmd->has_error) + return; + + /* XXX ? */ +} + + +/* + * Daemonize the child process + */ +void virCommandDaemonize(virCommandPtr cmd) +{ + if (!cmd || cmd->has_error) + return; + + cmd->flags |= VIR_EXEC_DAEMON; +} + + +/* + * Add an environment variable to the child + * using separate name & value strings + */ +void virCommandAddEnvPair(virCommandPtr cmd, + const char *name, + const char *value) +{ + char *env; + + if (!cmd || cmd->has_error) + return; + + if (virAsprintf(&env, "%s=%s", name, value ? value : "") < 0) { + cmd->has_error = 1; + return; + } + + if (VIR_REALLOC_N(cmd->env, cmd->nenv + 2) < 0) { + VIR_FREE(env); + cmd->has_error = 1; + return; + } + + cmd->env[cmd->nenv] = env; + cmd->env[cmd->nenv+1] = NULL; + cmd->nenv++; +} + + +/* + * Add an environemnt variable to the child + * using a preformated env string FOO=BAR + */ +void virCommandAddEnvString(virCommandPtr cmd, + const char *str) +{ + if (!cmd || cmd->has_error) + return; + + char *env; + + if (!cmd || cmd->has_error) + return; + + if (!(env = strdup(str))) { + cmd->has_error = 1; + return; + } + + if (VIR_REALLOC_N(cmd->env, cmd->nenv + 2) < 0) { + VIR_FREE(env); + cmd->has_error = 1; + return; + } + + cmd->env[cmd->nenv] = env; + cmd->env[cmd->nenv+1] = NULL; + cmd->nenv++; +} + + +/* + * Pass an environment variable to the child + * using current process' value + */ +void virCommandAddEnvPass(virCommandPtr cmd, + const char *name) +{ + char *value; + if (!cmd || cmd->has_error) + return; + + value = getenv(name); + if (value) + virCommandAddEnvPair(cmd, name, value); +} + + +void virCommandAddEnvPassCommon(virCommandPtr cmd) +{ + virCommandAddEnvPair(cmd, "LC_ALL", "C"); + + virCommandAddEnvPass(cmd, "LD_PRELOAD"); + virCommandAddEnvPass(cmd, "LD_LIBRARY_PATH"); + virCommandAddEnvPass(cmd, "PATH"); + virCommandAddEnvPass(cmd, "HOME"); + virCommandAddEnvPass(cmd, "USER"); + virCommandAddEnvPass(cmd, "LOGNAME"); + virCommandAddEnvPass(cmd, "TMPDIR"); +} + +/* + * Add a command line argument to the child + */ +void virCommandAddArg(virCommandPtr cmd, + const char *val) +{ + char *arg; + + if (!cmd || cmd->has_error) + return; + + if (!(arg = strdup(val))) { + cmd->has_error = 1; + return; + } + + if (VIR_REALLOC_N(cmd->args, cmd->nargs + 2) < 0) { + VIR_FREE(arg); + cmd->has_error = 1; + return; + } + + cmd->args[cmd->nargs] = arg; + cmd->args[cmd->nargs+1] = NULL; + cmd->nargs++; +} + + +void virCommandAddArgPair(virCommandPtr cmd, + const char *name, + const char *val) +{ + char *arg; + + if (!cmd || cmd->has_error) + return; + + if (virAsprintf(&arg, "%s=%s", name, val) < 0) { + cmd->has_error = 1; + return; + } + + if (VIR_REALLOC_N(cmd->args, cmd->nargs + 2) < 0) { + VIR_FREE(arg); + cmd->has_error = 1; + return; + } + + cmd->args[cmd->nargs] = arg; + cmd->args[cmd->nargs+1] = NULL; + cmd->nargs++; +} + +/* + * Add a NULL terminated list of args + */ +void virCommandAddArgSet(virCommandPtr cmd, + const char *const*vals) +{ + int narg = 0; + + if (!cmd || cmd->has_error) + return; + + while (vals[narg] != NULL) + narg++; + + if (VIR_REALLOC_N(cmd->args, cmd->nargs + narg + 1) < 0) { + cmd->has_error = 1; + return; + } + + narg = 0; + while (vals[narg] != NULL) { + char *arg = strdup(vals[narg]); + if (!arg) { + cmd->has_error = 1; + return; + } + cmd->args[cmd->nargs + narg] = arg; + narg++; + } + cmd->args[cmd->nargs + narg] = NULL; + cmd->nargs += narg; +} + + +/* + * Feed the child's stdin from a string buffer + */ +void virCommandSetInputBuffer(virCommandPtr cmd, + const char *inbuf) +{ + if (!cmd || cmd->has_error) + return; + + cmd->inbuf = inbuf; + cmd->infd = -1; + cmd->inpipe = -1; +} + + +/* + * Capture the child's stdout to a string buffer + */ +void virCommandSetOutputBuffer(virCommandPtr cmd, + char **outbuf) +{ + if (!cmd || cmd->has_error) + return; + + cmd->outbuf = outbuf; + cmd->outfdptr = &cmd->outfd; +} + + +/* + * Capture the child's stderr to a string buffer + */ +void virCommandSetErrorBuffer(virCommandPtr cmd, + char **errbuf) +{ + if (!cmd || cmd->has_error) + return; + + cmd->errbuf = errbuf; + cmd->errfdptr = &cmd->errfd; +} + + +/* + * Attach a file descriptor to the child's stdin + */ +void virCommandSetInputFD(virCommandPtr cmd, + int infd) +{ + if (!cmd || cmd->has_error) + return; + + cmd->inbuf = NULL; + cmd->inpipe = -1; + cmd->infd = infd; +} + + +/* + * Attach a file descriptor to the child's stdout + */ +void virCommandSetOutputFD(virCommandPtr cmd, + int *outfd) +{ + if (!cmd || cmd->has_error) + return; + + cmd->outbuf = NULL; + cmd->outfdptr = outfd; +} + + +/* + * Attach a file descriptor to the child's stderr + */ +void virCommandSetErrorFD(virCommandPtr cmd, + int *errfd) +{ + if (!cmd || cmd->has_error) + return; + + cmd->errbuf = NULL; + cmd->errfdptr = errfd; +} + + +void virCommandSetPreExecHook(virCommandPtr cmd, + virExecHook hook, + void *opaque) +{ + if (!cmd || cmd->has_error) + return; + + cmd->hook = hook; + cmd->opaque = opaque; +} + + +static int +virCommandProcessIO(virCommandPtr cmd) +{ + int infd = -1, outfd = -1, errfd = -1; + size_t inlen = 0, outlen = 0, errlen = 0; + size_t inoff = 0; + + /* With an input buffer, feed data to child + * via pipe */ + if (cmd->inbuf) { + inlen = strlen(cmd->inbuf); + infd = cmd->inpipe; + } + + /* With out/err buffer, we're the outfd/errfd + * have been filled with an FD for us */ + if (cmd->outbuf) + outfd = cmd->outfd; + if (cmd->errbuf) + errfd = cmd->errfd; + + for (;;) { + int i; + struct pollfd fds[3]; + int nfds = 0; + + if (infd != -1) { + fds[nfds].fd = infd; + fds[nfds].events = POLLOUT; + nfds++; + } + if (outfd != -1) { + fds[nfds].fd = outfd; + fds[nfds].events = POLLIN; + nfds++; + } + if (errfd != -1) { + fds[nfds].fd = errfd; + fds[nfds].events = POLLIN; + nfds++; + } + + if (nfds == 0) + break; + + if (poll(fds,nfds, -1) < 0) { + if ((errno == EAGAIN) || (errno == EINTR)) + continue; + virReportSystemError(errno, "%s", + _("unable to poll on child")); + return -1; + } + + for (i = 0; i < nfds ; i++) { + if (fds[i].fd == errfd || + fds[i].fd == outfd) { + char data[1024]; + char **buf; + size_t *len; + int done; + if (fds[i].fd == outfd) { + buf = cmd->outbuf; + len = &outlen; + } else { + buf = cmd->errbuf; + len = &errlen; + } + + done = read(fds[i].fd, data, sizeof(data)); + if (done < 0) { + if (errno != EINTR && + errno != EAGAIN) { + virReportSystemError(errno, "%s", + _("unable to write to child input")); + return -1; + } + } else if (done == 0) { + if (fds[i].fd == outfd) + outfd = -1; + else + errfd = -1; + } else { + if (VIR_REALLOC_N(*buf, *len + done + 1) < 0) { + virReportOOMError(); + return -1; + } + memmove(*buf + *len, data, done); + *len += done; + (*buf)[*len] = '\0'; + } + } else { + int done; + + done = write(infd, cmd->inbuf + inoff, + inlen - inoff); + if (done < 0) { + if (errno != EINTR && + errno != EAGAIN) { + virReportSystemError(errno, "%s", + _("unable to write to child input")); + return -1; + } + } else { + inoff += done; + if (inoff == inlen) { + close(infd); + infd = -1; + } + } + } + + } + } + + return 0; + +} + + +/* + * Run the command and wait for completion. + * Returns -1 on any error executing the + * command. Returns 0 if the command executed, + * with the exit status set + */ +int virCommandRun(virCommandPtr cmd, + int *exitstatus) +{ + int ret = 0; + char *outbuf = NULL; + char *errbuf = NULL; + int infd[2]; + if (!cmd || cmd->has_error) { + virReportOOMError(); + return -1; + } + + /* If we have an input buffer, we need + * a pipe to feed the data to the child */ + if (cmd->inbuf) { + if (pipe(infd) < 0) { + virReportSystemError(errno, "%s", + _("unable to open pipe")); + return -1; + } + cmd->infd = infd[0]; + cmd->inpipe = infd[1]; + } + + if (virCommandRunAsync(cmd, NULL) < 0) { + if (cmd->inbuf) { + close(infd[0]); + close(infd[1]); + } + return -1; + } + + /* If caller hasn't requested capture of + * stdout/err, then capture it ourselves + * so we can log it + */ + if (!cmd->outbuf && + !cmd->outfdptr) { + cmd->outfd = -1; + cmd->outfdptr = &cmd->outfd; + cmd->outbuf = &outbuf; + } + if (!cmd->errbuf && + !cmd->errfdptr) { + cmd->errfd = -1; + cmd->errfdptr = &cmd->errfd; + cmd->errbuf = &errbuf; + } + + if (cmd->inbuf || cmd->outbuf || cmd->errbuf) + ret = virCommandProcessIO(cmd); + + if (virCommandWait(cmd, exitstatus) < 0) + ret = -1; + + VIR_DEBUG("Result stdout: '%s' stderr: '%s'", + NULLSTR(*cmd->outbuf), + NULLSTR(*cmd->errbuf)); + + /* Reset any capturing, in case caller runs + * this identical command again */ + if (cmd->inbuf) { + close(infd[0]); + close(infd[1]); + } + if (cmd->outbuf == &outbuf) { + if (cmd->outfd != -1) + close(cmd->outfd); + cmd->outfd = -1; + cmd->outfdptr = NULL; + cmd->outbuf = NULL; + } + if (cmd->errbuf == &errbuf) { + if (cmd->errfd != -1) + close(cmd->errfd); + cmd->errfd = -1; + cmd->errfdptr = NULL; + cmd->errbuf = NULL; + } + + return ret; +} + + +/* + * Run the command asynchronously + * Returns -1 on any error executing the + * command. Returns 0 if the command executed. + */ +int virCommandRunAsync(virCommandPtr cmd, + pid_t *pid) +{ + int ret; + char *str; + + if (!cmd || cmd->has_error) { + virReportOOMError(); + return -1; + } + + if (cmd->pid != -1) { + virCommandError(VIR_ERR_INTERNAL_ERROR, + _("command is already running as pid %d"), + cmd->pid); + return -1; + } + + str = virArgvToString((const char *const *)cmd->args); + VIR_DEBUG("About to run %s", str ? str : cmd->args[0]); + VIR_FREE(str); + + ret = virExecWithHook((const char *const *)cmd->args, + (const char *const *)cmd->env, + &cmd->preserve, + &cmd->pid, + cmd->infd, + cmd->outfdptr, + cmd->errfdptr, + cmd->flags, + cmd->hook, + cmd->opaque, + cmd->pidfile); + + VIR_DEBUG("Command result %d, with PID is %d", + ret, (int)cmd->pid); + + if (ret == 0 && pid) + *pid = cmd->pid; + + return ret; +} + + +/* + * Wait for the async command to complete. + * Return -1 on any error waiting for + * completion. Returns 0 if the command + * finished with the exit status set + */ +int virCommandWait(virCommandPtr cmd, + int *exitstatus) +{ + int ret; + int status; + + if (!cmd || cmd->has_error) { + virReportOOMError(); + return -1; + } + + if (cmd->pid == -1) { + virCommandError(VIR_ERR_INTERNAL_ERROR, "%s", + _("command is not yet running")); + return -1; + } + + + /* Wait for intermediate process to exit */ + while ((ret = waitpid(cmd->pid, &status, 0)) == -1 && + errno == EINTR); + + if (ret == -1) { + virReportSystemError(errno, _("unable to wait for process %d"), + cmd->pid); + return -1; + } + + cmd->pid = -1; + + if (exitstatus == NULL) { + if (status != 0) { + virCommandError(VIR_ERR_INTERNAL_ERROR, + _("Intermediate daemon process exited with status %d."), + WEXITSTATUS(status)); + return -1; + } + } else { + *exitstatus = status; + } + + return 0; +} + + +/* + * Release all resources + */ +void virCommandFree(virCommandPtr cmd) +{ + int i; + if (!cmd) + return; + + if (cmd->outfd != -1) + close(cmd->outfd); + if (cmd->errfd != -1) + close(cmd->errfd); + + for (i = 0 ; i < cmd->nargs ; i++) + VIR_FREE(cmd->args[i]); + VIR_FREE(cmd->args); + + for (i = 0 ; i < cmd->nenv ; i++) + VIR_FREE(cmd->env[i]); + VIR_FREE(cmd->env); + + VIR_FREE(cmd->pidfile); + + VIR_FREE(cmd); +} diff --git a/src/util/command.h b/src/util/command.h new file mode 100644 index 0000000..f17767f --- /dev/null +++ b/src/util/command.h @@ -0,0 +1,197 @@ +/* + * command.h: Child command execution + * + * Copyright (C) 2010 Red Hat, Inc. + * + * 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 + * + */ + +#ifndef __VIR_COMMAND_H__ +# define __VIR_COMMAND_H__ + +# include "internal.h" +# include "util.h" + +typedef struct _virCommand virCommand; +typedef virCommand *virCommandPtr; + +/* + * Create a new command for named binary + */ +virCommandPtr virCommandNew(const char *binary); + +/* + * Create a new command with a NULL terminated + * set of args, taking binary from argv[0] + */ +virCommandPtr virCommandNewArgs(const char *const*args); + +/* All error report from these setup APIs is + * delayed until the Run/Exec/Wait methods + */ + +/* + * Preserve the specified file descriptor + * in the child, instead of closing it + */ +void virCommandPreserveFD(virCommandPtr cmd, + int fd); + +/* + * Save the child PID in a pidfile + */ +void virCommandSetPidFile(virCommandPtr cmd, + const char *pidfile); + +/* + * Remove all capabilities from the child + */ +void virCommandClearCaps(virCommandPtr cmd); + +/* + * Re-allow a specific capability + */ +void virCommandAllowCap(virCommandPtr cmd, + int capability); + +/* + * Daemonize the child process + */ +void virCommandDaemonize(virCommandPtr cmd); + + +/* + * Add an environment variable to the child + * using separate name & value strings + */ +void virCommandAddEnvPair(virCommandPtr cmd, + const char *name, + const char *value); + +/* + * Add an environemnt variable to the child + * using a preformated env string FOO=BAR + */ +void virCommandAddEnvString(virCommandPtr cmd, + const char *str); +/* + * Pass an environment variable to the child + * using current process' value + */ +void virCommandAddEnvPass(virCommandPtr cmd, + const char *name); +/* + * Pass a common set of environment variables + * to the child using current process' values + */ +void virCommandAddEnvPassCommon(virCommandPtr cmd); + +/* + * Add a command line argument to the child + */ +void virCommandAddArg(virCommandPtr cmd, + const char *val); +/* + * Add a command line argument to the child + */ +void virCommandAddArgPair(virCommandPtr cmd, + const char *name, + const char *val); +/* + * Add a NULL terminated list of args + */ +void virCommandAddArgSet(virCommandPtr cmd, + const char *const*vals); + + +/* + * Feed the child's stdin from a string buffer. + * + * NB: Only works with virCommandRun() + */ +void virCommandSetInputBuffer(virCommandPtr cmd, + const char *inbuf); +/* + * Capture the child's stdout to a string buffer + * + * NB: Only works with virCommandRun() + */ +void virCommandSetOutputBuffer(virCommandPtr cmd, + char **outbuf); +/* + * Capture the child's stderr to a string buffer + * + * NB: Only works with virCommandRun() + */ +void virCommandSetErrorBuffer(virCommandPtr cmd, + char **errbuf); + +/* + * Set a file descriptor as the child's stdin + */ +void virCommandSetInputFD(virCommandPtr cmd, + int infd); +/* + * Set a file descriptor as the child's stdout + */ +void virCommandSetOutputFD(virCommandPtr cmd, + int *outfd); +/* + * Set a file descriptor as the child's stderr + */ +void virCommandSetErrorFD(virCommandPtr cmd, + int *errfd); + +/* + * A hook function to run between fork + exec + */ +void virCommandSetPreExecHook(virCommandPtr cmd, + virExecHook hook, + void *opaque); + +/* + * Run the command and wait for completion. + * Returns -1 on any error executing the + * command. Returns 0 if the command executed, + * with the exit status set + */ +int virCommandRun(virCommandPtr cmd, + int *exitstatus) ATTRIBUTE_RETURN_CHECK; + +/* + * Run the command asynchronously + * Returns -1 on any error executing the + * command. Returns 0 if the command executed. + */ +int virCommandRunAsync(virCommandPtr cmd, + pid_t *pid) ATTRIBUTE_RETURN_CHECK; + +/* + * Wait for the async command to complete. + * Return -1 on any error waiting for + * completion. Returns 0 if the command + * finished with the exit status set + */ +int virCommandWait(virCommandPtr cmd, + int *exitstatus) ATTRIBUTE_RETURN_CHECK; + +/* + * Release all resources + */ +void virCommandFree(virCommandPtr cmd); + + +#endif /* __VIR_COMMAND_H__ */ diff --git a/tests/Makefile.am b/tests/Makefile.am index b5e09e3..5bd36f2 100644 --- a/tests/Makefile.am +++ b/tests/Makefile.am @@ -78,7 +78,8 @@ EXTRA_DIST = \ $(patsubst %,qemuhelpdata/%,$(qemuhelpdata)) noinst_PROGRAMS = virshtest conftest \ - nodeinfotest statstest qparamtest + nodeinfotest statstest qparamtest \ + commandtest commandhelper if WITH_XEN noinst_PROGRAMS += xml2sexprtest sexpr2xmltest \ @@ -155,6 +156,7 @@ TESTS = virshtest \ nodeinfotest \ statstest \ qparamtest \ + commandtest \ $(test_scripts) if WITH_XEN @@ -332,6 +334,16 @@ nodeinfotest_SOURCES = \ nodeinfotest.c testutils.h testutils.c nodeinfotest_LDADD = $(LDADDS) +commandtest_SOURCES = \ + commandtest.c testutils.h testutils.c +commandtest_CFLAGS = -Dabs_builddir="\"$(abs_builddir)\"" +commandtest_LDADD = $(LDADDS) + +commandhelper_SOURCES = \ + commandhelper.c +commandhelper_CFLAGS = -Dabs_builddir="\"$(abs_builddir)\"" +commandhelper_LDADD = $(LDADDS) + statstest_SOURCES = \ statstest.c testutils.h testutils.c statstest_LDADD = $(LDADDS) diff --git a/tests/commanddata/test1.log b/tests/commanddata/test1.log new file mode 100644 index 0000000..1b59206 --- /dev/null +++ b/tests/commanddata/test1.log @@ -0,0 +1,12 @@ +ENV:DISPLAY=:0.0 +ENV:HOME=/home/test +ENV:HOSTNAME=test +ENV:LANG=C +ENV:LOGNAME=testTMPDIR=/tmp +ENV:PATH=/usr/bin:/bin +ENV:USER=test +FD:0 +FD:1 +FD:2 +DAEMON:no +CWD:/tmp diff --git a/tests/commanddata/test10.log b/tests/commanddata/test10.log new file mode 100644 index 0000000..e1d6092 --- /dev/null +++ b/tests/commanddata/test10.log @@ -0,0 +1,14 @@ +ARG:-version +ARG:-log=bar.log +ENV:DISPLAY=:0.0 +ENV:HOME=/home/test +ENV:HOSTNAME=test +ENV:LANG=C +ENV:LOGNAME=testTMPDIR=/tmp +ENV:PATH=/usr/bin:/bin +ENV:USER=test +FD:0 +FD:1 +FD:2 +DAEMON:no +CWD:/tmp diff --git a/tests/commanddata/test11.log b/tests/commanddata/test11.log new file mode 100644 index 0000000..1b59206 --- /dev/null +++ b/tests/commanddata/test11.log @@ -0,0 +1,12 @@ +ENV:DISPLAY=:0.0 +ENV:HOME=/home/test +ENV:HOSTNAME=test +ENV:LANG=C +ENV:LOGNAME=testTMPDIR=/tmp +ENV:PATH=/usr/bin:/bin +ENV:USER=test +FD:0 +FD:1 +FD:2 +DAEMON:no +CWD:/tmp diff --git a/tests/commanddata/test12.log b/tests/commanddata/test12.log new file mode 100644 index 0000000..1b59206 --- /dev/null +++ b/tests/commanddata/test12.log @@ -0,0 +1,12 @@ +ENV:DISPLAY=:0.0 +ENV:HOME=/home/test +ENV:HOSTNAME=test +ENV:LANG=C +ENV:LOGNAME=testTMPDIR=/tmp +ENV:PATH=/usr/bin:/bin +ENV:USER=test +FD:0 +FD:1 +FD:2 +DAEMON:no +CWD:/tmp diff --git a/tests/commanddata/test13.log b/tests/commanddata/test13.log new file mode 100644 index 0000000..1b59206 --- /dev/null +++ b/tests/commanddata/test13.log @@ -0,0 +1,12 @@ +ENV:DISPLAY=:0.0 +ENV:HOME=/home/test +ENV:HOSTNAME=test +ENV:LANG=C +ENV:LOGNAME=testTMPDIR=/tmp +ENV:PATH=/usr/bin:/bin +ENV:USER=test +FD:0 +FD:1 +FD:2 +DAEMON:no +CWD:/tmp diff --git a/tests/commanddata/test2.log b/tests/commanddata/test2.log new file mode 100644 index 0000000..6bd7974 --- /dev/null +++ b/tests/commanddata/test2.log @@ -0,0 +1,14 @@ +ENV:DISPLAY=:0.0 +ENV:HOME=/home/test +ENV:HOSTNAME=test +ENV:LANG=C +ENV:LOGNAME=testTMPDIR=/tmp +ENV:PATH=/usr/bin:/bin +ENV:USER=test +FD:0 +FD:1 +FD:2 +FD:3 +FD:5 +DAEMON:no +CWD:/tmp diff --git a/tests/commanddata/test3.log b/tests/commanddata/test3.log new file mode 100644 index 0000000..1876685 --- /dev/null +++ b/tests/commanddata/test3.log @@ -0,0 +1,12 @@ +ENV:DISPLAY=:0.0 +ENV:HOME=/home/test +ENV:HOSTNAME=test +ENV:LANG=C +ENV:LOGNAME=testTMPDIR=/tmp +ENV:PATH=/usr/bin:/bin +ENV:USER=test +FD:0 +FD:1 +FD:2 +DAEMON:yes +CWD:/ diff --git a/tests/commanddata/test4.log b/tests/commanddata/test4.log new file mode 100644 index 0000000..f745c3f --- /dev/null +++ b/tests/commanddata/test4.log @@ -0,0 +1,10 @@ +ENV:HOME=/home/test +ENV:LC_ALL=C +ENV:LOGNAME=testTMPDIR=/tmp +ENV:PATH=/usr/bin:/bin +ENV:USER=test +FD:0 +FD:1 +FD:2 +DAEMON:no +CWD:/tmp diff --git a/tests/commanddata/test5.log b/tests/commanddata/test5.log new file mode 100644 index 0000000..5394428 --- /dev/null +++ b/tests/commanddata/test5.log @@ -0,0 +1,6 @@ +ENV:DISPLAY=:0.0 +FD:0 +FD:1 +FD:2 +DAEMON:no +CWD:/tmp diff --git a/tests/commanddata/test6.log b/tests/commanddata/test6.log new file mode 100644 index 0000000..cdfe445 --- /dev/null +++ b/tests/commanddata/test6.log @@ -0,0 +1,11 @@ +ENV:DISPLAY=:0.0 +ENV:HOME=/home/test +ENV:LC_ALL=C +ENV:LOGNAME=testTMPDIR=/tmp +ENV:PATH=/usr/bin:/bin +ENV:USER=test +FD:0 +FD:1 +FD:2 +DAEMON:no +CWD:/tmp diff --git a/tests/commanddata/test7.log b/tests/commanddata/test7.log new file mode 100644 index 0000000..87874fd --- /dev/null +++ b/tests/commanddata/test7.log @@ -0,0 +1,7 @@ +ENV:LANG=C +ENV:USER=test +FD:0 +FD:1 +FD:2 +DAEMON:no +CWD:/tmp diff --git a/tests/commanddata/test8.log b/tests/commanddata/test8.log new file mode 100644 index 0000000..e1d6092 --- /dev/null +++ b/tests/commanddata/test8.log @@ -0,0 +1,14 @@ +ARG:-version +ARG:-log=bar.log +ENV:DISPLAY=:0.0 +ENV:HOME=/home/test +ENV:HOSTNAME=test +ENV:LANG=C +ENV:LOGNAME=testTMPDIR=/tmp +ENV:PATH=/usr/bin:/bin +ENV:USER=test +FD:0 +FD:1 +FD:2 +DAEMON:no +CWD:/tmp diff --git a/tests/commanddata/test9.log b/tests/commanddata/test9.log new file mode 100644 index 0000000..e1d6092 --- /dev/null +++ b/tests/commanddata/test9.log @@ -0,0 +1,14 @@ +ARG:-version +ARG:-log=bar.log +ENV:DISPLAY=:0.0 +ENV:HOME=/home/test +ENV:HOSTNAME=test +ENV:LANG=C +ENV:LOGNAME=testTMPDIR=/tmp +ENV:PATH=/usr/bin:/bin +ENV:USER=test +FD:0 +FD:1 +FD:2 +DAEMON:no +CWD:/tmp diff --git a/tests/commandhelper.c b/tests/commandhelper.c new file mode 100644 index 0000000..f22a4d3 --- /dev/null +++ b/tests/commandhelper.c @@ -0,0 +1,112 @@ + +#include <config.h> + +#include <stdio.h> +#include <unistd.h> +#include <stdlib.h> +#include <fcntl.h> + +#include "internal.h" +#include "util.h" +#include "memory.h" + + +static int envsort(const void *a, const void *b) { + const char *const*astrptr = a; + const char *const*bstrptr = b; + const char *astr = *astrptr; + const char *bstr = *bstrptr; + char *aeq = strchr(astr, '='); + char *beq = strchr(bstr, '='); + char *akey = strndup(astr, aeq - astr); + char *bkey = strndup(bstr, beq - bstr); + int ret = strcmp(akey, bkey); + free(akey); + free(bkey); + return ret; +} + +int main(int argc, char **argv) { + int i, n; + char **origenv; + char **newenv; + FILE *log = fopen(abs_builddir "/commandhelper.log", "w"); + + if (!log) + goto error; + + for (i = 1 ; i < argc ; i++) { + fprintf(log, "ARG:%s\n", argv[i]); + } + + origenv = environ; + n = 0; + while (*origenv != NULL) { + n++; + origenv++; + } + + if (VIR_ALLOC_N(newenv, n) < 0) { + exit(EXIT_FAILURE); + } + + origenv = environ; + n = i = 0; + while (*origenv != NULL) { + newenv[i++] = *origenv; + n++; + origenv++; + } + qsort(newenv, n, sizeof(newenv[0]), envsort); + + for (i = 0 ; i < n ; i++) { + fprintf(log, "ENV:%s\n", newenv[i]); + } + + for (i = 0 ; i < sysconf(_SC_OPEN_MAX) ; i++) { + int f; + int closed; + if (i == fileno(log)) + continue; + closed = fcntl(i, F_GETFD, &f) == -1 && + errno == EBADF; + if (!closed) + fprintf(log, "FD:%d\n", i); + } + + fprintf(log, "DAEMON:%s\n", getppid() == 1 ? "yes" : "no"); + char cwd[1024]; + fprintf(log, "CWD:%s\n", getcwd(cwd, sizeof(cwd))); + + fclose(log); + + char buf[1024]; + ssize_t got; + + fprintf(stdout, "BEGIN STDOUT\n"); + fflush(stdout); + fprintf(stderr, "BEGIN STDERR\n"); + fflush(stderr); + + for (;;) { + got = read(STDIN_FILENO, buf, sizeof(buf)); + if (got < 0) + goto error; + if (got == 0) + break; + if (safewrite(STDOUT_FILENO, buf, got) != got) + goto error; + if (safewrite(STDERR_FILENO, buf, got) != got) + goto error; + } + + fprintf(stdout, "END STDOUT\n"); + fflush(stdout); + fprintf(stderr, "END STDERR\n"); + fflush(stderr); + + return EXIT_SUCCESS; + +error: + return EXIT_FAILURE; +} diff --git a/tests/commandtest.c b/tests/commandtest.c new file mode 100644 index 0000000..c66d345 --- /dev/null +++ b/tests/commandtest.c @@ -0,0 +1,494 @@ +#include <config.h> + +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <unistd.h> +#include <signal.h> + +#include "testutils.h" +#include "internal.h" +#include "nodeinfo.h" +#include "util.h" +#include "memory.h" +#include "command.h" + +#ifndef __linux__ + +static int +mymain(int argc ATTRIBUTE_UNUSED, char **argv ATTRIBUTE_UNUSED) +{ + exit (EXIT_AM_SKIP); +} + +#else + +static char *progname; +static char *abs_srcdir; + + +static int checkoutput(const char *testname) { + int ret = -1; + char cwd[1024]; + char *expectname = NULL; + char *expectlog = NULL; + char *actualname = NULL; + char *actuallog = NULL; + + if (!getcwd(cwd, sizeof(cwd))) + return -1; + + if (virAsprintf(&expectname, "%s/commanddata/%s.log", abs_srcdir, testname) < 0) + goto cleanup; + if (virAsprintf(&actualname, "%s/commandhelper.log", abs_builddir) < 0) + goto cleanup; + + if (virFileReadAll(expectname, 1024*64, &expectlog) < 0) { + fprintf(stderr, "cannot read %s\n", expectname); + goto cleanup; + } + + if (virFileReadAll(actualname, 1024*64, &actuallog) < 0) { + fprintf(stderr, "cannot read %s\n", actualname); + goto cleanup; + } + + if (STRNEQ(expectlog, actuallog)) { + virtTestDifference(stderr, expectlog, actuallog); + goto cleanup; + } + + + ret = 0; + +cleanup: + unlink(actuallog); + VIR_FREE(actuallog); + VIR_FREE(actualname); + VIR_FREE(expectlog); + VIR_FREE(expectname); + return ret; +} + +/* + * Run program, no args, inherit all ENV, keep CWD. + * Only stdin/out/err open + */ +static int test0(const void *unused ATTRIBUTE_UNUSED) { + virCommandPtr cmd = virCommandNew(abs_builddir "/commandhelper-doesnotexist"); + + if (virCommandRun(cmd, NULL) < 0) { + return 0; + } + + virCommandFree(cmd); + + return -1; +} + +/* + * Run program, no args, inherit all ENV, keep CWD. + * Only stdin/out/err open + */ +static int test1(const void *unused ATTRIBUTE_UNUSED) { + virCommandPtr cmd = virCommandNew(abs_builddir "/commandhelper"); + + if (virCommandRun(cmd, NULL) < 0) { + virErrorPtr err = virGetLastError(); + printf("Cannot run child %s\n", err->message); + return -1; + } + + virCommandFree(cmd); + + return checkoutput("test1"); +} + +/* + * Run program, no args, inherit all ENV, keep CWD. + * stdin/out/err + two extra FD open + */ +static int test2(const void *unused ATTRIBUTE_UNUSED) { + virCommandPtr cmd = virCommandNew(abs_builddir "/commandhelper"); + int newfd1 = dup(STDERR_FILENO); + int newfd2 = dup(STDERR_FILENO); + int newfd3 = dup(STDERR_FILENO); + close(newfd2); + + virCommandPreserveFD(cmd, newfd1); + virCommandPreserveFD(cmd, newfd3); + + if (virCommandRun(cmd, NULL) < 0) { + virErrorPtr err = virGetLastError(); + printf("Cannot run child %s\n", err->message); + return -1; + } + + virCommandFree(cmd); + + return checkoutput("test2"); +} + + +/* + * Run program, no args, inherit all ENV, CWD is / + * Only stdin/out/err open. + * Daemonized + */ +static int test3(const void *unused ATTRIBUTE_UNUSED) { + virCommandPtr cmd = virCommandNew(abs_builddir "/commandhelper"); + pid_t pid; + char *pidfile = virFilePid(abs_builddir, "commandhelper"); + + virCommandSetPidFile(cmd, pidfile); + virCommandDaemonize(cmd); + + if (virCommandRun(cmd, NULL) < 0) { + virErrorPtr err = virGetLastError(); + printf("Cannot run child %s\n", err->message); + return -1; + } + + if (virFileReadPid(abs_builddir, "commandhelper", &pid) != 0) { + printf("cannot read pidfile\n"); + return -1; + } + while (kill(pid, 0) != -1) + usleep(100*1000); + + virCommandFree(cmd); + + VIR_FREE(pidfile); + + return checkoutput("test3"); +} + + +/* + * Run program, no args, inherit filtered ENV, keep CWD. + * Only stdin/out/err open + */ +static int test4(const void *unused ATTRIBUTE_UNUSED) { + virCommandPtr cmd = virCommandNew(abs_builddir "/commandhelper"); + + virCommandAddEnvPassCommon(cmd); + + if (virCommandRun(cmd, NULL) < 0) { + virErrorPtr err = virGetLastError(); + printf("Cannot run child %s\n", err->message); + return -1; + } + + virCommandFree(cmd); + + return checkoutput("test4"); +} + + +/* + * Run program, no args, inherit filtered ENV, keep CWD. + * Only stdin/out/err open + */ +static int test5(const void *unused ATTRIBUTE_UNUSED) { + virCommandPtr cmd = virCommandNew(abs_builddir "/commandhelper"); + + virCommandAddEnvPass(cmd, "DISPLAY"); + virCommandAddEnvPass(cmd, "DOESNOTEXIST"); + + if (virCommandRun(cmd, NULL) < 0) { + virErrorPtr err = virGetLastError(); + printf("Cannot run child %s\n", err->message); + return -1; + } + + virCommandFree(cmd); + + return checkoutput("test5"); +} + + +/* + * Run program, no args, inherit filtered ENV, keep CWD. + * Only stdin/out/err open + */ +static int test6(const void *unused ATTRIBUTE_UNUSED) { + virCommandPtr cmd = virCommandNew(abs_builddir "/commandhelper"); + + virCommandAddEnvPassCommon(cmd); + virCommandAddEnvPass(cmd, "DISPLAY"); + virCommandAddEnvPass(cmd, "DOESNOTEXIST"); + + if (virCommandRun(cmd, NULL) < 0) { + virErrorPtr err = virGetLastError(); + printf("Cannot run child %s\n", err->message); + return -1; + } + + virCommandFree(cmd); + + return checkoutput("test6"); +} + +/* + * Run program, no args, inherit filtered ENV, keep CWD. + * Only stdin/out/err open + */ +static int test7(const void *unused ATTRIBUTE_UNUSED) { + virCommandPtr cmd = virCommandNew(abs_builddir "/commandhelper"); + + virCommandAddEnvString(cmd, "LANG=C"); + virCommandAddEnvPair(cmd, "USER", "test"); + + if (virCommandRun(cmd, NULL) < 0) { + virErrorPtr err = virGetLastError(); + printf("Cannot run child %s\n", err->message); + return -1; + } + + virCommandFree(cmd); + + return checkoutput("test7"); +} + + +/* + * Run program, some args, inherit all ENV, keep CWD. + * Only stdin/out/err open + */ +static int test8(const void *unused ATTRIBUTE_UNUSED) { + virCommandPtr cmd = virCommandNew(abs_builddir "/commandhelper"); + + virCommandAddArg(cmd, "-version"); + virCommandAddArgPair(cmd, "-log", "bar.log"); + + if (virCommandRun(cmd, NULL) < 0) { + virErrorPtr err = virGetLastError(); + printf("Cannot run child %s\n", err->message); + return -1; + } + + virCommandFree(cmd); + + return checkoutput("test8"); +} + + +/* + * Run program, some args, inherit all ENV, keep CWD. + * Only stdin/out/err open + */ +static int test9(const void *unused ATTRIBUTE_UNUSED) { + virCommandPtr cmd = virCommandNew(abs_builddir "/commandhelper"); + const char *const args[] = { + "-version", "-log=bar.log", NULL, + }; + + virCommandAddArgSet(cmd, args); + + if (virCommandRun(cmd, NULL) < 0) { + virErrorPtr err = virGetLastError(); + printf("Cannot run child %s\n", err->message); + return -1; + } + + virCommandFree(cmd); + + return checkoutput("test9"); +} + +/* + * Run program, some args, inherit all ENV, keep CWD. + * Only stdin/out/err open + */ +static int test10(const void *unused ATTRIBUTE_UNUSED) { + const char *args[] = { + abs_builddir "/commandhelper", + "-version", "-log=bar.log", NULL, + }; + virCommandPtr cmd = virCommandNewArgs(args); + + if (virCommandRun(cmd, NULL) < 0) { + virErrorPtr err = virGetLastError(); + printf("Cannot run child %s\n", err->message); + return -1; + } + + virCommandFree(cmd); + + return checkoutput("test10"); +} + +/* + * Run program, no args, inherit all ENV, keep CWD. + * Only stdin/out/err open. Set stdin data + */ +static int test11(const void *unused ATTRIBUTE_UNUSED) { + virCommandPtr cmd = virCommandNew(abs_builddir "/commandhelper"); + + virCommandSetInputBuffer(cmd, "Hello World\n"); + + if (virCommandRun(cmd, NULL) < 0) { + virErrorPtr err = virGetLastError(); + printf("Cannot run child %s\n", err->message); + return -1; + } + + virCommandFree(cmd); + + return checkoutput("test11"); +} + +/* + * Run program, no args, inherit all ENV, keep CWD. + * Only stdin/out/err open. Set stdin data + */ +static int test12(const void *unused ATTRIBUTE_UNUSED) { + virCommandPtr cmd = virCommandNew(abs_builddir "/commandhelper"); + char *outactual = NULL; + const char *outexpect = "BEGIN STDOUT\n" + "Hello World\n" + "END STDOUT\n"; + int ret = -1; + + virCommandSetInputBuffer(cmd, "Hello World\n"); + virCommandSetOutputBuffer(cmd, &outactual); + + if (virCommandRun(cmd, NULL) < 0) { + virErrorPtr err = virGetLastError(); + printf("Cannot run child %s\n", err->message); + return -1; + } + + virCommandFree(cmd); + + if (!STREQ(outactual, outexpect)) { + virtTestDifference(stderr, outactual, outexpect); + goto cleanup; + } + + if (checkoutput("test12") < 0) + goto cleanup; + + ret = 0; + +cleanup: + VIR_FREE(outactual); + return ret; +} + +/* + * Run program, no args, inherit all ENV, keep CWD. + * Only stdin/out/err open. Set stdin data + */ +static int test13(const void *unused ATTRIBUTE_UNUSED) { + virCommandPtr cmd = virCommandNew(abs_builddir "/commandhelper"); + char *outactual = NULL; + const char *outexpect = "BEGIN STDOUT\n" + "Hello World\n" + "END STDOUT\n"; + char *erractual = NULL; + const char *errexpect = "BEGIN STDERR\n" + "Hello World\n" + "END STDERR\n"; + int ret = -1; + + virCommandSetInputBuffer(cmd, "Hello World\n"); + virCommandSetOutputBuffer(cmd, &outactual); + virCommandSetErrorBuffer(cmd, &erractual); + + if (virCommandRun(cmd, NULL) < 0) { + virErrorPtr err = virGetLastError(); + printf("Cannot run child %s\n", err->message); + return -1; + } + + virCommandFree(cmd); + + if (!STREQ(outactual, outexpect)) { + virtTestDifference(stderr, outactual, outexpect); + goto cleanup; + } + if (!STREQ(erractual, errexpect)) { + virtTestDifference(stderr, erractual, errexpect); + goto cleanup; + } + + if (checkoutput("test13") < 0) + goto cleanup; + + ret = 0; + +cleanup: + VIR_FREE(outactual); + VIR_FREE(erractual); + return ret; +} + + +static int +mymain(int argc, char **argv) +{ + int ret = 0; + char cwd[PATH_MAX]; + + abs_srcdir = getenv("abs_srcdir"); + if (!abs_srcdir) + abs_srcdir = getcwd(cwd, sizeof(cwd)); + + progname = argv[0]; + + if (argc > 1) { + fprintf(stderr, "Usage: %s\n", progname); + return(EXIT_FAILURE); + } + + if (chdir("/tmp") < 0) + return(EXIT_FAILURE); + + virInitialize(); + + const char *const newenv[] = { + "PATH=/usr/bin:/bin", + "HOSTNAME=test", + "LANG=C", + "HOME=/home/test", + "USER=test", + "LOGNAME=test" + "TMPDIR=/tmp", + "DISPLAY=:0.0", + NULL + }; + environ = (char **)newenv; + +#define DO_TEST(NAME) \ + if (virtTestRun("Command Exec " #NAME " test", \ + 1, NAME, NULL) < 0) \ + ret = -1 + + char *actualname; + if (virAsprintf(&actualname, "%s/commandhelper.log", abs_builddir) < 0) + return EXIT_FAILURE; + unlink(actualname); + VIR_FREE(actualname); + + DO_TEST(test0); + DO_TEST(test1); + DO_TEST(test2); + DO_TEST(test3); + DO_TEST(test4); + DO_TEST(test5); + DO_TEST(test6); + DO_TEST(test7); + DO_TEST(test8); + DO_TEST(test9); + DO_TEST(test10); + DO_TEST(test11); + DO_TEST(test12); + DO_TEST(test13); + + return(ret==0 ? EXIT_SUCCESS : EXIT_FAILURE); +} + +#endif /* __linux__ */ + +VIRT_TEST_MAIN(mymain) -- 1.6.6.1 -- libvir-list mailing list libvir-list@redhat.com https://www.redhat.com/mailman/listinfo/libvir-list