diff --git a/Makefile.am b/Makefile.am index 7cfe21b..dc62671 100644 --- a/Makefile.am +++ b/Makefile.am @@ -1,143 +1,147 @@ # Copyright (c) 2009 Red Hat, Inc. # # Authors: Andrew Beekhof # Steven Dake (sdake@redhat.com) # # This software licensed under BSD license, the text of which follows: # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # - Redistributions of source code must retain the above copyright notice, # this list of conditions and the following disclaimer. # - Redistributions in binary form must reproduce the above copyright notice, # this list of conditions and the following disclaimer in the documentation # and/or other materials provided with the distribution. # - Neither the name of the MontaVista Software, Inc. nor the names of its # contributors may be used to endorse or promote products derived from this # software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF # THE POSSIBILITY OF SUCH DAMAGE. SPEC = $(PACKAGE_NAME).spec TARFILE = $(PACKAGE_NAME)-$(VERSION).tar.gz EXTRA_DIST = autogen.sh conf/booth.conf.example AUTOMAKE_OPTIONS = foreign MAINTAINERCLEANFILES = Makefile.in aclocal.m4 configure depcomp \ config.guess config.sub missing install-sh \ autoheader automake autoconf test_lense.sh dist_doc_DATA = README COPYING boothconfdir = ${BOOTHSYSCONFDIR} boothconf_DATA = conf/booth.conf.example boothsitedir = /usr/lib/ocf/resource.d/pacemaker boothsite_SCRIPTS = script/ocf/booth-site bootharbitratordir = ${INITDDIR} bootharbitrator_SCRIPTS = script/lsb/booth-arbitrator +TESTS = test/runtests.py + SUBDIRS = src coverity: cov-build --dir=cov make cov-analyze --dir cov --concurrency --wait-for-license cov-format-errors --dir cov install-exec-local: $(INSTALL) -d $(DESTDIR)/${boothconfdir} $(INSTALL) -d $(DESTDIR)/${bootharbitratordir} $(INSTALL) -d $(DESTDIR)/${boothsitedir} $(INSTALL) -d $(DESTDIR)/${SOCKETDIR} install-exec-hook: ln -sf ${sbindir}/boothd $(DESTDIR)/${sbindir}/booth uninstall-local: rmdir $(DESTDIR)/${boothconfdir} || :; rmdir $(DESTDIR)/${bootharbitratordir} || :; rmdir $(DESTDIR)/${boothsitedir} || :; rmdir $(DESTDIR)/${SOCKETDIR} || :; +test: check + lint: for dir in src; do make -C $$dir lint; done dist-clean-local: rm -f autoconf automake autoheader ## make rpm/srpm section. $(SPEC): $(SPEC).in rm -f $@-t $@ date="$(shell LC_ALL=C date "+%a %b %d %Y")" && \ if [ -f .tarball-version ]; then \ gitver="$(shell cat .tarball-version)" && \ rpmver=$$gitver && \ alphatag="" && \ dirty="" && \ numcomm="0"; \ else \ gitver="$(shell git describe --abbrev=4 --match='v*' HEAD 2>/dev/null)" && \ rpmver=`echo $$gitver | sed -e "s/^v//" -e "s/-.*//g"` && \ alphatag=`echo $$gitver | sed -e "s/.*-//" -e "s/^g//"` && \ vtag=`echo $$gitver | sed -e "s/-.*//g"` && \ numcomm=`git rev-list $$vtag..HEAD | wc -l` && \ git update-index --refresh > /dev/null 2>&1 || true && \ dirty=`git diff-index --name-only HEAD 2>/dev/null`; \ fi && \ if [ -n "$$dirty" ]; then dirty="dirty"; else dirty=""; fi && \ if [ "$$numcomm" = "0" ]; then \ sed \ -e "s#@version@#$$rpmver#g" \ -e "s#%glo.*alpha.*##g" \ -e "s#%glo.*numcomm.*##g" \ -e "s#@dirty@#$$dirty#g" \ -e "s#@date@#$$date#g" \ $< > $@-t; \ else \ sed \ -e "s#@version@#$$rpmver#g" \ -e "s#@alphatag@#$$alphatag#g" \ -e "s#@numcomm@#$$numcomm#g" \ -e "s#@dirty@#$$dirty#g" \ -e "s#@date@#$$date#g" \ $< > $@-t; \ fi; \ if [ -z "$$dirty" ]; then sed -i -e "s#%glo.*dirty.*##g" $@-t; fi chmod a-w $@-t mv $@-t $@ $(TARFILE): $(MAKE) dist RPMBUILDOPTS = --define "_sourcedir $(abs_builddir)" \ --define "_specdir $(abs_builddir)" \ --define "_builddir $(abs_builddir)" \ --define "_srcrpmdir $(abs_builddir)" \ --define "_rpmdir $(abs_builddir)" srpm: clean $(MAKE) $(SPEC) $(TARFILE) rpmbuild $(RPMBUILDOPTS) --nodeps -bs $(SPEC) rpm: clean $(MAKE) $(SPEC) $(TARFILE) rpmbuild $(RPMBUILDOPTS) -ba $(SPEC) diff --git a/src/booth.h b/src/booth.h index b6dfddd..59bd698 100644 --- a/src/booth.h +++ b/src/booth.h @@ -1,81 +1,82 @@ /* * Copyright (C) 2011 Jiaju Zhang * * 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.1 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ #ifndef _BOOTH_H #define _BOOTH_H #include #include #include #define BOOTH_LOG_DUMP_SIZE (1024*1024) #define BOOTH_RUN_DIR "/var/run" #define BOOTH_LOG_DIR "/var/log" #define BOOTH_LOGFILE_NAME "booth.log" -#define BOOTH_LOCKFILE_NAME "booth.pid" +#define BOOTH_DEFAULT_LOCKFILE BOOTH_RUN_DIR "/booth.pid" #define BOOTH_DEFAULT_CONF "/etc/sysconfig/booth" #define DAEMON_NAME "booth" #define BOOTH_NAME_LEN 63 +#define BOOTH_PATH_LEN 127 #define BOOTHC_SOCK_PATH "boothc_lock" #define BOOTH_PROTO_FAMILY AF_INET #define BOOTH_CMD_PORT 22075 #define BOOTHC_MAGIC 0x5F1BA08C #define BOOTHC_VERSION 0x00010000 #define BOOTHC_OPT_FORCE 0x00000001 struct boothc_header { uint32_t magic; uint32_t version; uint32_t cmd; uint32_t option; uint32_t expiry; uint32_t len; uint32_t result; }; typedef enum { BOOTHC_CMD_LIST = 1, BOOTHC_CMD_GRANT, BOOTHC_CMD_REVOKE, } cmd_request_t; typedef enum { BOOTHC_RLT_ASYNC = 1, BOOTHC_RLT_SYNC_SUCC, BOOTHC_RLT_SYNC_FAIL, BOOTHC_RLT_INVALID_ARG, BOOTHC_RLT_REMOTE_OP, } cmd_result_t; struct client { int fd; void *workfn; void *deadfn; }; int client_add(int fd, void (*workfn)(int ci), void (*deadfn)(int ci)); int do_read(int fd, void *buf, size_t count); int do_write(int fd, void *buf, size_t count); void process_connection(int ci); #endif /* _BOOTH_H */ diff --git a/src/main.c b/src/main.c index d4f62da..dc13c66 100644 --- a/src/main.c +++ b/src/main.c @@ -1,1047 +1,1053 @@ /* * Copyright (C) 2011 Jiaju Zhang * * 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.1 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "log.h" #include "booth.h" #include "config.h" #include "transport.h" #include "timer.h" #include "pacemaker.h" #include "ticket.h" #define RELEASE_VERSION "1.0" #define CLIENT_NALLOC 32 int log_logfile_priority = LOG_INFO; int log_syslog_priority = LOG_ERR; int log_stderr_priority = LOG_ERR; static int client_maxi; static int client_size = 0; struct client *client = NULL; struct pollfd *pollfd = NULL; int poll_timeout = -1; typedef enum { ACT_ARBITRATOR = 1, ACT_SITE, ACT_CLIENT, } booth_role_t; typedef enum { OP_LIST = 1, OP_GRANT, OP_REVOKE, } operation_t; struct command_line { int type; /* ACT_ */ int op; /* OP_ */ int force; + char configfile[BOOTH_PATH_LEN]; + char lockfile[BOOTH_PATH_LEN]; char site[BOOTH_NAME_LEN]; char ticket[BOOTH_NAME_LEN]; }; static struct command_line cl; int do_read(int fd, void *buf, size_t count) { int rv, off = 0; while (off < count) { rv = read(fd, (char *)buf + off, count - off); if (rv == 0) return -1; if (rv == -1 && errno == EINTR) continue; if (rv == -1) return -1; off += rv; } return 0; } int do_write(int fd, void *buf, size_t count) { int rv, off = 0; retry: rv = write(fd, (char *)buf + off, count); if (rv == -1 && errno == EINTR) goto retry; if (rv < 0) { log_error("write errno %d", errno); return rv; } if (rv != count) { count -= rv; off += rv; goto retry; } return 0; } static int do_connect(const char *sock_path) { struct sockaddr_un sun; socklen_t addrlen; int rv, fd; fd = socket(PF_UNIX, SOCK_STREAM, 0); if (fd < 0) goto out; memset(&sun, 0, sizeof(sun)); sun.sun_family = AF_UNIX; strcpy(&sun.sun_path[1], sock_path); addrlen = sizeof(sa_family_t) + strlen(sun.sun_path+1) + 1; rv = connect(fd, (struct sockaddr *) &sun, addrlen); if (rv < 0) { close(fd); fd = rv; } out: return fd; } static void init_header(struct boothc_header *h, int cmd, int option, int result, int data_len) { memset(h, 0, sizeof(struct boothc_header)); h->magic = BOOTHC_MAGIC; h->version = BOOTHC_VERSION; h->len = data_len; h->cmd = cmd; h->option = option; h->result = result; } static void client_alloc(void) { int i; if (!client) { client = malloc(CLIENT_NALLOC * sizeof(struct client)); pollfd = malloc(CLIENT_NALLOC * sizeof(struct pollfd)); } else { client = realloc(client, (client_size + CLIENT_NALLOC) * sizeof(struct client)); pollfd = realloc(pollfd, (client_size + CLIENT_NALLOC) * sizeof(struct pollfd)); if (!pollfd) log_error("can't alloc for pollfd"); } if (!client || !pollfd) log_error("can't alloc for client array"); for (i = client_size; i < client_size + CLIENT_NALLOC; i++) { client[i].workfn = NULL; client[i].deadfn = NULL; client[i].fd = -1; pollfd[i].fd = -1; pollfd[i].revents = 0; } client_size += CLIENT_NALLOC; } static void client_dead(int ci) { close(client[ci].fd); client[ci].workfn = NULL; client[ci].fd = -1; pollfd[ci].fd = -1; } int client_add(int fd, void (*workfn)(int ci), void (*deadfn)(int ci)) { int i; if (!client) client_alloc(); again: for (i = 0; i < client_size; i++) { if (client[i].fd == -1) { client[i].workfn = workfn; if (deadfn) client[i].deadfn = deadfn; else client[i].deadfn = client_dead; client[i].fd = fd; pollfd[i].fd = fd; pollfd[i].events = POLLIN; if (i > client_maxi) client_maxi = i; return i; } } client_alloc(); goto again; } static int setup_listener(const char *sock_path) { struct sockaddr_un addr; socklen_t addrlen; int rv, s; s = socket(AF_LOCAL, SOCK_STREAM, 0); if (s < 0) { log_error("socket error %d %d", s, errno); return s; } memset(&addr, 0, sizeof(addr)); addr.sun_family = AF_LOCAL; strcpy(&addr.sun_path[1], sock_path); addrlen = sizeof(sa_family_t) + strlen(addr.sun_path+1) + 1; rv = bind(s, (struct sockaddr *) &addr, addrlen); if (rv < 0) { log_error("bind error %d %d", rv, errno); close(s); return rv; } rv = listen(s, 5); if (rv < 0) { log_error("listen error %d %d", rv, errno); close(s); return rv; } return s; } void process_connection(int ci) { struct boothc_header h; char *data = NULL; char *site, *ticket; int local, rv; rv = do_read(client[ci].fd, &h, sizeof(h)); if (rv < 0) { log_error("connection %d read error %d", ci, rv); return; } if (h.magic != BOOTHC_MAGIC) { log_error("connection %d magic error %x", ci, h.magic); return; } if (h.version != BOOTHC_VERSION) { log_error("connection %d version error %x", ci, h.version); return; } if (h.len) { data = malloc(h.len); if (!data) { log_error("process_connection no mem %u", h.len); return; } memset(data, 0, h.len); rv = do_read(client[ci].fd, data, h.len); if (rv < 0) { log_error("connection %d read data error %d", ci, rv); goto out; } h.len = 0; } switch (h.cmd) { case BOOTHC_CMD_LIST: assert(!data); h.result = list_ticket(&data, &h.len); break; case BOOTHC_CMD_GRANT: site = data; ticket = data + BOOTH_NAME_LEN; if (!check_ticket(ticket)) { h.result = BOOTHC_RLT_INVALID_ARG; goto reply; } if (!check_site(site, &local)) { h.result = BOOTHC_RLT_INVALID_ARG; goto reply; } if (local) h.result = grant_ticket(ticket, h.option); else h.result = BOOTHC_RLT_REMOTE_OP; break; case BOOTHC_CMD_REVOKE: site = data; ticket = data + BOOTH_NAME_LEN; if (!check_ticket(ticket)) { h.result = BOOTHC_RLT_INVALID_ARG; goto reply; } if (!check_site(site, &local)) { h.result = BOOTHC_RLT_INVALID_ARG; goto reply; } if (local) h.result = revoke_ticket(ticket, h.option); else h.result = BOOTHC_RLT_REMOTE_OP; break; default: log_error("connection %d cmd %x unknown", ci, h.cmd); break; } reply: rv = do_write(client[ci].fd, &h, sizeof(h)); if (rv < 0) log_error("connection %d write error %d", ci, rv); if (h.len) { rv = do_write(client[ci].fd, data, h.len); if (rv < 0) log_error("connection %d write error %d", ci, rv); } out: free(data); } static void process_listener(int ci) { int fd, i; fd = accept(client[ci].fd, NULL, NULL); if (fd < 0) { log_error("process_listener: accept error %d %d", fd, errno); return; } i = client_add(fd, process_connection, NULL); log_debug("client connection %d fd %d", i, fd); } static int setup_config(int type) { int rv; - rv = read_config(BOOTH_DEFAULT_CONF); + rv = read_config(cl.configfile); if (rv < 0) goto out; rv = check_config(type); if (rv < 0) goto out; out: return rv; } static int setup_transport(void) { int rv; rv = booth_transport[booth_conf->proto].init(ticket_recv); if (rv < 0) goto out; rv = booth_transport[TCP].init(NULL); if (rv < 0) goto out; out: return rv; } static int setup_timer(void) { return timerlist_init(); } static int setup(int type) { int rv; rv = setup_config(type); if (rv < 0) goto fail; rv = setup_timer(); if (rv < 0) goto fail; rv = setup_transport(); if (rv < 0) goto fail; rv = setup_ticket(); if (rv < 0) goto fail; rv = setup_listener(BOOTHC_SOCK_PATH); if (rv < 0) goto fail; client_add(rv, process_listener, NULL); return 0; fail: return -1; } static int loop(void) { void (*workfn) (int ci); void (*deadfn) (int ci); int rv, i; while (1) { rv = poll(pollfd, client_maxi + 1, poll_timeout); if (rv == -1 && errno == EINTR) continue; if (rv < 0) { log_error("poll errno %d", errno); goto fail; } for (i = 0; i <= client_maxi; i++) { if (client[i].fd < 0) continue; if (pollfd[i].revents & POLLIN) { workfn = client[i].workfn; if (workfn) workfn(i); } if (pollfd[i].revents & (POLLERR | POLLHUP | POLLNVAL)) { deadfn = client[i].deadfn; if (deadfn) deadfn(i); } } process_timerlist(); } return 0; fail: return -1; } static int do_list(void) { struct boothc_header h, *rh; char *reply = NULL, *data; int data_len; int fd, rv; init_header(&h, BOOTHC_CMD_LIST, 0, 0, 0); fd = do_connect(BOOTHC_SOCK_PATH); if (fd < 0) { rv = fd; goto out; } rv = do_write(fd, &h, sizeof(h)); if (rv < 0) goto out_close; reply = malloc(sizeof(struct boothc_header)); if (!reply) { rv = -ENOMEM; goto out_close; } rv = do_read(fd, reply, sizeof(struct boothc_header)); if (rv < 0) goto out_free; rh = (struct boothc_header *)reply; data_len = rh->len; reply = realloc(reply, sizeof(struct boothc_header) + data_len); if (!reply) { rv = -ENOMEM; goto out_free; } data = reply + sizeof(struct boothc_header); rv = do_read(fd, data, data_len); if (rv < 0) goto out_free; do_write(STDOUT_FILENO, data, data_len); rv = 0; out_free: free(reply); out_close: close(fd); out: return rv; } static int do_grant(void) { char *buf; struct boothc_header *h, reply; int buflen; uint32_t force = 0; int fd, rv; buflen = sizeof(struct boothc_header) + sizeof(cl.site) + sizeof(cl.ticket); buf = malloc(buflen); if (!buf) { rv = -ENOMEM; goto out; } h = (struct boothc_header *)buf; if (cl.force) force = BOOTHC_OPT_FORCE; init_header(h, BOOTHC_CMD_GRANT, force, 0, sizeof(cl.site) + sizeof(cl.ticket)); strcpy(buf + sizeof(struct boothc_header), cl.site); strcpy(buf + sizeof(struct boothc_header) + sizeof(cl.site), cl.ticket); fd = do_connect(BOOTHC_SOCK_PATH); if (fd < 0) { rv = fd; goto out_free; } rv = do_write(fd, buf, buflen); if (rv < 0) goto out_close; rv = do_read(fd, &reply, sizeof(struct boothc_header)); if (rv < 0) goto out_close; if (reply.result == BOOTHC_RLT_INVALID_ARG) { log_info("invalid argument!"); rv = -1; goto out_close; } if (reply.result == BOOTHC_RLT_REMOTE_OP) { struct booth_node to; int s; memset(&to, 0, sizeof(struct booth_node)); to.family = BOOTH_PROTO_FAMILY; strcpy(to.addr, cl.site); s = booth_transport[TCP].open(&to); if (s < 0) goto out_close; rv = booth_transport[TCP].send(s, buf, buflen); if (rv < 0) { booth_transport[TCP].close(s); goto out_close; } rv = booth_transport[TCP].recv(s, &reply, sizeof(struct boothc_header)); if (rv < 0) { booth_transport[TCP].close(s); goto out_close; } booth_transport[TCP].close(s); } if (reply.result == BOOTHC_RLT_ASYNC) { log_info("grant command sent, but result is async."); rv = 0; } else if (reply.result == BOOTHC_RLT_SYNC_SUCC) { log_info("grant succeeded!"); rv = 0; } else if (reply.result == BOOTHC_RLT_SYNC_FAIL) { log_info("grant failed!"); rv = 0; } else { log_error("internal error!"); rv = -1; } out_close: close(fd); out_free: free(buf); out: return rv; } static int do_revoke(void) { char *buf; struct boothc_header *h, reply; int buflen; uint32_t force = 0; int fd, rv; buflen = sizeof(struct boothc_header) + sizeof(cl.site) + sizeof(cl.ticket); buf = malloc(buflen); if (!buf) { rv = -ENOMEM; goto out; } h = (struct boothc_header *)buf; if (cl.force) force = BOOTHC_OPT_FORCE; init_header(h, BOOTHC_CMD_REVOKE, force, 0, sizeof(cl.site) + sizeof(cl.ticket)); strcpy(buf + sizeof(struct boothc_header), cl.site); strcpy(buf + sizeof(struct boothc_header) + sizeof(cl.site), cl.ticket); fd = do_connect(BOOTHC_SOCK_PATH); if (fd < 0) { rv = fd; goto out_free; } rv = do_write(fd, buf, buflen); if (rv < 0) goto out_close; rv = do_read(fd, &reply, sizeof(struct boothc_header)); if (rv < 0) goto out_close; if (reply.result == BOOTHC_RLT_INVALID_ARG) { log_info("invalid argument!"); rv = -1; goto out_close; } if (reply.result == BOOTHC_RLT_REMOTE_OP) { struct booth_node to; int s; memset(&to, 0, sizeof(struct booth_node)); to.family = BOOTH_PROTO_FAMILY; strcpy(to.addr, cl.site); s = booth_transport[TCP].open(&to); if (s < 0) goto out_close; rv = booth_transport[TCP].send(s, buf, buflen); if (rv < 0) { booth_transport[TCP].close(s); goto out_close; } rv = booth_transport[TCP].recv(s, &reply, sizeof(struct boothc_header)); if (rv < 0) { booth_transport[TCP].close(s); goto out_close; } booth_transport[TCP].close(s); } if (reply.result == BOOTHC_RLT_ASYNC) { log_info("revoke command sent, but result is async."); rv = 0; } else if (reply.result == BOOTHC_RLT_SYNC_SUCC) { log_info("revoke succeeded!"); rv = 0; } else if (reply.result == BOOTHC_RLT_SYNC_FAIL) { log_info("revoke failed!"); rv = 0; } else { log_error("internal error!"); rv = -1; } out_close: close(fd); out_free: free(buf); out: return rv; } static int lockfile(void) { - char path[PATH_MAX]; char buf[16]; struct flock lock; int fd, rv; - snprintf(path, PATH_MAX, "%s/%s", BOOTH_RUN_DIR, BOOTH_LOCKFILE_NAME); - - fd = open(path, O_CREAT|O_WRONLY, 0666); + fd = open(cl.lockfile, O_CREAT|O_WRONLY, 0666); if (fd < 0) { log_error("lockfile open error %s: %s", - path, strerror(errno)); + cl.lockfile, strerror(errno)); return -1; } lock.l_type = F_WRLCK; lock.l_start = 0; lock.l_whence = SEEK_SET; lock.l_len = 0; rv = fcntl(fd, F_SETLK, &lock); if (rv < 0) { log_error("lockfile setlk error %s: %s", - path, strerror(errno)); + cl.lockfile, strerror(errno)); goto fail; } rv = ftruncate(fd, 0); if (rv < 0) { log_error("lockfile truncate error %s: %s", - path, strerror(errno)); + cl.lockfile, strerror(errno)); goto fail; } memset(buf, 0, sizeof(buf)); snprintf(buf, sizeof(buf), "%d\n", getpid()); rv = write(fd, buf, strlen(buf)); if (rv <= 0) { log_error("lockfile write error %s: %s", - path, strerror(errno)); + cl.lockfile, strerror(errno)); goto fail; } return fd; fail: close(fd); return -1; } static void unlink_lockfile(int fd) { - char path[PATH_MAX]; - - snprintf(path, PATH_MAX, "%s/%s", BOOTH_RUN_DIR, BOOTH_LOCKFILE_NAME); - unlink(path); + unlink(cl.lockfile); close(fd); } static void print_usage(void) { printf("Usage:\n"); printf("booth [options]\n"); printf("\n"); printf("Types:\n"); printf(" arbitrator: daemon running on arbitrator\n"); printf(" site: daemon running on cluster site\n"); printf(" client: command running from client\n"); printf("\n"); printf("Operations:\n"); printf("Please note that operations are valid iff type is client!\n"); printf(" list: List all the tickets\n"); printf(" grant: Grant ticket T(-t T) to site S(-s S)\n"); printf(" revoke: Revoke ticket T(-t T) from site S(-s S)\n"); printf("\n"); printf("Options:\n"); + printf(" -c FILE Specify config file [default " BOOTH_DEFAULT_CONF "]\n"); + printf(" -l LOCKFILE Specify lock file [default " BOOTH_DEFAULT_LOCKFILE "]\n"); printf(" -D Enable debugging to stderr and don't fork\n"); printf(" -t ticket name\n"); printf(" -s site name\n"); printf(" -f ticket attribute: force, only valid when " "granting\n"); printf(" -h Print this help, then exit\n"); } -#define OPTION_STRING "Dt:s:fh" +#define OPTION_STRING "c:Dl:t:s:fh" static char *logging_entity = NULL; static int read_arguments(int argc, char **argv) { int optchar; char *arg1 = argv[1]; char *op = NULL; if (argc < 2 || !strcmp(arg1, "help") || !strcmp(arg1, "--help") || !strcmp(arg1, "-h")) { print_usage(); exit(EXIT_SUCCESS); } if (!strcmp(arg1, "version") || !strcmp(arg1, "--version") || !strcmp(arg1, "-V")) { printf("%s %s (built %s %s)\n", argv[0], RELEASE_VERSION, __DATE__, __TIME__); exit(EXIT_SUCCESS); } if (!strcmp(arg1, "arbitrator")) { cl.type = ACT_ARBITRATOR; logging_entity = DAEMON_NAME "-arbitrator"; optind = 2; } else if (!strcmp(arg1, "site")) { cl.type = ACT_SITE; logging_entity = DAEMON_NAME "-site"; optind = 2; } else if (!strcmp(arg1, "client")) { cl.type = ACT_CLIENT; if (argc < 3) { print_usage(); exit(EXIT_FAILURE); } op = argv[2]; optind = 3; } else { cl.type = ACT_CLIENT; op = argv[1]; optind = 2; } switch (cl.type) { case ACT_ARBITRATOR: break; case ACT_SITE: break; case ACT_CLIENT: if (!strcmp(op, "list")) cl.op = OP_LIST; else if (!strcmp(op, "grant")) cl.op = OP_GRANT; else if (!strcmp(op, "revoke")) cl.op = OP_REVOKE; else { fprintf(stderr, "client operation \"%s\" is unknown\n", op); exit(EXIT_FAILURE); } break; } while (optind < argc) { optchar = getopt(argc, argv, OPTION_STRING); switch (optchar) { + case 'c': + strncpy(cl.configfile, optarg, BOOTH_PATH_LEN - 1); + break; case 'D': debug_level = 1; log_logfile_priority = LOG_DEBUG; log_syslog_priority = LOG_DEBUG; break; + case 'l': + strncpy(cl.lockfile, optarg, BOOTH_PATH_LEN - 1); + break; case 't': if (cl.op == OP_GRANT || cl.op == OP_REVOKE) strcpy(cl.ticket, optarg); else { print_usage(); exit(EXIT_FAILURE); } break; case 's': if (cl.op == OP_GRANT || cl.op == OP_REVOKE) strcpy(cl.site, optarg); else { print_usage(); exit(EXIT_FAILURE); } break; case 'f': if (cl.op == OP_GRANT) cl.force = 1; else { print_usage(); exit(EXIT_FAILURE); } break; case 'h': print_usage(); exit(EXIT_SUCCESS); break; case ':': case '?': fprintf(stderr, "Please use '-h' for usage.\n"); exit(EXIT_FAILURE); break; default: fprintf(stderr, "unknown option: %s\n", argv[optind]); exit(EXIT_FAILURE); break; }; } return 0; } static void set_scheduler(void) { struct sched_param sched_param; struct rlimit rlimit; int rv; rlimit.rlim_cur = RLIM_INFINITY; rlimit.rlim_max = RLIM_INFINITY; setrlimit(RLIMIT_MEMLOCK, &rlimit); rv = mlockall(MCL_CURRENT | MCL_FUTURE); if (rv < 0) { log_error("mlockall failed"); } rv = sched_get_priority_max(SCHED_RR); if (rv != -1) { sched_param.sched_priority = rv; rv = sched_setscheduler(0, SCHED_RR, &sched_param); if (rv == -1) log_error("could not set SCHED_RR priority %d err %d", sched_param.sched_priority, errno); } else { log_error("could not get maximum scheduler priority err %d", errno); } } static void set_oom_adj(int val) { FILE *fp; fp = fopen("/proc/self/oom_adj", "w"); if (!fp) return; fprintf(fp, "%i", val); fclose(fp); } static int do_server(int type) { int fd; int rv = -1; if (!debug_level) { if (daemon(0, 0) < 0) { perror("daemon error"); exit(EXIT_FAILURE); } } fd = lockfile(); if (fd < 0) return fd; if (type == ARBITRATOR) log_info("BOOTH arbitrator daemon started"); else if (type == SITE) log_info("BOOTH cluster site daemon started"); set_scheduler(); set_oom_adj(-16); rv = setup(SITE); if (rv == 0) rv = loop(); unlink_lockfile(fd); return rv; } static int do_client(void) { int rv = -1; switch (cl.op) { case OP_LIST: rv = do_list(); break; case OP_GRANT: rv = do_grant(); break; case OP_REVOKE: rv = do_revoke(); break; } return rv; } int main(int argc, char *argv[]) { int rv; memset(&cl, 0, sizeof(cl)); + strncpy(cl.configfile, BOOTH_DEFAULT_CONF, BOOTH_PATH_LEN - 1); + strncpy(cl.lockfile, BOOTH_DEFAULT_LOCKFILE, BOOTH_PATH_LEN - 1); rv = read_arguments(argc, argv); if (rv < 0) goto out; if (cl.type == ACT_CLIENT) { cl_log_enable_stderr(TRUE); cl_log_set_facility(0); } else { cl_log_set_entity(logging_entity); cl_log_enable_stderr(debug_level ? TRUE : FALSE); cl_log_set_facility(HA_LOG_FACILITY); } cl_inherit_logging_environment(0); switch (cl.type) { case ACT_ARBITRATOR: rv = do_server(ARBITRATOR); break; case ACT_SITE: rv = do_server(SITE); break; case ACT_CLIENT: rv = do_client(); break; } out: return rv ? EXIT_FAILURE : EXIT_SUCCESS; } diff --git a/test/arbtests.py b/test/arbtests.py new file mode 100755 index 0000000..caba010 --- /dev/null +++ b/test/arbtests.py @@ -0,0 +1,6 @@ +#!/usr/bin/python + +from servertests import ServerTests + +class ArbitratorConfigTests(ServerTests): + mode = 'arbitrator' diff --git a/test/assertions.py b/test/assertions.py new file mode 100644 index 0000000..49938a1 --- /dev/null +++ b/test/assertions.py @@ -0,0 +1,41 @@ +class BoothAssertions: + def configFileMissingMyIP(self, config_file=None, lock_file=None): + (pid, ret, stdout, stderr, runner) = \ + self.run_booth(config_file=config_file, lock_file=lock_file, + expected_exitcode=1) + + expected_error = "ERROR: can't find myself in config file" + self.assertRegexpMatches(self.read_log(), expected_error) + + def assertLockFileError(self, config_file=None, config_text=None, + lock_file=True, args=[]): + (pid, ret, stdout, stderr, runner) = \ + self.run_booth(config_text=config_text, config_file=config_file, + lock_file=lock_file, args=args, expected_exitcode=1) + expected_error = 'lockfile open error %s: Permission denied' % runner.lock_file_used() + self.assertRegexpMatches(self.read_log(), expected_error) + + ###################################################################### + # backported from 2.7 just in case we're running on an older Python + def assertRegexpMatches(self, text, expected_regexp, msg=None): + """Fail the test unless the text matches the regular expression.""" + if isinstance(expected_regexp, basestring): + expected_regexp = re.compile(expected_regexp) + if not expected_regexp.search(text): + msg = msg or "Regexp didn't match" + msg = '%s: %r not found in %r' % (msg, expected_regexp.pattern, text) + raise self.failureException(msg) + + def assertNotRegexpMatches(self, text, unexpected_regexp, msg=None): + """Fail the test if the text matches the regular expression.""" + if isinstance(unexpected_regexp, basestring): + unexpected_regexp = re.compile(unexpected_regexp) + match = unexpected_regexp.search(text) + if match: + msg = msg or "Regexp matched" + msg = '%s: %r matches %r in %r' % (msg, + text[match.start():match.end()], + unexpected_regexp.pattern, + text) + raise self.failureException(msg) + ###################################################################### diff --git a/test/boothrunner.py b/test/boothrunner.py new file mode 100755 index 0000000..7ea3d70 --- /dev/null +++ b/test/boothrunner.py @@ -0,0 +1,108 @@ +#!/usr/bin/python + +import os +import subprocess +import time +import unittest + +class BoothRunner: + default_config_file = '/etc/sysconfig/booth' + default_lock_file = '/var/run/booth.pid' + + def __init__(self, boothd_path, mode, args): + self.boothd_path = boothd_path + self.args = [ mode ] + self.final_args = args # will be appended to self.args + self.mode = mode + self.config_file = None + self.lock_file = None + + def set_config_file_arg(self): + self.args += [ '-c', self.config_file ] + + def set_config_file(self, config_file): + self.config_file = config_file + self.set_config_file_arg() + + def set_lock_file(self, lock_file): + self.lock_file = lock_file + self.args += [ '-l', self.lock_file ] + + def set_debug(self): + self.args += [ '-D' ] + + def all_args(self): + return [ self.boothd_path ] + self.args + self.final_args + + def show_output(self, stdout, stderr): + if stdout: + print "STDOUT:" + print "------" + print stdout, + if stderr: + print "STDERR:" + print "------" + print stderr, + print "-" * 70 + + def subproc_completed_within(self, p, timeout): + start = time.time() + wait = 0.1 + while True: + if p.poll() is not None: + return True + elapsed = time.time() - start + if elapsed + wait > timeout: + wait = timeout - elapsed + print "Waiting on %d for %.1fs ..." % (p.pid, wait) + time.sleep(wait) + elapsed = time.time() - start + if elapsed >= timeout: + return False + wait *= 2 + + def lock_file_used(self): + return self.lock_file or self.default_lock_file + + def config_file_used(self): + return self.config_file or self.default_config_file + + def config_text_used(self): + config_file = self.config_file_used() + try: + c = open(config_file) + except: + return None + text = "".join(c.readlines()) + c.close() + + text = text.replace('\t', '') + text = text.replace('\n', '|\n') + + return text + + def show_args(self): + print "\n" + print "-" * 70 + print "Running", ' '.join(self.all_args()) + msg = "with config from %s" % self.config_file_used() + config_text = self.config_text_used() + if config_text is not None: + msg += ": [%s]" % config_text + print msg + + def run(self): + p = subprocess.Popen(self.all_args(), stdout=subprocess.PIPE, stderr=subprocess.PIPE) + if not p: + raise RuntimeError, "failed to start subprocess" + + print "Started subprocess pid %d" % p.pid + + completed = self.subproc_completed_within(p, 2) + + if completed: + (stdout, stderr) = p.communicate() + self.show_output(stdout, stderr) + return (p.pid, p.returncode, stdout, stderr) + + return (p.pid, None, None, None) diff --git a/test/boothtestenv.py b/test/boothtestenv.py new file mode 100755 index 0000000..a19e6ba --- /dev/null +++ b/test/boothtestenv.py @@ -0,0 +1,57 @@ +#!/usr/bin/python + +import os +import time +import tempfile +import unittest + +from assertions import BoothAssertions +from boothrunner import BoothRunner + +class BoothTestEnvironment(unittest.TestCase, BoothAssertions): + test_src_path = os.path.abspath(os.path.dirname(__file__)) + dist_path = os.path.join(test_src_path, '..' ) + src_path = os.path.join(dist_path, 'src' ) + boothd_path = os.path.join(src_path, 'boothd') + conf_path = os.path.join(dist_path, 'conf' ) + example_config_path = os.path.join(conf_path, 'booth.conf.example') + + def setUp(self): + if not self._testMethodName.startswith('test_'): + raise RuntimeError, "unexpected test method name: " + self._testMethodName + self.test_name = self._testMethodName[5:] + self.test_path = os.path.join(self.test_run_path, self.test_name) + os.makedirs(self.test_path) + + def get_tempfile(self, identity): + tf = tempfile.NamedTemporaryFile( + prefix='%s.%d.' % (identity, time.time()), + dir=self.test_path, + delete=False + ) + return tf.name + + def init_log(self): + self.log_file = self.get_tempfile('log') + os.putenv('HA_debugfile', self.log_file) # See cluster-glue/lib/clplumbing/cl_log.c + + def read_log(self): + if not os.path.exists(self.log_file): + return '' + + l = open(self.log_file) + msgs = ''.join(l.readlines()) + l.close() + return msgs + + def check_return_code(self, pid, return_code, expected_exitcode): + if return_code is None: + print "pid %d still running" % pid + if expected_exitcode is not None: + self.fail("expected exit code %d, not long-running process" % expected_exitcode) + else: + print "pid %d exited with code %d" % (pid, return_code) + msg = "should exit with code %s" % expected_exitcode + msg += "\nlog follows (see %s)" % self.log_file + msg += "\n-----------\n%s" % self.read_log() + self.assertEqual(return_code, expected_exitcode, msg) diff --git a/test/clientenv.py b/test/clientenv.py new file mode 100755 index 0000000..e4f9f7d --- /dev/null +++ b/test/clientenv.py @@ -0,0 +1,24 @@ +#!/usr/bin/python + +from boothtestenv import BoothTestEnvironment +from boothrunner import BoothRunner + +class ClientTestEnvironment(BoothTestEnvironment): + mode = 'client' + + def run_booth(self, config_text=None, config_file=None, lock_file=True, args=[], + expected_exitcode=0, debug=False): + ''' + Runs boothd. + + Returns a (pid, return_code, stdout, stderr, runner) tuple, + where return_code/stdout/stderr are None iff pid is still running. + ''' + self.init_log() + + runner = BoothRunner(self.boothd_path, self.mode, args) + runner.show_args() + (pid, return_code, stdout, stderr) = runner.run() + self.check_return_code(pid, return_code, expected_exitcode) + + return (pid, return_code, stdout, stderr, runner) diff --git a/test/clienttests.py b/test/clienttests.py new file mode 100755 index 0000000..78ccc45 --- /dev/null +++ b/test/clienttests.py @@ -0,0 +1,6 @@ +#!/usr/bin/python + +from clientenv import ClientTestEnvironment + +class ClientConfigTests(ClientTestEnvironment): + mode = 'client' diff --git a/test/runtests.py b/test/runtests.py new file mode 100755 index 0000000..5bc0700 --- /dev/null +++ b/test/runtests.py @@ -0,0 +1,53 @@ +#!/usr/bin/python + +import os +import re +import shutil +import sys +import tempfile +import time +import unittest + +from clienttests import ClientConfigTests +from sitetests import SiteConfigTests +from arbtests import ArbitratorConfigTests + +if __name__ == '__main__': + if os.geteuid() == 0: + sys.stderr.write("Must be run non-root; aborting.\n") + sys.exit(1) + + tmp_path = '/tmp/booth-tests' + if not os.path.exists(tmp_path): + os.makedirs(tmp_path) + test_run_path = tempfile.mkdtemp(prefix='%d.' % time.time(), dir=tmp_path) + + suite = unittest.TestSuite() + testclasses = [ + SiteConfigTests, + #ArbitratorConfigTests, + ClientConfigTests, + ] + for testclass in testclasses: + testclass.test_run_path = test_run_path + suite.addTests(unittest.TestLoader().loadTestsFromTestCase(testclass)) + + runner_args = { + #'verbosity' : 2, + } + major, minor, micro, releaselevel, serial = sys.version_info + if major > 2 or (major == 2 and minor >= 7): + # New in 2.7 + runner_args['buffer'] = True + runner_args['failfast'] = True + pass + + runner = unittest.TextTestRunner(**runner_args) + result = runner.run(suite) + + if result.wasSuccessful(): + shutil.rmtree(test_run_path) + sys.exit(0) + else: + print "Left %s for debugging" % test_run_path + sys.exit(1) diff --git a/test/serverenv.py b/test/serverenv.py new file mode 100755 index 0000000..2bfe99e --- /dev/null +++ b/test/serverenv.py @@ -0,0 +1,143 @@ +#!/usr/bin/python + +import os +import time +import unittest + +from assertions import BoothAssertions +from boothrunner import BoothRunner +from boothtestenv import BoothTestEnvironment + +class ServerTestEnvironment(BoothTestEnvironment): + typical_config = """\ +# This is like the config in the manual +transport="UDP" +port="6666" +# Here's another comment +arbitrator="147.2.207.14" +site="147.4.215.19" +site="147.18.2.1" +ticket="ticketA" +ticket="ticketB" +""" + + def run_booth(self, config_text=None, config_file=None, lock_file=True, args=[], + expected_exitcode=0, debug=False): + ''' + Runs boothd. Defaults to using a temporary lock file and + the standard config file path. + + Returns a (pid, return_code, stdout, stderr, runner) tuple, + where return_code/stdout/stderr are None iff pid is still running. + ''' + self.init_log() + + runner = BoothRunner(self.boothd_path, self.mode, args) + + if config_text: + config_file = self.write_config_file(config_text) + if config_file: + runner.set_config_file(config_file) + + if lock_file is True: + lock_file = os.path.join(self.test_path, 'boothd-lock.pid') + if lock_file: + runner.set_lock_file(lock_file) + + if debug: + runner.set_debug() + + runner.show_args() + (pid, return_code, stdout, stderr) = runner.run() + self.check_return_code(pid, return_code, expected_exitcode) + + expected_daemon = expected_exitcode == 0 or expected_exitcode is None + got_daemon = return_code == 0 or return_code is None + + if got_daemon: + self.check_daemon_handling(runner, expected_daemon) + + return (pid, return_code, stdout, stderr, runner) + + def write_config_file(self, config_text): + config_file = self.get_tempfile('config') + c = open(config_file, 'w') + c.write(config_text) + c.close() + return config_file + + def check_daemon_handling(self, runner, expected_daemon): + ''' + Check that the lock file contains a pid referring to a running + daemon. Then kill the daemon, and ensure that the lock file + vanishes (bnc#749763). + ''' + daemon_pid = self.get_daemon_pid_from_lock_file(runner.lock_file) + err = "lock file should contain pid" + if not expected_daemon: + err += ", even though we didn't expect a daemon" + self.assertTrue(daemon_pid is not None, err) + + daemon_running = self.is_pid_running_daemon(daemon_pid) + err = "pid in lock file should referred to a running daemon" + self.assertTrue(daemon_running, err) + + if daemon_running: + print "killing %s ..." % daemon_pid + os.kill(int(daemon_pid), 15) + print "killed" + time.sleep(1) + daemon_pid = self.get_daemon_pid_from_lock_file(runner.lock_file) + self.assertTrue(daemon_pid is not None, + 'bnc#749763: lock file should vanish after daemon is killed') + + def get_daemon_pid_from_lock_file(self, lock_file): + ''' + Returns the pid contained in lock_file, or None if it doesn't exist. + ''' + if not os.path.exists(lock_file): + print "%s does not exist" % lock_file + return None + + l = open(lock_file) + lines = l.readlines() + l.close() + self.assertEqual(len(lines), 1, "Lock file should contain one line") + pid = lines[0].rstrip() + print "lockfile contains: %s" % pid + return pid + + def is_pid_running_daemon(self, pid): + ''' + Returns true iff the given pid refers to a running boothd process. + ''' + + path = "/proc/%s" % pid + pid_running = os.path.isdir(path) + + # print "======" + # import subprocess + # print subprocess.check_output(['lsof', '-p', pid]) + # print subprocess.check_output(['ls', path]) + # print subprocess.check_output(['cat', "/proc/%s/cmdline" % pid]) + # print "======" + + if not pid_running: + return False + + c = open("/proc/%s/cmdline" % pid) + cmdline = "".join(c.readlines()) + print cmdline + c.close() + + if cmdline.find('boothd') == -1: + print 'no boothd in cmdline:', cmdline + return False + + # self.assertRegexpMatches( + # cmdline, + # 'boothd', + # "lock file should refer to pid of running boothd" + # ) + + return True diff --git a/test/servertests.py b/test/servertests.py new file mode 100755 index 0000000..1e61393 --- /dev/null +++ b/test/servertests.py @@ -0,0 +1,34 @@ +#!/usr/bin/python + +import copy +from pprint import pprint, pformat +import re + +from serverenv import ServerTestEnvironment + +class ServerTests(ServerTestEnvironment): + # We don't know enough about the build/test system to rely on the + # existence, permissions, contents of the default config file. So + # we can't predict (and hence test) how booth will behave when -c + # is not specified. + # + # def test_no_args(self): + # # If do_server() called lockfile() first then this would be + # # the appropriate test: + # #self.assertLockFileError(lock_file=False) + # + # # If do_server() called setup() first, and the default + # # config file was readable non-root, then this would be the + # # appropriate test: + # self.configFileMissingMyIP(lock_file=False) + # + # def test_custom_lock_file(self): + # (pid, ret, stdout, stderr, runner) = self.run_booth(expected_exitcode=1) + # self.assertRegexpMatches( + # stderr, + # 'failed to open %s: ' % runner.config_file_used(), + # 'should fail to read default config file' + # ) + + def test_example_config(self): + self.configFileMissingMyIP(config_file=self.example_config_path) diff --git a/test/sitetests.py b/test/sitetests.py new file mode 100755 index 0000000..dfdf6b9 --- /dev/null +++ b/test/sitetests.py @@ -0,0 +1,6 @@ +#!/usr/bin/python + +from servertests import ServerTests + +class SiteConfigTests(ServerTests): + mode = 'site' diff --git a/test/utils.py b/test/utils.py new file mode 100755 index 0000000..6704e01 --- /dev/null +++ b/test/utils.py @@ -0,0 +1,8 @@ +#!/usr/bin/python + +import subprocess + +def run_cmd(cmd): + p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + (stdout, stderr) = p.communicate() + return (stdout, stderr, p.returncode)