From 19feefcaa1b219675e09c7d52169f7b0815aa123 Mon Sep 17 00:00:00 2001 From: "Richard W.M. Jones" Date: Sat, 8 Aug 2009 18:25:33 +0100 Subject: [PATCH] Hostinfo day 4: Implement command processing code. --- README | 29 +----- configure.ac | 2 +- hostinfo-set/Makefile.am | 2 + hostinfo-status/Makefile.am | 2 + hostinfo-test/Makefile.am | 2 + hostinfod/Makefile.am | 5 +- hostinfod/commands.c | 220 +++++++++++++++++++++++++++++++++++++++- hostinfod/hostinfo-protocol.pod | 32 ++++-- hostinfod/hostinfo.pod | 3 + hostinfod/hostinfod.h | 52 ++++++++-- hostinfod/main.c | 44 ++++++-- hostinfod/ping.c | 57 +++++++++++ 12 files changed, 398 insertions(+), 52 deletions(-) create mode 100644 hostinfod/ping.c diff --git a/README b/README index 2b8a7a8..6a6f62b 100644 --- a/README +++ b/README @@ -55,31 +55,12 @@ Accessing host information from guests ---------------------------------------------------------------------- No special software is required inside the guest. Simply connect to -the serial port[1] (eg. /dev/ttyS1 or COM2) using your favorite serial -terminal software and send commands. +the serial port (usually, but not always, /dev/ttyS1 or COM2) using +your favorite serial terminal software and send commands. The protocol is a simple, line-based, text protocol. No special libraries are needed to use it from programs or by hand. The commands -that you can send are documented in the hostinfod(8) manual page. +that you can send are documented in the hostinfo-protocol(5) manual +page. -If you get no response on the serial port at all, check on the host -side that the guest is allowed access (see /etc/hostinfod/). Also -check that the serial port has been set up in libvirt: - - virsh dumpxml DOMAIN - -(should show a clause) - -Also check that hostinfod (the daemon) is running on the host. - -Note that serial port settings like speed, parity etc. make no -difference for these emulated serial ports, so that won't be a factor. - -If you get an answer from the hostinfo daemon, but it indicates a -permissions error, check on the host side that the guest is allowed to -access the particular resource (see /etc/hostinfod/). - -[1] _Which_ serial port can vary - you need to either try them one by -one or ask the host administrator to tell you. We try to stay on the -second serial port (ttyS1 or COM2) but there are some configurations -where we get bumped up or down to other serial ports. +For troubleshooting, see hostinfo(8) manual page. diff --git a/configure.ac b/configure.ac index 125eb28..cc05e37 100644 --- a/configure.ac +++ b/configure.ac @@ -34,7 +34,7 @@ dnl Check support for 64 bit file offsets. AC_SYS_LARGEFILE dnl C functions which may be missing on older systems. -AC_CHECK_FUNCS([inotify_init1]) +AC_CHECK_FUNCS([inotify_init1 clock_gettime]) dnl Check for required packages using pkg-config. PKG_CHECK_MODULES([HOSTINFOD],[apr-1 >= 1.3]) diff --git a/hostinfo-set/Makefile.am b/hostinfo-set/Makefile.am index e9fe279..f3602fd 100644 --- a/hostinfo-set/Makefile.am +++ b/hostinfo-set/Makefile.am @@ -17,6 +17,8 @@ EXTRA_DIST = hostinfo-set.pl +CLEANFILES = hostinfo-set.8 + bin_SCRIPTS = hostinfo-set man_MANS = hostinfo-set.8 diff --git a/hostinfo-status/Makefile.am b/hostinfo-status/Makefile.am index d2e34e3..1791bac 100644 --- a/hostinfo-status/Makefile.am +++ b/hostinfo-status/Makefile.am @@ -17,6 +17,8 @@ EXTRA_DIST = hostinfo-status.pl +CLEANFILES = hostinfo-status.8 + bin_SCRIPTS = hostinfo-status man_MANS = hostinfo-status.8 diff --git a/hostinfo-test/Makefile.am b/hostinfo-test/Makefile.am index 5430676..25f9c7e 100644 --- a/hostinfo-test/Makefile.am +++ b/hostinfo-test/Makefile.am @@ -17,6 +17,8 @@ EXTRA_DIST = hostinfo-test.pl +CLEANFILES = hostinfo-test.1 + bin_SCRIPTS = hostinfo-test man_MANS = hostinfo-test.1 diff --git a/hostinfod/Makefile.am b/hostinfod/Makefile.am index 16272e0..5fc2cb1 100644 --- a/hostinfod/Makefile.am +++ b/hostinfod/Makefile.am @@ -17,6 +17,8 @@ EXTRA_DIST = hostinfo.pod hostinfo-protocol.pod +CLEANFILES = hostinfo.8 hostinfo-protocol.5 + sbin_PROGRAMS = hostinfod hostinfod_SOURCES = \ @@ -25,7 +27,8 @@ hostinfod_SOURCES = \ error.c \ hostinfod.h \ main.c \ - monitor_sockets.c + monitor_sockets.c \ + ping.c hostinfod_CFLAGS = \ -Wall \ diff --git a/hostinfod/commands.c b/hostinfod/commands.c index 1bbddc0..708b4a8 100644 --- a/hostinfod/commands.c +++ b/hostinfod/commands.c @@ -27,19 +27,237 @@ #include #include +#include +#include #include #include #include "hostinfod.h" +#define PROTOCOL_VERSION "1.0" +#define CRLF "\r\n" + +apr_hash_t *commands = NULL; + +static const char *string_of_arg_type (enum arg_type); +static void init_commands_hash (void); +static int not_printable (const char *str); + void -execute_command (time_t now, +execute_command (const struct timespec *now, struct guest_description *hval, const char *command) { + size_t len, i, j; + int neg; + char *cmd; + apr_array_header_t args; + struct arg arg; + command_fn fn; + debug ("%s: %s", hval->name, command); + /* Create a new pool for allocation during the lifetime of the + * request/response. **NB** the hval->reply field is allocated + * from this pool, which is why it gets nulled below. + */ + if (hval->rpool) + apr_pool_destroy (hval->rpool); + hval->rpool = NULL; + hval->reply = NULL; + hval->reply_size = hval->reply_posn = 0; + apr_pool_create (&hval->rpool, hval->pool); + + /* Split up the command. Commands have a very narrowly-defined + * format, and we reject any malformed commands with a 400 error. + * + * NB. A lot of the code below assumes 7 bit, printable ASCII, + * and this is only safe because of the call to 'not_printable' + * here. + */ + len = strlen (command); + if (len < 1 || len > 4094 || not_printable (command)) { + warning ("%s: command too short, too long or contained unprintable chars", + hval->name); + send_error (hval, 400); + return; + } + + /* Command is alphanumeric string, non-zero length. */ + i = strspn (command, + "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + "abcdefghijklmnopqrstuvwxyz" + "0123456789"); + if (i < 1) { + warning ("%s: no command part in request", hval->name); + send_error (hval, 400); + return; + } + + cmd = apr_pstrmemdup (hval->rpool, command, i); + for (j = 0; j < i; ++j) + cmd[j] = tolower (cmd[j]); + + args = apr_array_make (hval->rpool, 0, sizeof (struct arg)); + + while (command[i]) { /* command[i] is the space before the next arg */ + if (command[i] != ' ') { + warning ("%s: there must be a single space between command and each argument", + hval->name); + send_error (hval, 400); + return; + } + + neg = 0; + + i++; + switch (command[i]) { + case '\0': + warning ("%s: trailing space after command", hval->name); + send_error (hval, 400); + return; + + case '"': /* string literal */ + arg.type = arg_type_string; + XXX; + break; + + case '-': /* integer literal */ + neg = 1; + case '0': + case '1': + case '2': + case '3': + case '4': + case '5': + case '6': + case '7': + case '8': + case '9': + arg.type = arg_type_int; + XXX (); + break; + + case 't': /* boolean */ + case 'f': + case 'T': + case 'F': + arg.type = arg_type_bool; + if (strncasecmp (&command[i], "true", 4) == 0) { + arg.u.i = 1; + i += 4; + } else if (strncasecmp (&command[i], "false", 5) == 0) { + arg.u.i = 0; + i += 4; + } else + goto unknown_arg; + break; + + default: + unknown_arg: + warning ("%s: unknown or malformed argument starting at position %d ('%c')", + hval->name, i, command[i]); + send_error (hval, 400); + return; + } /* switch */ + + APR_ARRAY_PUSH (args, struct arg) = *arg; + } + + if (verbose) { + /* Debug output what the guest sent / what we decoded. */ + debug ("%s: command '%s' with %d args", hval->name, cmd, args->nelts); + for (i = 0; i < args->nelts; ++i) { + struct arg arg = APR_ARRAY_IDX (args, i, struct arg); + + switch (arg.type) { + case arg_type_bool: + case arg_type_int: + debug ("%s: arg %d : %s = %ld", + hval->name, i, string_of_arg_type (arg.type), arg.u.i); + break; + case arg_type_string: + debug ("%s: arg %d : %s = %s", + hval->name, i, string_of_arg_type (arg.type), arg.u.str); + break; + } + } + } + + /* Know about this command? */ + fn = apr_hash_get (commands, cmd, APR_HASH_KEY_STRING); + if (!fn) { + send_error (hval, 404); + return; + } + + /* Before dispatching the command, check the command is enabled + * and guest is not calling it too frequently. + */ + error ("XXXXXXX enabled check not implemented XXXXXXX"); + error ("XXXXXXX frequency check not implemented XXXXXXX"); + + /* Dispatch the command. */ + fn (hval, cmd, args); +} + +/* All commands must consist only of printable 7 bit ASCII. + * NB. Don't use isprint(3). + */ +static int +not_printable (const char *str) +{ + int c; + + while ((c = *(unsigned char *)str)) { + if (c < 32 || c > 126) + return 0; + str++; + } + return 1; +} + +/* For single line replies. */ +void +send_reply (struct guest_description *hval, int code, const char *fs, ...) +{ + const char *msg; + + /* All success codes must be 2xx. */ + assert (code >= 200 && code < 300); + + XXX; +} + +void +send_error (struct guest_description *hval, int code) +{ + const char *msg; + /* All errors must be 4xx or 5xx. */ + assert (code >= 400 && code < 600); + /* NB: If you add a code, update COMMON STATUS CODES section + * in hostinfo-protocol.pod. + */ + switch (code) { + case 400: msg = "Bad request"; break; + case 401: msg = "Command disabled"; break; + case 404: msg = "Command not found"; break; + case 406: msg = "Too frequent"; break; + case 500: msg = "Internal server error"; break; + default: msg = "Unknown error"; break; + } + /* Construct the reply. */ + hval->reply = + apr_psprintf (hval->rpool, PROTOCOL_VERSION " %03d %s" CRLF, + code, msg); + hval->reply_size = strlen (hval->reply); + hval->reply_posn = 0; + hval->state = guest_state_reply; + /* Penalty is always increased on errors, to ensure the guest + * cannot flood us with invalid commands. + */ + hval->penalty++; } diff --git a/hostinfod/hostinfo-protocol.pod b/hostinfod/hostinfo-protocol.pod index df838c0..c50d6e4 100644 --- a/hostinfod/hostinfo-protocol.pod +++ b/hostinfod/hostinfo-protocol.pod @@ -155,20 +155,27 @@ are not case sensitive, and you can send them in lowercase or mixed case. The request is always a single line, always consists only of 7 bit -printable ASCII (apart from the final CRLF), and must be less or equal -to 4096 characters in length (that includes the final CRLF). +printable ASCII bytes in the range 32-126 (apart from the final CRLF), +and must be less or equal to 4096 bytes in length (that includes the +final CRLF). Arguments that are strings I be quoted (using double-quotes). Special characters inside the strings are escaped using backslashes. -The rules are precisely the same as for C literal strings, so -for example C<"\t"> is a string containing a single tab character. +The rules are precisely the same as for C literal strings, so for +example C<"\t"> is a string containing a single tab character. -Arguments that are integers appear as integer literals. +Strings may not contain C<\0> characters in the middle, nor can they +be NULL. -Other argument types that are allowed are: booleans (I or -I). +Unless specified otherwise, the charset for strings is I, but +any bytes outside the range 32-126 must be sent as escape sequences, +eg. C<"\xC2\xA3"> would encode the pound (currency) sign. -Note that integers, booleans must never be quoted. +Arguments that are integers appear as integer literals, with optional +minus sign (C<->) before. As with C, use a I<0> to indicate octal and +I<0x> to indicate hexadecimal literals. + +Boolean arguments should be sent as I or I. =head2 PING @@ -184,6 +191,11 @@ alpha-numeric characters ([0-9a-zA-Z]{1,16}). Returns C back to the caller. +=head3 Example + + >>> PING "hello" + <<< 1.0 200 hello + =head3 Description This command is used to test the hostinfo connection. @@ -194,8 +206,8 @@ The possible responses to this are: =item * -The command succeeds and echos back the same C string. -This indicates that everything is working. +The command succeeds and echos back the same C string. This +indicates that the connection through to the host daemon is working. =item * diff --git a/hostinfod/hostinfo.pod b/hostinfod/hostinfo.pod index 3fe8bc0..9bebced 100644 --- a/hostinfod/hostinfo.pod +++ b/hostinfod/hostinfo.pod @@ -384,6 +384,9 @@ Use the L program in the guest to test this. =back +Note that serial port settings like speed, parity etc. make no +difference for these emulated serial ports, so that won't be a factor. + =head1 FILES =over 4 diff --git a/hostinfod/hostinfod.h b/hostinfod/hostinfod.h index 103783e..9a6fdba 100644 --- a/hostinfod/hostinfod.h +++ b/hostinfod/hostinfod.h @@ -35,27 +35,43 @@ enum guest_state { }; struct guest_description { - int counter; apr_pool_t *pool; /* Pool for lifetime of guest connection. */ + + apr_pool_t *rpool; /* Pool for lifetime of single req/resp. */ + const char *name; /* "driver-name" */ const char *sock_path; /* Full path to socket. */ int sock; /* Real socket. */ apr_socket_t *aprsock; /* APR socket. */ apr_pollfd_t pollfd; /* APR poll descriptor. */ enum guest_state state; /* State of the connection. */ - - /* Increments every time guest does something bad, decremented once per min */ - unsigned penalty; - time_t last_penalty_decr; + int counter; /* Used in reread_socket_dir. */ unsigned request_max; /* Max. length of request buffer. */ unsigned request_posn; /* Position in request buffer. */ char *request; /* Request buffer. */ - unsigned reply_alloc; /* Allocated for reply buffer. */ - unsigned reply_size; /* Size used in reply buffer. */ + unsigned reply_size; /* Size of reply buffer. */ unsigned reply_posn; /* Position in reply buffer. */ char *reply; /* Reply buffer. */ + + /* Increments every time guest does something bad, decremented once per min */ + unsigned penalty; + struct timespec last_penalty_decr; +}; + +enum arg_type { + arg_type_string, + arg_type_int, + arg_type_bool, +}; + +struct arg { + enum arg_type type; + union { + long i; /* For int, bool types. */ + char *str; /* For string type. */ + } u; }; /* main.c */ @@ -94,6 +110,26 @@ extern int sockets_inotify_fd; extern void monitor_socket_dir (void); /* commands.c */ -extern void execute_command (time_t now, struct guest_description *hval, const char *command); +extern apr_hash_t *commands; +extern void execute_command (const struct timespec *now, struct guest_description *hval, const char *command); +extern void send_error (struct guest_description *hval, int code); + +typedef void (*command_fn) (struct guest_description *hval, const char *cmd, apr_array_header_t *args); + +#define REGISTER_COMMAND(name,callback) \ + static void register_##name (void) __attribute__((constructor)); \ + \ + static void \ + register_##name (void) \ + { \ + if (commands == NULL) \ + commands = apr_hash_make (pool); \ + \ + /* The assignment below just causes a warning \ + * if the callback has the wrong type. \ + */ \ + command_fn cb = (callback); \ + apr_hash_set (commands, (name), APR_HASH_KEY_STRING, cb); \ + } #endif /* HOSTINFOD_H */ diff --git a/hostinfod/main.c b/hostinfod/main.c index 17da054..fa835bb 100644 --- a/hostinfod/main.c +++ b/hostinfod/main.c @@ -27,6 +27,7 @@ #include #include #include +#include #include #include #include @@ -514,6 +515,22 @@ guest_force_close (struct guest_description *hval) guest_removed (hval); } +/* Difference between two timespec structures (r = a - b) */ +static struct timespec * +diff_timespec (struct timespec *r, + const struct timespec *a, const struct timespec *b) +{ + if (a->tv_nsec - b->tv_nsec < 0) { + r->tv_sec = a->tv_sec - b->tv_sec - 1; + r->tv_nsec = 1000000000 + a->tv_nsec - b->tv_nsec; + } else { + r->tv_sec = a->tv_sec - b->tv_sec; + r->tv_nsec = a->tv_nsec - b->tv_nsec; + } + + return r; +} + /* This is called when there is some event from the guest, eg. * connection finished, read, write or closed. */ @@ -524,9 +541,16 @@ guest_event (const apr_pollfd_t *pollfd, void *hvalv) int err, max, r, extra; socklen_t len; char *p; - time_t now; - - time (&now); + struct timespec now; + +#ifdef HAVE_CLOCK_GETTIME + clock_gettime (CLOCK_MONOTONIC, &now); +#else + struct timeval tv; + gettimeofday (&tv, NULL); + now.tv_sec = tv.tv_sec; + now.tv_nsec = tv.tv_usec * 1000; +#endif /* If the guest keeps doing bad stuff, eventually lose patience with it. */ if (hval->penalty >= 100) { @@ -537,9 +561,15 @@ guest_event (const apr_pollfd_t *pollfd, void *hvalv) } /* Decrement the penalty once a minute, so the guest can recover. */ - if (hval->penalty > 0 && now - hval->last_penalty_decr >= 60) { - hval->penalty--; - hval->last_penalty_decr = now; + if (hval->penalty > 0) { + struct timespec diff; + + diff_timespec (&diff, &now, &hval->last_penalty_decr); + + if (diff.tv_sec >= 60) { + hval->penalty--; + hval->last_penalty_decr = now; + } } switch (hval->state) { @@ -621,7 +651,7 @@ guest_event (const apr_pollfd_t *pollfd, void *hvalv) *(p+1) = '\0'; - execute_command (now, hval, hval->request); + execute_command (&now, hval, hval->request); hval->request_posn = 0; break; diff --git a/hostinfod/ping.c b/hostinfod/ping.c new file mode 100644 index 0000000..075bd1b --- /dev/null +++ b/hostinfod/ping.c @@ -0,0 +1,57 @@ +/* virt-hostinfo + * Copyright (C) 2009 Red Hat Inc. + * + * 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., 675 Mass Ave, Cambridge, MA 02139, USA. + */ + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include +#include +#include + +#include "hostinfod.h" + +static void +ping (struct guest_description *hval, + const char *cmd, apr_array_header_t *args) +{ + const char *echodata; + size_t i, len; + + if (!get_args ("s", &echodata)) { + send_error (hval, 400); + return; + } + len = strlen (echodata); + if (len < 1 || len > 16) { + send_error (hval, 400); + return; + } + + for (i = 0; i < len; ++i) + if (!((echodata[i] >= 'a' && echodata[i] <= 'z') || + (echodata[i] >= 'A' && echodata[i] <= 'Z') || + (echodata[i] >= '0' && echodata[i] <= '9'))) { + send_error (hval, 400); + return; + } + + send_reply ( + +} +REGISTER_COMMAND ("ping", ping) -- 1.8.3.1