Page MenuHomeClusterLabs Projects

No OneTemporary

diff --git a/configure.ac b/configure.ac
index 61f85ee..2748846 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1,516 +1,516 @@
# -*- Autoconf -*-
# Process this file with autoconf to produce a configure script.
# bootstrap / init
AC_PREREQ([2.61])
AC_INIT([booth],
m4_esyscmd([build-aux/git-version-gen --fallback 1.0 .tarball-version .gitarchivever]),
[users@clusterlabs.org])
AC_USE_SYSTEM_EXTENSIONS
AM_INIT_AUTOMAKE([-Wno-portability subdir-objects])
AC_CONFIG_MACRO_DIR([build-aux])
AC_CONFIG_SRCDIR([src/main.c])
AC_CONFIG_HEADER([src/b_config.h src/booth_config.h])
AC_CANONICAL_HOST
AC_LANG([C])
AC_SUBST(WITH_LIST, [""])
dnl Fix default variables - "prefix" variable if not specified
if test "$prefix" = "NONE"; then
prefix="/usr"
dnl Fix "localstatedir" variable if not specified
if test "$localstatedir" = "\${prefix}/var"; then
localstatedir="/var"
fi
dnl Fix "sysconfdir" variable if not specified
if test "$sysconfdir" = "\${prefix}/etc"; then
sysconfdir="/etc"
fi
dnl Fix "libdir" variable if not specified
if test "$libdir" = "\${exec_prefix}/lib"; then
if test -e /usr/lib64; then
libdir="/usr/lib64"
else
libdir="/usr/lib"
fi
fi
fi
AC_MSG_NOTICE(Sanitizing exec_prefix: ${exec_prefix})
case $exec_prefix in
dnl For consistency with Corosync, map NONE->$prefix
NONE) exec_prefix=$prefix;;
prefix) exec_prefix=$prefix;;
esac
# Checks for programs.
# check stolen from gnulib/m4/gnu-make.m4
if ! ${MAKE-make} --version /cannot/make/this >/dev/null 2>&1; then
AC_MSG_ERROR([you don't seem to have GNU make; it is required])
fi
AC_PROG_CC
AC_PROG_INSTALL
AC_PROG_LN_S
AC_PROG_MAKE_SET
AC_PROG_RANLIB
PKG_PROG_PKG_CONFIG
AC_PATH_PROGS(ASCIIDOC, asciidoc)
AC_PATH_PROGS(ASCIIDOCTOR, asciidoctor)
AC_PATH_PROGS(A2X, a2x)
AM_CONDITIONAL(IS_ASCIIDOC, test x"${ASCIIDOC}" != x"")
AM_CONDITIONAL(IS_A2X, test x"${A2X}" != x"")
AM_CONDITIONAL(BUILD_ASCIIDOC, test x"${A2X}" != x"" || test x"${ASCIIDOCTOR}" != x"")
# libgcrypt or mhash for hmac
libgcrypt_installed="yes"
AC_CHECK_HEADERS(gcrypt.h, , [libgcrypt_installed="no"],)
AC_CHECK_LIB(gcrypt, gcry_md_open, , [libgcrypt_installed="no"])
AM_CONDITIONAL(BUILD_AUTH_C, test "x${libgcrypt_installed}" = "xyes")
if test "x$libgcrypt_installed" = "xno"; then
mhash_installed="yes"
AC_CHECK_HEADERS(mhash.h, , [mhash_installed="no"],)
AC_CHECK_LIB(mhash, mhash_init, , [mhash_installed="no"])
AM_CONDITIONAL(BUILD_AUTH_C, test "x${mhash_installed}" = "xyes")
fi
AC_CHECK_LIB([xml2], xmlReadDoc)
PKG_CHECK_MODULES(XML, [libxml-2.0])
PKG_CHECK_MODULES(GLIB, [glib-2.0])
PKG_CHECK_MODULES([PCMK], [pacemaker-service],,
[PKG_CHECK_MODULES([PCMK], [pcmk-service])])
# Python casing, prefer 3.3+ to 2.{6...}
if test "x$PYTHON" = "x"; then
AM_PATH_PYTHON([3.3],, [AM_PATH_PYTHON([2.6])])
else
# Just set Automake variables (mainly PYTHON_VERSION)
AM_PATH_PYTHON
fi
PYTHON_SHEBANG="$PYTHON ${PYTHON_OPTS--Es}"
AC_SUBST([PYTHON_SHEBANG])
AM_CONDITIONAL(PYTHON_IS_VERSION3, test "x${PYTHON_VERSION%%.*}" = "x3")
# Checks for header files.
AC_FUNC_ALLOCA
AC_HEADER_DIRENT
AC_HEADER_STDC
AC_HEADER_SYS_WAIT
AC_CHECK_HEADERS([arpa/inet.h fcntl.h limits.h netdb.h netinet/in.h stdint.h \
stdlib.h string.h sys/ioctl.h sys/param.h sys/socket.h \
sys/time.h syslog.h unistd.h sys/types.h getopt.h malloc.h \
sys/sockio.h utmpx.h])
AC_CHECK_HEADERS(heartbeat/glue_config.h)
AC_CHECK_HEADERS(mhash.h)
AC_CHECK_HEADER([zlib.h],
[AC_SUBST(ZLIB_LIBS, ["-lz"])],
[AC_MSG_ERROR([zlib development files required])])
saved_CPPFLAGS="${CPPFLAGS}"
CPPFLAGS="${CPPFLAGS} ${PCMK_CFLAGS} ${GLIB_CFLAGS}"
AC_CHECK_HEADER([crm/services.h], [],
[AC_MSG_ERROR([crm/services.h header required])])
CPPFLAGS="${saved_CPPFLAGS}"
# Checks for typedefs, structures, and compiler characteristics.
AC_C_CONST
AC_TYPE_UID_T
AC_C_INLINE
AC_TYPE_INT16_T
AC_TYPE_INT32_T
AC_TYPE_INT64_T
AC_TYPE_INT8_T
AC_TYPE_SIZE_T
AC_TYPE_SSIZE_T
AC_HEADER_TIME
AC_TYPE_UINT16_T
AC_TYPE_UINT32_T
AC_TYPE_UINT64_T
AC_TYPE_UINT8_T
AC_C_VOLATILE
# Checks for library functions.
AC_FUNC_CLOSEDIR_VOID
AC_FUNC_ERROR_AT_LINE
AC_REPLACE_FNMATCH
AC_FUNC_FORK
AC_PROG_GCC_TRADITIONAL
AC_FUNC_MALLOC
AC_FUNC_MEMCMP
AC_FUNC_REALLOC
AC_FUNC_SELECT_ARGTYPES
AC_TYPE_SIGNAL
AC_FUNC_VPRINTF
AC_CHECK_FUNCS([alarm alphasort atexit bzero dup2 endgrent endpwent fcntl \
- getcwd getpeerucred getpeereid gettimeofday inet_ntoa memmove \
+ getcwd getpeerucred getpeereid gettimeofday memmove \
memset mkdir scandir select socket strcasecmp strchr strdup \
strerror strrchr strspn strstr \
sched_get_priority_max sched_setscheduler])
AC_CONFIG_FILES([Makefile
booth.pc
src/Makefile
docs/Makefile
conf/Makefile])
AC_CONFIG_FILES([conf/booth-arbitrator.service conf/booth@.service])
AC_CONFIG_FILES([script/unit-test.py])
AC_CONFIG_FILES([script/service-runnable], [chmod +x script/service-runnable])
# ===============================================
# Helpers
# ===============================================
## PKG_CHECK_VAR wrapper that allows defining a default value
## when value from pkg-config is not detected
AC_DEFUN([BOOTH_PKG_CHECK_VAR], [
varname=$1
default=$4
AC_MSG_CHECKING([for pkg-conf $2 var $3])
PKG_CHECK_VAR([$1], [$2], [$3])
AS_VAR_IF([$1], [""],
[AS_VAR_IF([default], [""],
AC_MSG_ERROR([not found]),
[AS_VAR_COPY([$varname], [default]) && AC_MSG_RESULT([not found, using default ${!varname}])])],
[AC_MSG_RESULT([yes (detected: ${!varname})])])
])
## helper for CC stuff
cc_supports_flag() {
local CFLAGS="-Werror $@"
AC_MSG_CHECKING(whether $CC supports "$@")
AC_COMPILE_IFELSE([AC_LANG_SOURCE([[int main(){return 0;}]])],
[RC=0; AC_MSG_RESULT(yes)],[RC=1; AC_MSG_RESULT(no)])
return $RC
}
## local defines
PACKAGE_FEATURES=""
LINT_FLAGS="-weak -unrecog +posixlib +ignoresigns -fcnuse \
-badflag -D__gnuc_va_list=va_list -D__attribute\(x\)="
# local options
AC_ARG_ENABLE([fatal-warnings],
[ --enable-fatal-warnings : enable fatal warnings. ],
[ default="no" ])
AC_ARG_ENABLE([debug],
[ --enable-debug : enable debug build. ],
[ default="no" ])
AC_ARG_ENABLE([user-flags],
[ --enable-user-flags : rely on user environment. ],
[ default="no" ])
AC_ARG_ENABLE([coverage],
[ --enable-coverage : coverage analysis of the codebase. ],
[ default="no" ])
AC_ARG_WITH([initddir],
[ --with-initddir=DIR : path to init script directory. ],
[ INITDDIR="$withval" ],
[ INITDDIR="$sysconfdir/init.d" ])
AC_ARG_WITH([html_man],
[ --with-html_man : Enable generating man pages in HTML.])
AM_CONDITIONAL(BUILD_ASCIIDOC_HTML_MAN,
(test "x${ASCIIDOC}" != "x" || test x"${ASCIIDOCTOR}" != x"") && test "x$with_html_man" = "xyes")
AC_ARG_WITH([glue],
[ --without-glue : Avoid libraries from (cluster-)glue project.],
[],
[with_glue=yes])
AC_ARG_WITH([run_build_tests],
[ --with-run-build-tests : Enable running build tests when generating RPM],
[run_build_tests=yes],
[])
AM_CONDITIONAL([RUN_BUILD_TESTS], [test "x$run_build_tests" = "xyes"])
# figure out ocfdir automatically and allow manual override (mostly for CI)
BOOTH_PKG_CHECK_VAR([OCFROOT], [resource-agents], [ocfrootdir], [/usr/lib/ocf])
AC_ARG_WITH([ocfdir],
[ --with-ocfdir=DIR : path to ocfdir (default: autodetected). ],
[ ocfdir="$withval" ],
[ ocfdir="$OCFROOT" ])
AC_SUBST([ocfdir])
# figure out logging provider
logging_provider=""
if test "x$logging_provider" = "x" && test "x$with_glue" = "xyes"; then
AC_CHECK_LIB([plumb], [cl_log], [logging_provider="libplumb"])
fi
if test "x$logging_provider" = "x" && test "x$with_glue" = "xno"; then
PKG_CHECK_MODULES([LIBQB], [libqb])
AC_DEFINE([LOGGING_LIBQB], [], [use libqb as a logging provider])
PKG_CHECK_MODULES([LIBQB1], [libqb >= 1.0],
[AC_DEFINE([LOGGING_LIBQB_MAJOR], [1],
[libqb major version lower bound])],
[AC_MSG_WARN([[syslog identifier will not get changed]])])
logging_provider=libqb
fi
case "$logging_provider" in
libplumb)
LOGGER="ha_logger"
;;
libqb)
LOGGER="logger -t booth-script"
;;
*)
AC_MSG_ERROR([logging provider required (libplumb, or libqb when --without-glue)])
;;
esac
AM_CONDITIONAL([LOGGING_LIBQB], [test "x$logging_provider" = "xlibqb"])
AC_SUBST([LOGGER])
# figure out range2random provider
range2random_provider=""
if test "x$range2random_provider" = "x" && test "x$with_glue" = "xyes"; then
AC_CHECK_LIB([plumb], [get_next_random], [range2random_provider="libplumb"])
AC_CHECK_DECL([cl_rand_from_interval], [], [range2random_provider=""],
[#include <clplumbing/cl_random.h>])
fi
if test "x$range2random_provider" = "x" && test "x$with_glue" = "xno"; then
AC_CHECK_LIB([glib-2.0], [g_random_int_range], [range2random_provider="glib"])
fi
case "$range2random_provider" in
libplumb)
;;
glib)
PKG_CHECK_MODULES([GLIB], [glib-2.0])
AC_DEFINE([RANGE2RANDOM_GLIB], [], [use glib as a range2random provider])
;;
*)
AC_MSG_ERROR([range2random provider required (libplumb, or glib when --without-glue)])
;;
esac
AM_CONDITIONAL([RANGE2RANDOM_GLIB], [test "x$range2random_provider" = "xglib"])
# figure out nametag/distinguished-role provider
nametag_provider=""
if test "x$nametag_provider" = "x" && test "x$with_glue" != "xno"; then
AC_CHECK_LIB([plumbgpl], [set_proc_title], [nametag_provider="libplumbgpl"])
fi
if test "x$nametag_provider" = "x" && test "x$with_glue" = "xno"; then
AC_SEARCH_LIBS([sd_notify], [systemd systemd-daemon], [nametag_provider="libsystemd"])
fi
NOTIFY_ACCESS_SWITCH='# '
case "$nametag_provider" in
libplumbgpl)
;;
libsystemd)
PKG_CHECK_MODULES([LIBSYSTEMD], [libsystemd],, [
PKG_CHECK_MODULES([LIBSYSTEMD], [libsystemd-daemon])
])
AC_DEFINE([NAMETAG_LIBSYSTEMD], [], [use libsystemd as a nametag provider])
NOTIFY_ACCESS_SWITCH=
;;
*)
AC_MSG_ERROR([nametag provider required (libplumbgpl, or libsystemd when --without-glue)])
;;
esac
AM_CONDITIONAL([NAMETAG_LIBSYSTEMD], [test "x$nametag_provider" = "xlibsystemd"])
AC_SUBST([NOTIFY_ACCESS_SWITCH])
# figure out if "coredump nursing" supported and desired
coredump_nursing="no"
if test "x$with_glue" != "xno"; then
AC_CHECK_LIB([plumb], [cl_enable_coredumps], [coredump_nursing="libplumb"])
fi
if test "x$coredump_nursing" != "xno"; then
AC_DEFINE(COREDUMP_NURSING, [], [eligible for coredump nursing])
fi
AM_CONDITIONAL([COREDUMP_NURSING], [test "x$coredump_nursing" != "xno"])
# define CRM daemon user & group
BOOTH_PKG_CHECK_VAR([CRM_DAEMON_USER], [pacemaker], [daemon_user], [hacluster])
AC_DEFINE_UNQUOTED(CRM_DAEMON_USER,"$CRM_DAEMON_USER", User to run Booth daemon as)
BOOTH_PKG_CHECK_VAR([CRM_DAEMON_GROUP], [pacemaker], [daemon_group], [haclient])
AC_DEFINE_UNQUOTED(CRM_DAEMON_GROUP,"$CRM_DAEMON_GROUP", Group to run Booth daemon as)
# *FLAGS handling goes here
ENV_CFLAGS="$CFLAGS"
ENV_CPPFLAGS="$CPPFLAGS"
ENV_LDFLAGS="$LDFLAGS"
# debug build stuff
if test "x${enable_debug}" = xyes; then
AC_DEFINE_UNQUOTED([DEBUG], [1], [Compiling Debugging code])
OPT_CFLAGS="-O0 -U_FORTIFY_SOURCE"
PACKAGE_FEATURES="$PACKAGE_FEATURES debug"
else
OPT_CFLAGS="-O3"
fi
# gdb flags
if test "x${GCC}" = xyes; then
GDB_FLAGS="-ggdb3"
else
GDB_FLAGS="-g"
fi
dnl Check for POSIX clock_gettime
dnl
AC_CACHE_CHECK([have clock_gettime],ac_cv_HAVE_CLOCK_GETTIME,[
AC_TRY_COMPILE([
#include <time.h>
],
[ struct timespec tv; clock_gettime(CLOCK_REALTIME, &tv); return 0;],
ac_cv_HAVE_CLOCK_GETTIME=yes,ac_cv_HAVE_CLOCK_GETTIME=no,ac_cv_HAVE_CLOCK_GETTIME=cross)])
AM_CONDITIONAL(BUILD_TIMER_C, test x"$ac_cv_HAVE_CLOCK_GETTIME" = x"yes")
# extra warnings
EXTRA_WARNINGS=""
WARNLIST="
all
shadow
missing-prototypes
missing-declarations
strict-prototypes
declaration-after-statement
pointer-arith
write-strings
bad-function-cast
missing-format-attribute
format=2
format-security
format-nonliteral
no-long-long
unsigned-char
gnu89-inline
no-strict-aliasing
"
for j in $WARNLIST; do
if cc_supports_flag -W$j; then
EXTRA_WARNINGS="$EXTRA_WARNINGS -W$j";
fi
done
if test "x${enable_coverage}" = xyes && \
cc_supports_flag -ftest-coverage && \
cc_supports_flag -fprofile-arcs ; then
AC_MSG_NOTICE([Enabling Coverage (enable -O0 by default)])
OPT_CFLAGS="-O0"
COVERAGE_CFLAGS="-ftest-coverage -fprofile-arcs"
COVERAGE_LDFLAGS="-ftest-coverage -fprofile-arcs"
PACKAGE_FEATURES="$PACKAGE_FEATURES coverage"
else
COVERAGE_CFLAGS=""
COVERAGE_LDFLAGS=""
fi
if test "x${enable_ansi}" = xyes && \
cc_supports_flag -std=iso9899:199409 ; then
AC_MSG_NOTICE([Enabling ANSI Compatibility])
ANSI_CPPFLAGS="-ansi -D_GNU_SOURCE -DANSI_ONLY"
PACKAGE_FEATURES="$PACKAGE_FEATURES ansi"
else
ANSI_CPPFLAGS=""
fi
if test "x${enable_fatal_warnings}" = xyes && \
cc_supports_flag -Werror ; then
AC_MSG_NOTICE([Enabling Fatal Warnings (-Werror)])
WERROR_CFLAGS="-Werror"
PACKAGE_FEATURES="$PACKAGE_FEATURES fatal-warnings"
else
WERROR_CFLAGS=""
fi
# don't add addtional cflags
if test "x${enable_user_flags}" = xyes; then
OPT_CFLAGS=""
GDB_FLAGS=""
EXTRA_WARNINGS=""
fi
# final build of *FLAGS
CFLAGS="$ENV_CFLAGS $OPT_CFLAGS $GDB_FLAGS $OS_CFLAGS \
$COVERAGE_CFLAGS $EXTRA_WARNINGS $WERROR_CFLAGS $NSS_CFLAGS"
CPPFLAGS="$ENV_CPPFLAGS $ANSI_CPPFLAGS $OS_CPPFLAGS $GLIB_CFLAGS $RESMON_CFLAGS $XML_CFLAGS"
LDFLAGS="$ENV_LDFLAGS $COVERAGE_LDFLAGS $OS_LDFLAGS"
LIBS="$LIBS $XML_LIBS"
# substitute what we need:
AC_SUBST([INITDDIR])
AC_SUBST([LINT_FLAGS])
BOOTH_LIB_DIR=${localstatedir}/lib/booth
BOOTH_CORE_DIR=${localstatedir}/lib/booth/cores
BOOTHSYSCONFDIR=${sysconfdir}/booth
AC_SUBST([HAVE_LOG_CIB_DIFF])
AC_SUBST([HAVE_XML_LOG_PATCHSET])
AC_SUBST([BOOTH_LIB_DIR])
AC_SUBST([BOOTH_CORE_DIR])
AC_SUBST([BOOTHSYSCONFDIR])
AC_DEFINE_UNQUOTED([BOOTH_LIB_DIR], "$(eval echo ${BOOTH_LIB_DIR})", [booth lib directory])
AC_DEFINE_UNQUOTED([BOOTH_CORE_DIR], "$(eval echo ${BOOTH_CORE_DIR})", [booth working directory])
AC_DEFINE_UNQUOTED([BOOTHSYSCONFDIR], "$(eval echo ${BOOTHSYSCONFDIR})", [booth config directory])
AC_DEFINE_UNQUOTED([PACKAGE_FEATURES], "${PACKAGE_FEATURES}", [booth built-in features])
AC_OUTPUT
AC_MSG_RESULT([])
AC_MSG_RESULT([$PACKAGE configuration:])
AC_MSG_RESULT([ Version = ${VERSION}])
AC_MSG_RESULT([ Prefix = ${prefix}])
AC_MSG_RESULT([ Executables = ${sbindir}])
AC_MSG_RESULT([ Man pages = ${mandir}])
AC_MSG_RESULT([ Doc dir = ${docdir}])
AC_MSG_RESULT([ Libraries = ${libdir}])
AC_MSG_RESULT([ Header files = ${includedir}])
AC_MSG_RESULT([ Arch-independent files = ${datadir}])
AC_MSG_RESULT([ State information = ${localstatedir}])
AC_MSG_RESULT([ System configuration = ${sysconfdir}])
AC_MSG_RESULT([ System init.d directory = ${INITDDIR}])
AC_MSG_RESULT([ booth config dir = ${BOOTHSYSCONFDIR}])
AC_MSG_RESULT([ ocf dir = ${ocfdir}])
AC_MSG_RESULT([ Features = ${PACKAGE_FEATURES}])
AC_MSG_RESULT([ Logging provider = ${logging_provider}])
AC_MSG_RESULT([ Range2random provider = ${range2random_provider}])
AC_MSG_RESULT([ Nametag provider = ${nametag_provider}])
AC_MSG_RESULT([ Coredump nursing = ${coredump_nursing}])
AC_MSG_RESULT([ Working directory = ${BOOTH_CORE_DIR}])
AC_MSG_RESULT([ HA group name = ${CRM_DAEMON_GROUP}])
AC_MSG_RESULT([ HA user name = ${CRM_DAEMON_USER}])
AC_MSG_RESULT([])
AC_MSG_RESULT([$PACKAGE build info:])
AC_MSG_RESULT([ Default optimization = ${OPT_CFLAGS}])
AC_MSG_RESULT([ Default debug options = ${GDB_CFLAGS}])
AC_MSG_RESULT([ Extra compiler warnings = ${EXTRA_WARNING}])
AC_MSG_RESULT([ Env. defined CFLAG = ${ENV_CFLAGS}])
AC_MSG_RESULT([ Env. defined CPPFLAGS = ${ENV_CPPFLAGS}])
AC_MSG_RESULT([ Env. defined LDFLAGS = ${ENV_LDFLAGS}])
AC_MSG_RESULT([ ANSI defined CPPFLAGS = ${ANSI_CPPFLAGS}])
AC_MSG_RESULT([ Coverage CFLAGS = ${COVERAGE_CFLAGS}])
AC_MSG_RESULT([ Coverage LDFLAGS = ${COVERAGE_LDFLAGS}])
AC_MSG_RESULT([ Fatal War. CFLAGS = ${WERROR_CFLAGS}])
AC_MSG_RESULT([ Final CFLAGS = ${CFLAGS}])
AC_MSG_RESULT([ Final CPPFLAGS = ${CPPFLAGS}])
AC_MSG_RESULT([ Final LDFLAGS = ${LDFLAGS}])
diff --git a/src/config.c b/src/config.c
index e3f0a90..8e41553 100644
--- a/src/config.c
+++ b/src/config.c
@@ -1,1026 +1,1055 @@
/*
* Copyright (C) 2011 Jiaju Zhang <jjzhang@suse.de>
* Copyright (C) 2013-2014 Philipp Marek <philipp.marek@linbit.com>
*
* 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 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 program; if not, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/
#include "b_config.h"
#include <stdio.h>
#include <ctype.h>
#include <stdlib.h>
#include <assert.h>
#include <zlib.h>
#include <sys/types.h>
#include <pwd.h>
#include <grp.h>
#include <errno.h>
#include <string.h>
#include <netdb.h>
#include "booth.h"
#include "config.h"
#include "raft.h"
#include "ticket.h"
#include "log.h"
static int ticket_size = 0;
static int ticket_realloc(void)
{
const int added = 5;
int had, want;
void *p;
had = booth_conf->ticket_allocated;
want = had + added;
p = realloc(booth_conf->ticket,
sizeof(struct ticket_config) * want);
if (!p) {
log_error("can't alloc more tickets");
return -ENOMEM;
}
booth_conf->ticket = p;
memset(booth_conf->ticket + had, 0,
sizeof(struct ticket_config) * added);
booth_conf->ticket_allocated = want;
return 0;
}
static void hostname_to_ip(char * hostname)
{
- struct hostent *he;
- struct in_addr **addr_list;
+ struct addrinfo hints;
+ struct addrinfo *result, *rp;
+ int res;
+ int addr_found = 0;
+ const char *ntop_res;
- if ((he = gethostbyname(hostname)) == NULL) {
+ memset(&hints, 0, sizeof(hints));
+ hints.ai_family = AF_UNSPEC;
+ hints.ai_socktype = SOCK_STREAM;
+
+ res = getaddrinfo(hostname, NULL, &hints, &result);
+
+ if (res != 0) {
log_error("can't find IP for the host \"%s\"", hostname);
return;
}
- addr_list = (struct in_addr **) he->h_addr_list;
+ /* Return the first found AF_INET or AF_INET6 address */
+ for (rp = result; rp && !addr_found; rp = rp->ai_next) {
+ if (rp->ai_family != AF_INET && rp->ai_family != AF_INET6) {
+ continue ;
+ }
- /* Return the first found address */
- if (addr_list[0] != NULL) {
- strncpy(hostname, inet_ntoa(*addr_list[0]), BOOTH_NAME_LEN - 1);
- /* buffer overflow will not happen (IPv6 notation < 63 chars),
- but suppress the warnings */
- hostname[BOOTH_NAME_LEN - 1] = '\0';
+ switch (rp->ai_family) {
+ case AF_INET:
+ ntop_res = inet_ntop(rp->ai_family,
+ &((struct sockaddr_in *)(rp->ai_addr))->sin_addr,
+ hostname, BOOTH_NAME_LEN - 1);
+ break;
+ case AF_INET6:
+ ntop_res = inet_ntop(rp->ai_family,
+ &((struct sockaddr_in6 *)(rp->ai_addr))->sin6_addr,
+ hostname, BOOTH_NAME_LEN - 1);
+ break;
+ }
+
+ if (ntop_res) {
+ /* buffer overflow will not happen (IPv6 notation < 63 chars),
+ but suppress the warnings */
+ hostname[BOOTH_NAME_LEN - 1] = '\0';
+ addr_found = 1;
+ }
}
- else {
+
+ if (!addr_found) {
log_error("no IP addresses found for the host \"%s\"", hostname);
}
+
+ freeaddrinfo(result);
}
static int add_site(char *addr_string, int type)
{
int rv;
struct booth_site *site;
uLong nid;
uint32_t mask;
int i;
rv = 1;
if (booth_conf->site_count == MAX_NODES) {
log_error("too many nodes");
goto out;
}
if (strnlen(addr_string, sizeof(booth_conf->site[0].addr_string))
>= sizeof(booth_conf->site[0].addr_string)) {
log_error("site address \"%s\" too long", addr_string);
goto out;
}
site = booth_conf->site + booth_conf->site_count;
site->family = AF_INET;
site->type = type;
/* buffer overflow will not hapen (we've already checked that
addr_string will fit incl. terminating '\0' above), but
suppress the warnings with copying everything but the boundary
byte, which is valid as-is, since this last byte will be safely
pre-zeroed from the struct booth_config initialization */
strncpy(site->addr_string, addr_string, sizeof(site->addr_string) - 1);
if (!(inet_pton(AF_INET, site->addr_string, &site->sa4.sin_addr) > 0) &&
!(inet_pton(AF_INET6, site->addr_string, &site->sa6.sin6_addr) > 0)) {
/* Not a valid address, so let us try to convert it into an IP address */
hostname_to_ip(site->addr_string);
}
site->index = booth_conf->site_count;
site->bitmask = 1 << booth_conf->site_count;
/* Catch site overflow */
assert(site->bitmask);
booth_conf->all_bits |= site->bitmask;
if (type == SITE)
booth_conf->sites_bits |= site->bitmask;
site->tcp_fd = -1;
booth_conf->site_count++;
rv = 0;
memset(&site->sa6, 0, sizeof(site->sa6));
nid = crc32(0L, NULL, 0);
/* Using the ASCII representation in site->addr_string (both sizeof()
* and strlen()) gives quite a lot of collisions; a brute-force run
* from 0.0.0.0 to 24.0.0.0 gives ~4% collisions, and this tends to
* increase even more.
* Whether there'll be a collision in real-life, with 3 or 5 nodes, is
* another question ... but for now get the ID from the binary
* representation - that had *no* collisions up to 32.0.0.0.
* Note that POSIX mandates inet_pton to arange the address pointed
* to by "dst" in network byte order, assuring little/big-endianess
* mutual compatibility. */
if (inet_pton(AF_INET,
site->addr_string,
&site->sa4.sin_addr) > 0) {
site->family = AF_INET;
site->sa4.sin_family = site->family;
site->sa4.sin_port = htons(booth_conf->port);
site->saddrlen = sizeof(site->sa4);
site->addrlen = sizeof(site->sa4.sin_addr);
site->site_id = crc32(nid, (void*)&site->sa4.sin_addr, site->addrlen);
} else if (inet_pton(AF_INET6,
site->addr_string,
&site->sa6.sin6_addr) > 0) {
site->family = AF_INET6;
site->sa6.sin6_family = site->family;
site->sa6.sin6_flowinfo = 0;
site->sa6.sin6_port = htons(booth_conf->port);
site->saddrlen = sizeof(site->sa6);
site->addrlen = sizeof(site->sa6.sin6_addr);
site->site_id = crc32(nid, (void*)&site->sa6.sin6_addr, site->addrlen);
} else {
log_error("Address string \"%s\" is bad", site->addr_string);
rv = EINVAL;
}
/* Make sure we will never collide with NO_ONE,
* or be negative (to get "get_local_id() < 0" working). */
mask = 1 << (sizeof(site->site_id)*8 -1);
assert(NO_ONE & mask);
site->site_id &= ~mask;
/* Test for collisions with other sites */
for(i=0; i<site->index; i++)
if (booth_conf->site[i].site_id == site->site_id) {
log_error("Got a site-ID collision. Please file a bug on https://github.com/ClusterLabs/booth/issues/new, attaching the configuration file.");
exit(1);
}
out:
return rv;
}
inline static char *skip_while_in(const char *cp, int (*fn)(int), const char *allowed)
{
/* strchr() returns a pointer to the terminator if *cp == 0. */
while (*cp &&
(fn(*cp) ||
strchr(allowed, *cp)))
cp++;
/* discard "const" qualifier */
return (char*)cp;
}
inline static char *skip_while(char *cp, int (*fn)(int))
{
while (fn(*cp))
cp++;
return cp;
}
inline static char *skip_until(char *cp, char expected)
{
while (*cp && *cp != expected)
cp++;
return cp;
}
static inline int is_end_of_line(char *cp)
{
char c = *cp;
return c == '\n' || c == 0 || c == '#';
}
static int add_ticket(const char *name, struct ticket_config **tkp,
const struct ticket_config *def)
{
int rv;
struct ticket_config *tk;
if (booth_conf->ticket_count == booth_conf->ticket_allocated) {
rv = ticket_realloc();
if (rv < 0)
return rv;
}
tk = booth_conf->ticket + booth_conf->ticket_count;
booth_conf->ticket_count++;
if (!check_max_len_valid(name, sizeof(tk->name))) {
log_error("ticket name \"%s\" too long.", name);
return -EINVAL;
}
if (find_ticket_by_name(name, NULL)) {
log_error("ticket name \"%s\" used again.", name);
return -EINVAL;
}
if (* skip_while_in(name, isalnum, "-/")) {
log_error("ticket name \"%s\" invalid; only alphanumeric names.", name);
return -EINVAL;
}
strcpy(tk->name, name);
tk->timeout = def->timeout;
tk->term_duration = def->term_duration;
tk->retries = def->retries;
memcpy(tk->weight, def->weight, sizeof(tk->weight));
tk->mode = def->mode;
if (tkp)
*tkp = tk;
return 0;
}
static int postproc_ticket(struct ticket_config *tk)
{
if (!tk)
return 1;
if (!tk->renewal_freq) {
tk->renewal_freq = tk->term_duration/2;
}
if (tk->timeout*(tk->retries+1) >= tk->renewal_freq) {
log_error("%s: total amount of time to "
"retry sending packets cannot exceed "
"renewal frequency "
"(%d*(%d+1) >= %d)",
tk->name, tk->timeout, tk->retries, tk->renewal_freq);
return 0;
}
return 1;
}
/* returns number of weights, or -1 on bad input. */
static int parse_weights(const char *input, int weights[MAX_NODES])
{
int i, v;
char *cp;
for(i=0; i<MAX_NODES; i++) {
/* End of input? */
if (*input == 0)
break;
v = strtol(input, &cp, 0);
if (input == cp) {
log_error("No integer weight value at \"%s\"", input);
return -1;
}
weights[i] = v;
while (*cp) {
/* Separator characters */
if (isspace(*cp) ||
strchr(",;:-+", *cp))
cp++;
/* Next weight */
else if (isdigit(*cp))
break;
/* Rest */
else {
log_error("Invalid character at \"%s\"", cp);
return -1;
}
}
input = cp;
}
/* Fill rest of vector. */
for(v=i; v<MAX_NODES; v++) {
weights[v] = 0;
}
return i;
}
/* returns TICKET_MODE_AUTO if failed to parse the ticket mode. */
static ticket_mode_e retrieve_ticket_mode(const char *input)
{
if (strcasecmp(input, "manual") == 0) {
return TICKET_MODE_MANUAL;
}
return TICKET_MODE_AUTO;
}
/* scan val for time; time is [0-9]+(ms)?, i.e. either in seconds
* or milliseconds
* returns -1 on failure, otherwise time in ms
*/
static long read_time(char *val)
{
long t;
char *ep;
t = strtol(val, &ep, 10);
if (ep == val) { /* matched none */
t = -1L;
} else if (*ep == '\0') { /* matched all */
t = t*1000L; /* in seconds, convert to ms */
} else if (strcmp(ep, "ms")) { /* ms not exactly matched */
t = -1L;
} /* otherwise, time in ms */
/* if second fractions configured, send finer resolution
* times (i.e. term_valid_for) */
if (t % 1000L) {
TIME_MULT = 1000;
}
return t;
}
/* make arguments for execv(2)
* tk_test.path points to the path
* tk_test.argv is argument vector (starts with the prog)
* (strtok pokes holes in the configuration parameter value, i.e.
* we don't need to allocate memory for arguments)
*/
static int parse_extprog(char *val, struct ticket_config *tk)
{
char *p;
int i = 0;
if (tk_test.path) {
free(tk_test.path);
}
if (!(tk_test.path = strdup(val))) {
log_error("out of memory");
return -1;
}
p = strtok(tk_test.path, " \t");
tk_test.argv[i++] = p;
do {
p = strtok(NULL, " \t");
if (i >= MAX_ARGS) {
log_error("too many arguments for the acquire-handler");
free(tk_test.path);
return -1;
}
tk_test.argv[i++] = p;
} while (p);
return 0;
}
struct toktab grant_type[] = {
{ "auto", GRANT_AUTO},
{ "manual", GRANT_MANUAL},
{ NULL, 0},
};
struct toktab attr_op[] = {
{"eq", ATTR_OP_EQ},
{"ne", ATTR_OP_NE},
{NULL, 0},
};
static int lookup_tokval(char *key, struct toktab *tab)
{
struct toktab *tp;
for (tp = tab; tp->str; tp++) {
if (!strcmp(tp->str, key))
return tp->val;
}
return 0;
}
/* attribute prerequisite
*/
static int parse_attr_prereq(char *val, struct ticket_config *tk)
{
struct attr_prereq *ap = NULL;
char *p;
ap = (struct attr_prereq *)calloc(1, sizeof(struct attr_prereq));
if (!ap) {
log_error("out of memory");
return -1;
}
p = strtok(val, " \t");
if (!p) {
log_error("not enough arguments to attr-prereq");
goto err_out;
}
ap->grant_type = lookup_tokval(p, grant_type);
if (!ap->grant_type) {
log_error("%s is not a grant type", p);
goto err_out;
}
p = strtok(NULL, " \t");
if (!p) {
log_error("not enough arguments to attr-prereq");
goto err_out;
}
if (!(ap->attr_name = strdup(p))) {
log_error("out of memory");
goto err_out;
}
p = strtok(NULL, " \t");
if (!p) {
log_error("not enough arguments to attr-prereq");
goto err_out;
}
ap->op = lookup_tokval(p, attr_op);
if (!ap->op) {
log_error("%s is not an attribute operation", p);
goto err_out;
}
p = strtok(NULL, " \t");
if (!p) {
log_error("not enough arguments to attr-prereq");
goto err_out;
}
if (!(ap->attr_val = strdup(p))) {
log_error("out of memory");
goto err_out;
}
tk->attr_prereqs = g_list_append(tk->attr_prereqs, ap);
if (!tk->attr_prereqs) {
log_error("out of memory");
goto err_out;
}
return 0;
err_out:
if (ap) {
if (ap->attr_val)
free(ap->attr_val);
if (ap->attr_name)
free(ap->attr_name);
free(ap);
}
return -1;
}
extern int poll_timeout;
int read_config(const char *path, int type)
{
char line[1024];
FILE *fp;
char *s, *key, *val, *end_of_key;
const char *error;
char *cp, *cp2;
int i;
int lineno = 0;
int got_transport = 0;
int min_timeout = 0;
struct ticket_config defaults = { { 0 } };
struct ticket_config *current_tk = NULL;
fp = fopen(path, "r");
if (!fp) {
log_error("failed to open %s: %s", path, strerror(errno));
return -1;
}
booth_conf = malloc(sizeof(struct booth_config)
+ TICKET_ALLOC * sizeof(struct ticket_config));
if (!booth_conf) {
fclose(fp);
log_error("failed to alloc memory for booth config");
return -ENOMEM;
}
memset(booth_conf, 0, sizeof(struct booth_config)
+ TICKET_ALLOC * sizeof(struct ticket_config));
ticket_size = TICKET_ALLOC;
booth_conf->proto = UDP;
booth_conf->port = BOOTH_DEFAULT_PORT;
booth_conf->maxtimeskew = BOOTH_DEFAULT_MAX_TIME_SKEW;
booth_conf->authkey[0] = '\0';
/* Provide safe defaults. -1 is reserved, though. */
booth_conf->uid = -2;
booth_conf->gid = -2;
strcpy(booth_conf->site_user, "hacluster");
strcpy(booth_conf->site_group, "haclient");
strcpy(booth_conf->arb_user, "nobody");
strcpy(booth_conf->arb_group, "nobody");
parse_weights("", defaults.weight);
defaults.clu_test.path = NULL;
defaults.clu_test.pid = 0;
defaults.clu_test.status = 0;
defaults.clu_test.progstate = EXTPROG_IDLE;
defaults.term_duration = DEFAULT_TICKET_EXPIRY;
defaults.timeout = DEFAULT_TICKET_TIMEOUT;
defaults.retries = DEFAULT_RETRIES;
defaults.acquire_after = 0;
defaults.mode = TICKET_MODE_AUTO;
error = "";
log_debug("reading config file %s", path);
while (fgets(line, sizeof(line), fp)) {
lineno++;
s = skip_while(line, isspace);
if (is_end_of_line(s) || *s == '#')
continue;
key = s;
/* Key */
end_of_key = skip_while_in(key, isalnum, "-_");
if (end_of_key == key) {
error = "No key";
goto err;
}
if (!*end_of_key)
goto exp_equal;
/* whitespace, and something else but nothing more? */
s = skip_while(end_of_key, isspace);
if (*s != '=') {
exp_equal:
error = "Expected '=' after key";
goto err;
}
s++;
/* It's my buffer, and I terminate if I want to. */
/* But not earlier than that, because we had to check for = */
*end_of_key = 0;
/* Value tokenizing */
s = skip_while(s, isspace);
switch (*s) {
case '"':
case '\'':
val = s+1;
s = skip_until(val, *s);
/* Terminate value */
if (!*s) {
error = "Unterminated quoted string";
goto err;
}
/* Remove and skip quote */
*s = 0;
s++;
if (*(s = skip_while(s, isspace)) && *s != '#') {
error = "Surplus data after value";
goto err;
}
*s = 0;
break;
case 0:
no_value:
error = "No value";
goto err;
break;
default:
val = s;
/* Rest of line. */
i = strlen(s);
/* i > 0 because of "case 0" above. */
while (i > 0 && isspace(s[i-1]))
i--;
s += i;
*s = 0;
}
if (val == s)
goto no_value;
if (strlen(key) > BOOTH_NAME_LEN
|| strlen(val) > BOOTH_NAME_LEN) {
error = "key/value too long";
goto err;
}
if (strcmp(key, "transport") == 0) {
if (got_transport) {
error = "config file has multiple transport lines";
goto err;
}
if (strcasecmp(val, "UDP") == 0)
booth_conf->proto = UDP;
else if (strcasecmp(val, "SCTP") == 0)
booth_conf->proto = SCTP;
else {
error = "invalid transport protocol";
goto err;
}
got_transport = 1;
continue;
}
if (strcmp(key, "port") == 0) {
booth_conf->port = atoi(val);
continue;
}
if (strcmp(key, "name") == 0) {
safe_copy(booth_conf->name,
val, BOOTH_NAME_LEN,
"name");
continue;
}
#if HAVE_LIBGCRYPT || HAVE_LIBMHASH
if (strcmp(key, "authfile") == 0) {
safe_copy(booth_conf->authfile,
val, BOOTH_PATH_LEN,
"authfile");
continue;
}
if (strcmp(key, "maxtimeskew") == 0) {
booth_conf->maxtimeskew = atoi(val);
continue;
}
#endif
if (strcmp(key, "site") == 0) {
if (add_site(val, SITE))
goto err;
continue;
}
if (strcmp(key, "arbitrator") == 0) {
if (add_site(val, ARBITRATOR))
goto err;
continue;
}
if (strcmp(key, "site-user") == 0) {
safe_copy(booth_conf->site_user, optarg, BOOTH_NAME_LEN,
"site-user");
continue;
}
if (strcmp(key, "site-group") == 0) {
safe_copy(booth_conf->site_group, optarg, BOOTH_NAME_LEN,
"site-group");
continue;
}
if (strcmp(key, "arbitrator-user") == 0) {
safe_copy(booth_conf->arb_user, optarg, BOOTH_NAME_LEN,
"arbitrator-user");
continue;
}
if (strcmp(key, "arbitrator-group") == 0) {
safe_copy(booth_conf->arb_group, optarg, BOOTH_NAME_LEN,
"arbitrator-group");
continue;
}
if (strcmp(key, "debug") == 0) {
if (type != CLIENT && type != GEOSTORE)
debug_level = max(debug_level, atoi(val));
continue;
}
if (strcmp(key, "ticket") == 0) {
if (current_tk && strcmp(current_tk->name, "__defaults__")) {
if (!postproc_ticket(current_tk)) {
goto err;
}
}
if (!strcmp(val, "__defaults__")) {
current_tk = &defaults;
} else if (add_ticket(val, &current_tk, &defaults)) {
goto err;
}
continue;
}
/* current_tk must be allocated at this point, otherwise
* we don't know to which ticket the key refers
*/
if (!current_tk) {
error = "Unexpected keyword";
goto err;
}
if (strcmp(key, "expire") == 0) {
current_tk->term_duration = read_time(val);
if (current_tk->term_duration <= 0) {
error = "Expected time >0 for expire";
goto err;
}
continue;
}
if (strcmp(key, "timeout") == 0) {
current_tk->timeout = read_time(val);
if (current_tk->timeout <= 0) {
error = "Expected time >0 for timeout";
goto err;
}
if (!min_timeout) {
min_timeout = current_tk->timeout;
} else {
min_timeout = min(min_timeout, current_tk->timeout);
}
continue;
}
if (strcmp(key, "retries") == 0) {
current_tk->retries = strtol(val, &s, 0);
if (*s || s == val ||
current_tk->retries<3 || current_tk->retries > 100) {
error = "Expected plain integer value in the range [3, 100] for retries";
goto err;
}
continue;
}
if (strcmp(key, "renewal-freq") == 0) {
current_tk->renewal_freq = read_time(val);
if (current_tk->renewal_freq <= 0) {
error = "Expected time >0 for renewal-freq";
goto err;
}
continue;
}
if (strcmp(key, "acquire-after") == 0) {
current_tk->acquire_after = read_time(val);
if (current_tk->acquire_after < 0) {
error = "Expected time >=0 for acquire-after";
goto err;
}
continue;
}
if (strcmp(key, "before-acquire-handler") == 0) {
if (parse_extprog(val, current_tk)) {
goto err;
}
continue;
}
if (strcmp(key, "attr-prereq") == 0) {
if (parse_attr_prereq(val, current_tk)) {
goto err;
}
continue;
}
if (strcmp(key, "mode") == 0) {
current_tk->mode = retrieve_ticket_mode(val);
continue;
}
if (strcmp(key, "weights") == 0) {
if (parse_weights(val, current_tk->weight) < 0)
goto err;
continue;
}
error = "Unknown keyword";
goto err;
}
fclose(fp);
if ((booth_conf->site_count % 2) == 0) {
log_warn("Odd number of nodes is strongly recommended!");
}
/* Default: make config name match config filename. */
if (!booth_conf->name[0]) {
cp = strrchr(path, '/');
cp = cp ? cp+1 : (char *)path;
cp2 = strrchr(cp, '.');
if (!cp2)
cp2 = cp + strlen(cp);
if (cp2-cp >= BOOTH_NAME_LEN) {
log_error("booth config file name too long");
goto out;
}
strncpy(booth_conf->name, cp, cp2-cp);
*(booth_conf->name+(cp2-cp)) = '\0';
}
if (!postproc_ticket(current_tk)) {
goto out;
}
poll_timeout = min(POLL_TIMEOUT, min_timeout/10);
if (!poll_timeout)
poll_timeout = POLL_TIMEOUT;
return 0;
err:
fclose(fp);
out:
log_error("%s in config file line %d",
error, lineno);
free(booth_conf);
booth_conf = NULL;
return -1;
}
int check_config(int type)
{
struct passwd *pw;
struct group *gr;
char *cp, *input;
if (!booth_conf)
return -1;
input = (type == ARBITRATOR)
? booth_conf->arb_user
: booth_conf->site_user;
if (!*input)
goto u_inval;
if (isdigit(input[0])) {
booth_conf->uid = strtol(input, &cp, 0);
if (*cp != 0) {
u_inval:
log_error("User \"%s\" cannot be resolved into a UID.", input);
return ENOENT;
}
}
else {
pw = getpwnam(input);
if (!pw)
goto u_inval;
booth_conf->uid = pw->pw_uid;
}
input = (type == ARBITRATOR)
? booth_conf->arb_group
: booth_conf->site_group;
if (!*input)
goto g_inval;
if (isdigit(input[0])) {
booth_conf->gid = strtol(input, &cp, 0);
if (*cp != 0) {
g_inval:
log_error("Group \"%s\" cannot be resolved into a UID.", input);
return ENOENT;
}
}
else {
gr = getgrnam(input);
if (!gr)
goto g_inval;
booth_conf->gid = gr->gr_gid;
}
return 0;
}
static int get_other_site(struct booth_site **node)
{
struct booth_site *n;
int i;
*node = NULL;
if (!booth_conf)
return 0;
for (i = 0; i < booth_conf->site_count; i++) {
n = booth_conf->site + i;
if (n != local && n->type == SITE) {
if (!*node) {
*node = n;
} else {
return 0;
}
}
}
return !*node ? 0 : 1;
}
int find_site_by_name(char *site, struct booth_site **node, int any_type)
{
struct booth_site *n;
int i;
if (!booth_conf)
return 0;
if (!strcmp(site, OTHER_SITE))
return get_other_site(node);
for (i = 0; i < booth_conf->site_count; i++) {
n = booth_conf->site + i;
if ((n->type == SITE || any_type) &&
strncmp(n->addr_string, site, sizeof(n->addr_string)) == 0) {
*node = n;
return 1;
}
}
return 0;
}
int find_site_by_id(uint32_t site_id, struct booth_site **node)
{
struct booth_site *n;
int i;
if (site_id == NO_ONE) {
*node = no_leader;
return 1;
}
if (!booth_conf)
return 0;
for (i = 0; i < booth_conf->site_count; i++) {
n = booth_conf->site + i;
if (n->site_id == site_id) {
*node = n;
return 1;
}
}
return 0;
}
const char *type_to_string(int type)
{
switch (type)
{
case ARBITRATOR: return "arbitrator";
case SITE: return "site";
case CLIENT: return "client";
case GEOSTORE: return "attr";
}
return "??invalid-type??";
}

File Metadata

Mime Type
text/x-diff
Expires
Sat, Jan 25, 5:32 AM (13 h, 50 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
1308535
Default Alt Text
(40 KB)

Event Timeline