From: Radosław Korzeniewski Date: Mon, 23 Nov 2020 12:02:23 +0000 (+0100) Subject: Plugins: Add a common Bacula FD Plugin library. X-Git-Tag: Release-11.3.2~752 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=6cd46a15d1d64fb91636e0ef47a12f83850baf41;p=thirdparty%2Fbacula.git Plugins: Add a common Bacula FD Plugin library. --- diff --git a/bacula/src/plugins/fd/pluginlib/Makefile.in b/bacula/src/plugins/fd/pluginlib/Makefile.in new file mode 100644 index 000000000..c061160f9 --- /dev/null +++ b/bacula/src/plugins/fd/pluginlib/Makefile.in @@ -0,0 +1,101 @@ +# +# Makefile for building FD plugins PluginLibrary for Bacula +# +# Copyright (C) 2000-2020 Kern Sibbald +# License: BSD 2-Clause; see file LICENSE-FOSS + +# Author: Radoslaw Korzeniewski, radekk@inteos.pl, Inteos Sp. z o.o. + +@MCOMMON@ + +# No optimization for now for easy debugging + +SRCDIR = ../../.. +FDDIR = $(SRCDIR)/filed +LIBDIR = $(SRCDIR)/lib +FINDLIBDIR = $(SRCDIR)/findlib + +topdir = @BUILD_DIR@ +thisdir = src/plugins/fd/pluginlib + +UNITTESTSOBJ = $(LIBDIR)/unittests.lo + +PLUGINLIBSSRC = pluginlib.cpp pluginlib.h +PLUGINLIBSOBJ = $(filter %.lo,$(PLUGINLIBSSRC:.cpp=.lo)) +ISO8601SRC = iso8601.cpp iso8601.h +ISO8601OBJ = $(filter %.lo,$(ISO8601SRC:.cpp=.lo)) +EXECPROGSRC = execprog.cpp execprog.h +EXECPROGOBJ = $(filter %.lo,$(EXECPROGSRC:.cpp=.lo)) +COMMCTXSRC = commctx.cpp commctx.h +COMMCTXOBJ = $(filter %.lo,$(COMMCTXSRC:.cpp=.lo)) + +PLUGINLIBSTEST = pluginlib_test.cpp $(PLUGINLIBSSRC) $(UNITTESTSOBJ) +PLUGINLIBSTESTOBJ = $(filter %.lo,$(PLUGINLIBSTEST:.cpp=.lo)) +ISO8601TEST = iso8601_test.cpp $(ISO8601SRC) $(UNITTESTSOBJ) +ISO8601TESTOBJ = $(filter %.lo,$(ISO8601TEST:.cpp=.lo)) + +COMMONPLUGINOBJ = $(PLUGINLIBSOBJ) $(ISO8601OBJ) $(EXECPROGOBJ) +COMMONPLUGINTESTS = pluginlib_test iso8601_test + +.SUFFIXES: .c .lo + +LIBBAC = -lbac -L$(LIBDIR)/.libs + +.c.lo: + @echo "Compiling $< ..." + $(NO_ECHO)$(LIBTOOL_COMPILE) $(CXX) $(DEFS) $(DEBUG) $(CPPFLAGS) $(CFLAGS) -I$(SRCDIR) -I$(FDDIR) -I$(LIBDIR) -I$(FINDLIBDIR) -I. -c $< + +.cpp.lo: + @echo "Compiling c++ $< ..." + $(NO_ECHO)$(LIBTOOL_COMPILE) $(CXX) $(DEFS) $(DEBUG) $(CPPFLAGS) $(CFLAGS) -I$(SRCDIR) -I$(FDDIR) -I$(LIBDIR) -I$(FINDLIBDIR) -I. -c $< + +all: $(COMMONPLUGINOBJ) $(COMMONPLUGINTESTS) + +$(LIBDIR)/unittests.lo: + $(MAKE) -C $(LIBDIR) unittests.lo + +pluginlib_test: Makefile $(PLUGINLIBSTESTOBJ) $(PLUGINLIBSSRC) + @echo "Building $@ ..." + $(NO_ECHO)$(LIBTOOL_LINK) --silent $(CXX) $(LDFLAGS) $(LIBCURL) $(LIBBAC) $(PLUGINLIBSTESTOBJ) -o $@ + +iso8601_test: Makefile $(ISO8601TESTOBJ) $(ISO8601SRC) + @echo "Building $@ ..." + $(NO_ECHO)$(LIBTOOL_LINK) --silent $(CXX) $(LDFLAGS) $(LIBCURL) $(LIBBAC) $(PLUGINLIBSTESTOBJ) -o $@ + +install: all + $(MKDIR) $(DESTDIR)$(plugindir) + $(LIBTOOL_INSTALL) $(INSTALL_PROGRAM) bpipe-fd.la $(DESTDIR)$(plugindir) + $(RMF) $(DESTDIR)$(plugindir)/bpipe-fd.la + +install-test-plugin: all + $(MKDIR) $(DESTDIR)$(plugindir) + $(LIBTOOL_INSTALL) $(INSTALL_PROGRAM) test-plugin-fd.la $(DESTDIR)$(plugindir) + $(RMF) $(DESTDIR)$(plugindir)/test-plugin-fd.la + $(LIBTOOL_INSTALL) $(INSTALL_PROGRAM) test-deltaseq-fd.la $(DESTDIR)$(plugindir) + $(RMF) $(DESTDIR)$(plugindir)/test-deltaseq-fd.la + $(LIBTOOL_INSTALL) $(INSTALL_PROGRAM) test-handlexacl-plugin-fd.la $(DESTDIR)$(plugindir) + $(RMF) $(DESTDIR)$(plugindir)/test-handlexacl-plugin-fd.la + +Makefile: Makefile.in $(topdir)/config.status + cd $(topdir) \ + && CONFIG_FILES=$(thisdir)/$@ CONFIG_HEADERS= $(SHELL) ./config.status + +libtool-clean: + @find . -name '*.lo' -print | xargs $(LIBTOOL_CLEAN) $(RMF) + @$(RMF) *.la + @$(RMF) -r .libs _libs + +clean: libtool-clean + @rm -f main *.so *.o 1 2 3 + @rm -f $(COMMONPLUGINTESTS) + +distclean: clean + @rm -f Makefile *.la *.lo + @rm -rf .libs + +libtool-uninstall: + $(LIBTOOL_UNINSTALL) $(RMF) $(DESTDIR)$(plugindir)/bpipe-fd.so + +uninstall: @LIBTOOL_UNINSTALL_TARGET@ + +depend: diff --git a/bacula/src/plugins/fd/pluginlib/pluginlib.cpp b/bacula/src/plugins/fd/pluginlib/pluginlib.cpp new file mode 100644 index 000000000..0982b19e8 --- /dev/null +++ b/bacula/src/plugins/fd/pluginlib/pluginlib.cpp @@ -0,0 +1,499 @@ +/* + Bacula® - The Network Backup Solution + + Copyright (C) 2007-2017 Bacula Systems SA + All rights reserved. + + The main author of Bacula is Kern Sibbald, with contributions from many + others, a complete list can be found in the file AUTHORS. + + Licensees holding a valid Bacula Systems SA license may use this file + and others of this release in accordance with the proprietary license + agreement provided in the LICENSE file. Redistribution of any part of + this release is not permitted. + + Bacula® is a registered trademark of Kern Sibbald. +*/ +/* + * + * All rights reserved. IP transferred to Bacula Systems according to agreement. + * + * Common definitions and utility functions for Inteos plugins. + * Functions defines a common framework used in our utilities and plugins. + * Author: Radosław Korzeniewski, radekk@inteos.pl, Inteos Sp. z o.o. + */ + +#include "pluginlib.h" + + +/* Events that are passed to plugin +typedef enum { + bEventJobStart = 1, + bEventJobEnd = 2, + bEventStartBackupJob = 3, + bEventEndBackupJob = 4, + bEventStartRestoreJob = 5, + bEventEndRestoreJob = 6, + bEventStartVerifyJob = 7, + bEventEndVerifyJob = 8, + bEventBackupCommand = 9, + bEventRestoreCommand = 10, + bEventEstimateCommand = 11, + bEventLevel = 12, + bEventSince = 13, + bEventCancelCommand = 14, + bEventVssBackupAddComponents = 15, + bEventVssRestoreLoadComponentMetadata = 16, + bEventVssRestoreSetComponentsSelected = 17, + bEventRestoreObject = 18, + bEventEndFileSet = 19, + bEventPluginCommand = 20, + bEventVssBeforeCloseRestore = 21, + bEventVssPrepareSnapshot = 22, + bEventOptionPlugin = 23, + bEventHandleBackupFile = 24, + bEventComponentInfo = 25 +} bEventType; +*/ + +const char *eventtype2str(bEvent *event){ + switch (event->eventType){ + case bEventJobStart: + return "bEventJobStart"; + case bEventJobEnd: + return "bEventJobEnd"; + case bEventStartBackupJob: + return "bEventStartBackupJob"; + case bEventEndBackupJob: + return "bEventEndBackupJob"; + case bEventStartRestoreJob: + return "bEventStartRestoreJob"; + case bEventEndRestoreJob: + return "bEventEndRestoreJob"; + case bEventStartVerifyJob: + return "bEventStartVerifyJob"; + case bEventEndVerifyJob: + return "bEventEndVerifyJob"; + case bEventBackupCommand: + return "bEventBackupCommand"; + case bEventRestoreCommand: + return "bEventRestoreCommand"; + case bEventEstimateCommand: + return "bEventEstimateCommand"; + case bEventLevel: + return "bEventLevel"; + case bEventSince: + return "bEventSince"; + case bEventCancelCommand: + return "bEventCancelCommand"; + case bEventVssBackupAddComponents: + return "bEventVssBackupAddComponents"; + case bEventVssRestoreLoadComponentMetadata: + return "bEventVssRestoreLoadComponentMetadata"; + case bEventVssRestoreSetComponentsSelected: + return "bEventVssRestoreSetComponentsSelected"; + case bEventRestoreObject: + return "bEventRestoreObject"; + case bEventEndFileSet: + return "bEventEndFileSet"; + case bEventPluginCommand: + return "bEventPluginCommand"; + case bEventVssBeforeCloseRestore: + return "bEventVssBeforeCloseRestore"; + case bEventVssPrepareSnapshot: + return "bEventVssPrepareSnapshot"; + case bEventOptionPlugin: + return "bEventOptionPlugin"; + case bEventHandleBackupFile: + return "bEventHandleBackupFile"; + case bEventComponentInfo: + return "bEventComponentInfo"; + default: + return "Unknown"; + } +} + + +/* + * Return the real size of the disk based on the size suffix. + * + * in: + * disksize - the numeric value of the disk size to compute + * suff - the suffix for a disksize value + * out: + * uint64_t - the size of the disk computed with suffix + */ +uint64_t pluglib_size_suffix(int disksize, char suff) +{ + uint64_t size; + + switch (suff){ + case 'G': + size = (uint64_t)disksize * 1024 * 1048576; + break; + case 'M': + size = (uint64_t)disksize * 1048576; + break; + case 'T': + size = (uint64_t)disksize * 1048576 * 1048576; + break; + case 'K': + case 'k': + size = (uint64_t)disksize * 1024; + break; + default: + size = disksize; + } + return size; +} + +/* + * Return the real size of the disk based on the size suffix. + * This version uses a floating point numbers (double) for computation. + * + * in: + * disksize - the numeric value of the disk size to compute + * suff - the suffix for a disksize value + * out: + * uint64_t - the size of the disk computed with suffix + */ +uint64_t pluglib_size_suffix(double disksize, char suff) +{ + uint64_t size; + + switch (suff){ + case 'G': + size = disksize * 1024.0 * 1048576.0; + break; + case 'M': + size = disksize * 1048576.0; + break; + case 'T': + size = disksize * 1048576.0 * 1048576.0; + break; + case 'K': + case 'k': + size = disksize * 1024.0; + break; + default: + size = disksize; + } + return size; +} + +/* + * Creates a path hierarchy on local FS. + * It is used for local restore mode to create a required directory. + * The functionality is similar to 'mkdir -p'. + * + * TODO: make a support for relative path + * TODO: check if we can use findlib/makepath implementation instead + * + * in: + * bpContext - for Bacula debug and jobinfo messages + * path - a full path to create, does not check if the path is relative, + * could fail in this case + * out: + * bRC_OK - path creation was successful + * bRC_Error - on any error + */ +bRC pluglib_mkpath(bpContext* ctx, char* path, bool isfatal) +{ +#ifdef PLUGINPREFIX +#define _OLDPREFIX PLUGINPREFIX +#endif +#define PLUGINPREFIX "pluglibmkpath:" + struct stat statp; + POOL_MEM dir(PM_FNAME); + char *p, *q; + + if (!path){ + return bRC_Error; + } + if (stat(path, &statp) == 0){ + if (S_ISDIR(statp.st_mode)){ + return bRC_OK; + } else { + DMSG(ctx, DERROR, "Path %s is not directory\n", path); + JMSG(ctx, isfatal ? M_FATAL : M_ERROR, "Path %s is not directory\n", path); + return bRC_Error; + } + } + DMSG(ctx, DDEBUG, "mkpath verify dir: %s\n", path); + pm_strcpy(dir, path); + p = dir.addr() + 1; + while (*p && (q = strchr(p, (int)PathSeparator)) != NULL){ + *q = 0; + DMSG(ctx, DDEBUG, "mkpath scanning(1): %s\n", dir.c_str()); + if (stat(dir.c_str(), &statp) == 0){ + *q = PathSeparator; + p = q + 1; + continue; + } + DMSG0(ctx, DDEBUG, "mkpath will create dir(1).\n"); + if (mkdir(dir.c_str(), 0750) < 0){ + /* error */ + berrno be; + DMSG2(ctx, DERROR, "Cannot create directory %s Err=%s\n", dir.c_str(), be.bstrerror()); + JMSG2(ctx, isfatal ? M_FATAL : M_ERROR, "Cannot create directory %s Err=%s\n", dir.c_str(), be.bstrerror()); + return bRC_Error; + } + *q = PathSeparator; + p = q + 1; + } + DMSG0(ctx, DDEBUG, "mkpath will create dir(2).\n"); + if (mkdir(path, 0750) < 0){ + /* error */ + berrno be; + DMSG2(ctx, DERROR, "Cannot create directory %s Err=%s\n", path, be.bstrerror()); + JMSG2(ctx, isfatal ? M_FATAL : M_ERROR, "Cannot create directory %s Err=%s\n", path, be.bstrerror()); + return bRC_Error; + } + DMSG0(ctx, DDEBUG, "mkpath finish.\n"); +#ifdef _OLDPREFIX +#define PLUGINPREFIX _OLDPREFIX +#undef _OLDPREFIX +#else +#undef PLUGINPREFIX +#endif + return bRC_OK; +} + +/** + * @brief + * + * @param str + * @param sep + * @return alist* + */ +alist * plugutil_str_split_to_alist(const char * str, const char sep) +{ + POOL_MEM buf(PM_NAME); + const char * p; + const char * q; + const char * s; + alist * list; + + if (str == NULL || strlen(str) == 0){ + return NULL; + } + + list = New(alist(5, true)); + p = str; + + do { + // search for separator char - sep + q = strchr(p, sep); + if (q == NULL){ + // copy whole string from p to buf + pm_strcpy(buf, p); + } else { + // copy string from p up to q + pm_memcpy(buf, p, q - p + 1); + buf.c_str()[q - p] = '\0'; + p = q + 1; // next element + } + // in buf we have splitted string part + s = bstrdup(buf.c_str()); + list->append((void*)s); + } while (q != NULL); + + return list; +} + +/* + * Render a xe tool parameter for string value. + * + * in: + * bpContext - for Bacula debug and jobinfo messages + * param - a pointer to the param variable where we will render a parameter + * pname - a name of the parameter to compare + * fmt - a low-level parameter name + * name - a name of the parameter from parameter list + * value - a value to render + * out: + * True if parameter was rendered + * False if it was not the parameter required + */ +bool render_param(POOLMEM **param, const char *pname, const char *fmt, const char *name, char *value) +{ + if (bstrcasecmp(name, pname)){ + if (!*param){ + *param = get_pool_memory(PM_NAME); + Mmsg(*param, " -%s '%s' ", fmt, value); + DMsg1(DDEBUG, "render param:%s\n", *param); + } + return true; + } + return false; +} + +/* + * Render a xe tool parameter for integer value. + * + * in: + * bpContext - for Bacula debug and jobinfo messages + * param - a pointer to the param variable where we will render a parameter + * pname - a name of the parameter to compare + * fmt - a low-level parameter name + * name - a name of the parameter from parameter list + * value - a value to render + * out: + * True if parameter was rendered + * False if it was not the parameter required + */ +bool render_param(POOLMEM **param, const char *pname, const char *fmt, const char *name, int value) +{ + if (bstrcasecmp(name, pname)){ + if (!*param){ + *param = get_pool_memory(PM_NAME); + Mmsg(*param, " -%s %d ", value); + DMsg1(DDEBUG, "render param:%s\n", *param); + } + return true; + } + return false; +} + +/** + * @brief + * + * @param param + * @param pname + * @param name + * @param value + * @return true + * @return false + */ +bool parse_param(POOL_MEM ¶m, const char *pname, const char *name, char *value) +{ + if (bstrcasecmp(name, pname)){ + pm_strcpy(param, value); + DMsg1(DDEBUG, "render param:%s\n", param.c_str()); + return true; + } + return false; +}; + +/* + * Setup XECOMMCTX parameter for boolean value. + * + * in: + * bpContext - for Bacula debug and jobinfo messages + * param - a pointer to the param variable where we will render a parameter + * pname - a name of the parameter to compare + * name - a name of the parameter from parameter list + * value - a value to render + * out: + * True if parameter was rendered + * False if it was not the parameter required + */ +bool render_param(bool ¶m, const char *pname, const char *name, bool value) +{ + if (bstrcasecmp(name, pname)){ + if (param){ + param = value; + DMsg2(DDEBUG, "render param: %s=%s\n", pname, param ? "True" : "False"); + } + return true; + } + return false; +} + +/* + * Setup XECOMMCTX parameter for boolean from string value. + * The parameter value will be false if value start with '0' character and + * will be true in any other case. So, when a plugin will have a following: + * param + * param=xxx + * param=1 + * then a param will be set to true. + * + * in: + * bpContext - for Bacula debug and jobinfo messages + * param - a pointer to the param variable where we will render a parameter + * pname - a name of the parameter to compare + * name - a name of the parameter from parameter list + * value - a value to render + * out: + * True if parameter was rendered + * False if it was not the parameter required + */ +bool parse_param(bool ¶m, const char *pname, const char *name, char *value) +{ + if (bstrcasecmp(name, pname)){ + if (value && *value == '0'){ + param = false; + } else { + param = true; + } + DMsg2(DINFO, "%s parameter: %s\n", name, param ? "True" : "False"); + return true; + } + return false; +} + +/* + * Setup Plugin parameter for integer from string value. + * + * in: + * param - a pointer to the param variable where we will render a parameter + * pname - a name of the parameter to compare + * name - a name of the parameter from parameter list + * value - a value to render + * out: + * True if parameter was parsed + * False if it was not the parameter required + */ +bool parse_param(int ¶m, const char *pname, const char *name, char *value, bool * err) +{ + // clear error flag when requested + if (err != NULL) *err = false; + + if (value && bstrcasecmp(name, pname)){ + /* convert str to integer */ + param = atoi(value); + if (param == 0){ + /* error in conversion */ + DMsg2(DERROR, "Invalid %s parameter: %s\n", name, value); + // setup error flag + if (err != NULL) *err = true; + return false; + } + DMsg2(DINFO, "%s parameter: %d\n", name, param); + + return true; + } + return false; +} + +/* + * Render and add a parameter for string value to alist. + * When alist is NULL (uninitialized) then it creates a new list to use. + * + * in: + * bpContext - for Bacula debug and jobinfo messages + * list - pointer to alist class to use + * pname - a name of the parameter to compare + * name - a name of the parameter from parameter list + * value - a value to render + * out: + * True if parameter was rendered + * False if it was not the parameter required + */ +bool add_param_str(alist **list, const char *pname, const char *name, char *value) +{ + POOLMEM *param; + + if (bstrcasecmp(name, pname)){ + if (!*list){ + *list = New(alist(8, not_owned_by_alist)); + } + param = get_pool_memory(PM_NAME); + Mmsg(param, "%s", value); + (*list)->append(param); + DMsg2(DDEBUG, "add param: %s=%s\n", name, value); + return true; + } + return false; +} diff --git a/bacula/src/plugins/fd/pluginlib/pluginlib.h b/bacula/src/plugins/fd/pluginlib/pluginlib.h new file mode 100644 index 000000000..b475374ad --- /dev/null +++ b/bacula/src/plugins/fd/pluginlib/pluginlib.h @@ -0,0 +1,167 @@ +/* + Bacula® - The Network Backup Solution + + Copyright (C) 2007-2017 Bacula Systems SA + All rights reserved. + + The main author of Bacula is Kern Sibbald, with contributions from many + others, a complete list can be found in the file AUTHORS. + + Licensees holding a valid Bacula Systems SA license may use this file + and others of this release in accordance with the proprietary license + agreement provided in the LICENSE file. Redistribution of any part of + this release is not permitted. + + Bacula® is a registered trademark of Kern Sibbald. +*/ +/* + * + * All rights reserved. IP transferred to Bacula Systems according to agreement. + * + * Common definitions and utility functions for Inteos plugins. + * Functions defines a common framework used in our utilities and plugins. + * Author: Radosław Korzeniewski, radekk@inteos.pl, Inteos Sp. z o.o. + */ + +#ifndef _PLUGINLIB_H_ +#define _PLUGINLIB_H_ + +#include +#include +#include + +#include "bacula.h" +#include "fd_plugins.h" + +/* Pointers to Bacula functions used in plugins */ +extern bFuncs *bfuncs; +extern bInfo *binfo; + +/* module definition */ +#ifndef PLUGMODULE +#define PLUGMODULE "PluginLib::" +#endif + +// #ifndef PLUGINPREFIX +// #define PLUGINPREFIX PLUGMODULE +// #endif + +/* size of different string or query buffers */ +#define BUFLEN 4096 +#define BIGBUFLEN 65536 + +/* debug and messages functions */ +#define JMSG0(ctx,type,msg) \ + if (ctx) bfuncs->JobMessage ( ctx, __FILE__, __LINE__, type, 0, PLUGINPREFIX " " msg ); +#define JMSG1 JMSG +#define JMSG(ctx,type,msg,var) \ + if (ctx) bfuncs->JobMessage ( ctx, __FILE__, __LINE__, type, 0, PLUGINPREFIX " " msg, var ); +#define JMSG2(ctx,type,msg,var1,var2) \ + if (ctx) bfuncs->JobMessage ( ctx, __FILE__, __LINE__, type, 0, PLUGINPREFIX " " msg, var1, var2 ); +#define JMSG3(ctx,type,msg,var1,var2,var3) \ + if (ctx) bfuncs->JobMessage ( ctx, __FILE__, __LINE__, type, 0, PLUGINPREFIX " " msg, var1, var2, var3 ); +#define JMSG4(ctx,type,msg,var1,var2,var3,var4) \ + if (ctx) bfuncs->JobMessage ( ctx, __FILE__, __LINE__, type, 0, PLUGINPREFIX " " msg, var1, var2, var3, var4 ); + +#define DMSG0(ctx,level,msg) \ + if (ctx) bfuncs->DebugMessage ( ctx, __FILE__, __LINE__, level, PLUGINPREFIX " " msg ); +#define DMSG1 DMSG +#define DMSG(ctx,level,msg,var) \ + if (ctx) bfuncs->DebugMessage ( ctx, __FILE__, __LINE__, level, PLUGINPREFIX " " msg, var ); +#define DMSG2(ctx,level,msg,var1,var2) \ + if (ctx) bfuncs->DebugMessage ( ctx, __FILE__, __LINE__, level, PLUGINPREFIX " " msg, var1, var2 ); +#define DMSG3(ctx,level,msg,var1,var2,var3) \ + if (ctx) bfuncs->DebugMessage ( ctx, __FILE__, __LINE__, level, PLUGINPREFIX " " msg, var1, var2, var3 ); +#define DMSG4(ctx,level,msg,var1,var2,var3,var4) \ + if (ctx) bfuncs->DebugMessage ( ctx, __FILE__, __LINE__, level, PLUGINPREFIX " " msg, var1, var2, var3, var4 ); +#define DMSG6(ctx,level,msg,var1,var2,var3,var4,var5,var6) \ + if (ctx) bfuncs->DebugMessage ( ctx, __FILE__, __LINE__, level, PLUGINPREFIX " " msg, var1, var2, var3, var4, var5, var6 ); + +/* fixed debug level definitions */ +#define D1 1 /* debug for every error */ +#define DERROR D1 +#define D2 10 /* debug only important stuff */ +#define DINFO D2 +#define D3 200 /* debug for information only */ +#define DDEBUG D3 +#define D4 800 /* debug for detailed information only */ +#define DVDEBUG D4 + +#define getBaculaVar(bvar,val) bfuncs->getBaculaValue(ctx, bvar, val); + +/* used for sanity check in plugin functions */ +#define ASSERT_CTX \ + if (!ctx || !ctx->pContext || !bfuncs) \ + { \ + return bRC_Error; \ + } + +/* defines for handleEvent */ +#define DMSG_EVENT_STR(event,value) DMSG2(ctx, DINFO, "%s value=%s\n", eventtype2str(event), NPRT((char *)value)); +#define DMSG_EVENT_CHAR(event,value) DMSG2(ctx, DINFO, "%s value='%c'\n", eventtype2str(event), (char)value); +#define DMSG_EVENT_LONG(event,value) DMSG2(ctx, DINFO, "%s value=%ld\n", eventtype2str(event), (intptr_t)value); +#define DMSG_EVENT_PTR(event,value) DMSG2(ctx, DINFO, "%s value=%p\n", eventtype2str(event), value); + +/* pure debug macros */ +#define DMsg0(level,msg) Dmsg1(level,PLUGMODULE "%s: " msg,__func__) +#define DMsg1(level,msg,a1) Dmsg2(level,PLUGMODULE "%s: " msg,__func__,a1) +#define DMsg2(level,msg,a1,a2) Dmsg3(level,PLUGMODULE "%s: " msg,__func__,a1,a2) +#define DMsg3(level,msg,a1,a2,a3) Dmsg4(level,PLUGMODULE "%s: " msg,__func__,a1,a2,a3) +#define DMsg4(level,msg,a1,a2,a3,a4) Dmsg5(level,PLUGMODULE "%s: " msg,__func__,a1,a2,a3,a4) + +#define BOOLSTR(b) (b?"True":"False") + +/* + * Common structure for key/pair values + */ +class key_pair : public SMARTALLOC +{ +public: + POOL_MEM key; + POOL_MEM value; + + key_pair() : key(PM_NAME), value(PM_MESSAGE) {}; + key_pair(const char *k, const char *v) + { + pm_strcpy(key, k); + pm_strcpy(value, v); + }; + ~key_pair() {}; +}; + +const char *eventtype2str(bEvent *event); +uint64_t pluglib_size_suffix(int disksize, char suff); +uint64_t pluglib_size_suffix(double disksize, char suff); +bRC pluglib_mkpath(bpContext* ctx, char* path, bool isfatal); + +/* + * Checks if plugin command points to our Plugin + * + * in: + * command - the plugin command used for backup/restore + * out: + * True - if it is our plugin command + * False - the other plugin command + */ +inline bool isourplugincommand(const char *pluginprefix, const char *command) +{ + /* check if it is our Plugin command */ + if (strncmp(pluginprefix, command, strlen(pluginprefix)) == 0){ + /* it is not our plugin prefix */ + return true; + } + return false; +} + +alist * plugutil_str_split_to_alist(const char * str, const char sep = '.'); + +/* plugin parameters manipulation */ +bool render_param(POOLMEM **param, const char *pname, const char *fmt, const char *name, char *value); +bool render_param(POOLMEM **param, const char *pname, const char *fmt, const char *name, int value); +bool render_param(bool ¶m, const char *pname, const char *name, bool value); +bool parse_param(bool ¶m, const char *pname, const char *name, char *value); +bool parse_param(int ¶m, const char *pname, const char *name, char *value, bool *err = NULL); +bool parse_param(POOL_MEM ¶m, const char *pname, const char *name, char *value); +bool add_param_str(alist **list, const char *pname, const char *name, char *value); + +#endif /* _PLUGINLIB_H_ */ \ No newline at end of file diff --git a/bacula/src/plugins/fd/pluginlib/pluginlib_test.cpp b/bacula/src/plugins/fd/pluginlib/pluginlib_test.cpp new file mode 100644 index 000000000..3b870aaf6 --- /dev/null +++ b/bacula/src/plugins/fd/pluginlib/pluginlib_test.cpp @@ -0,0 +1,75 @@ +/* + Bacula® - The Network Backup Solution + + Copyright (C) 2007-2017 Bacula Systems SA + All rights reserved. + + The main author of Bacula is Kern Sibbald, with contributions from many + others, a complete list can be found in the file AUTHORS. + + Licensees holding a valid Bacula Systems SA license may use this file + and others of this release in accordance with the proprietary license + agreement provided in the LICENSE file. Redistribution of any part of + this release is not permitted. + + Bacula® is a registered trademark of Kern Sibbald. +*/ +/* + * + * All rights reserved. IP transferred to Bacula Systems according to agreement. + * + * Common definitions and utility functions for Inteos plugins. + * Functions defines a common framework used in our utilities and plugins. + * Author: Radosław Korzeniewski, radekk@inteos.pl, Inteos Sp. z o.o. + */ + +#include "pluginlib.h" +#include "unittests.h" + +bFuncs *bfuncs; +bInfo *binfo; + +int main() +{ + Unittests pluglib_test("pluglib_test"); + alist * list; + char * s; + + // Pmsg0(0, "Initialize tests ...\n"); + + list = plugutil_str_split_to_alist("123456789"); + ok(list != NULL, "default split"); + ok(list->size() == 1, "expect single strings"); + foreach_alist(s, list){ + ok(strlen(s) == 9, "check element length"); + } + delete list; + + list = plugutil_str_split_to_alist("123.456"); + ok(list != NULL, "split: 123.456"); + ok(list->size() == 2, "expect two strings"); + foreach_alist(s, list){ + ok(strlen(s) == 3, "check element length"); + } + delete list; + + list = plugutil_str_split_to_alist("12345.56789.abcde"); + ok(list != NULL, "split: 12345.56789.abcde"); + ok(list->size() == 3, "expect three strings"); + foreach_alist(s, list){ + ok(strlen(s) == 5, "check element length"); + } + delete list; + + list = plugutil_str_split_to_alist("1.bacula..Eric.Kern"); + ok(list != NULL, "split: 1.bacula..Eric.Kern"); + ok(list->size() == 5, "expect three strings"); + ok(strcmp((char*)list->first(), "1") == 0, "check element 1"); + ok(strcmp((char*)list->next(), "bacula") == 0, "check element bacula"); + ok(strlen((char*)list->next()) == 0, "check empty element"); + ok(strcmp((char*)list->next(), "Eric") == 0, "check element Eric"); + ok(strcmp((char*)list->next(), "Kern") == 0, "check element Kern"); + delete list; + + return report(); +} diff --git a/bacula/src/plugins/fd/pluginlib/ptcomm.cpp b/bacula/src/plugins/fd/pluginlib/ptcomm.cpp new file mode 100644 index 000000000..943555eed --- /dev/null +++ b/bacula/src/plugins/fd/pluginlib/ptcomm.cpp @@ -0,0 +1,834 @@ +/* + Bacula® - The Network Backup Solution + + Copyright (C) 2007-2017 Bacula Systems SA + All rights reserved. + + The main author of Bacula is Kern Sibbald, with contributions from many + others, a complete list can be found in the file AUTHORS. + + Licensees holding a valid Bacula Systems SA license may use this file + and others of this release in accordance with the proprietary license + agreement provided in the LICENSE file. Redistribution of any part of + this release is not permitted. + + Bacula® is a registered trademark of Kern Sibbald. +*/ +/** + * @file ptcomm.cpp + * @author Radosław Korzeniewski (radoslaw@korzeniewski.net) + * @brief This is a Bacula plugin library for interfacing with Metaplugin backend. + * @version 2.0.0 + * @date 2020-11-20 + * + * @copyright Copyright (c) 2020 + */ + +#include "ptcomm.h" +#include +#include + +/* Plugin compile time variables required by pluglib */ +#define PLUGINPREFIX "ptcomm:" + +/* + * libbac uses its own sscanf implementation which is not compatible with + * libc implementation, unfortunately. + * use bsscanf for Bacula sscanf flavor + */ +#ifdef sscanf +#undef sscanf +#endif +// #define NEED_REVIEW + +/* from lib/scan.c */ +extern int parse_args(POOLMEM *cmd, POOLMEM **args, int *argc, + char **argk, char **argv, int max_args); + +/* + * Closes external pipe if available (opened). + * + * in: + * bpContext - for Bacula debug and jobinfo messages + * out: + * true - when closed without a problem + * false - when got any error + */ +bool PTCOMM::close_extpipe(bpContext *ctx) +{ + int rc; + + /* close expipe if used */ + if (extpipe > 0){ + rc = close(extpipe); + extpipe = -1; + if (rc != 0){ + berrno be; + DMSG(ctx, DERROR, "Cannot close ExtPIPE. Err=%s\n", be.bstrerror()); + JMSG(ctx, M_ERROR, "Cannot close ExtPIPE. Err=%s\n", be.bstrerror()); + return false; + } + } + return true; +} + +/* + * Terminate the connection represented by BPIPE object. + * it shows a debug and job messages when connection close is unsuccessful + * and when ctx is available only. + * + * in: + * bpContext - Bacula Plugin context required for debug/job messages to show, + * it could be NULL in this case no messages will be shown + * out: + * none + */ +void PTCOMM::terminate(bpContext *ctx) +{ + if (is_closed()) + return; + + pid_t worker_pid = bpipe->worker_pid; + int status = close_bpipe(bpipe); + + bpipe = NULL; // indicte closed bpipe + + if (status && ctx) + { + /* error during close */ + berrno be; + DMSG(ctx, DERROR, "Error closing backend. Err=%s\n", be.bstrerror(status)); + JMSG(ctx, M_ERROR, "Error closing backend. Err=%s\n", be.bstrerror(status)); + } + + if (worker_pid) + { + /* terminate the backend */ + kill(worker_pid, SIGTERM); + } + + if (extpipe > 0) + close_extpipe(ctx); +}; + +/** + * @brief Reads `nbytes` of data from backend into a buffer `buf`. + * + * This is a dedicated method for reading raw data from backend. + * It reads exact `nbytes` number of bytes and stores it at `buf`. + * It will not return until all requested data is ready or got error. + * You have to use it when you known exact number of bytes to read from + * the backend. The method handles errors and timeout reading data. + * + * @param ctx - for Bacula debug jobinfo messages + * @param buf - the memory buffer where we will read data + * @param nbytes - the exact number of bytes to read into `buf` + * @return true - when read was successful + * @return false - on any error + */ +bool PTCOMM::recvbackend_data(bpContext *ctx, char *buf, int32_t nbytes) +{ + int status; + int rbytes = 0; + + _timeout.tv_sec = PTCOMM_DEFAULT_TIMEOUT; + _timeout.tv_usec = 0; + + while (nbytes) + { + fd_set rfds; + + FD_ZERO(&rfds); + FD_SET(rfd, &rfds); + FD_SET(efd, &rfds); + + status = select(maxfd, &rfds, NULL, NULL, &_timeout); + if (status == 0) + { + // this means timeout waiting + f_error = true; + DMSG1(ctx, DERROR, "BPIPE read timeout=%d.\n", _timeout.tv_sec); + JMSG1(ctx, is_fatal() ? M_FATAL : M_ERROR, "BPIPE read timeout=%d.\n", _timeout.tv_sec); + return false; + } + + // check if any data on error channel + if (FD_ISSET(efd, &rfds)) + { + // do read of error channel + f_error = true; + status = read(efd, errmsg.c_str(), errmsg.size() - 1); + errmsg.c_str()[status] = '\0'; // terminate string + strip_trailing_junk(errmsg.c_str()); + if (status < 0) + { + /* show any error during message read */ + berrno be; + DMSG(ctx, DERROR, "BPIPE read error on error channel: ERR=%s\n", be.bstrerror()); + JMSG(ctx, is_fatal() ? M_FATAL : M_ERROR, "BPIPE read error on error channel: ERR=%s\n", be.bstrerror()); + } else { + // got data on error channel, report it + DMSG1(ctx, DERROR, "Backend reported error: %s\n", errmsg.c_str()); + JMSG1(ctx, is_fatal() ? M_FATAL : M_ERROR, "Backend reported error: %s\n", errmsg.c_str()); + } + } + + // check if data descriptor is ready + if (FD_ISSET(rfd, &rfds)) + { + // do read of data + status = read(rfd, buf + rbytes, nbytes); + if (status < 0) + { + /* show any error during data read */ + berrno be; + f_error = true; + DMSG(ctx, DERROR, "BPIPE read error: ERR=%s\n", be.bstrerror()); + JMSG(ctx, is_fatal() ? M_FATAL : M_ERROR, "BPIPE read error: ERR=%s\n", be.bstrerror()); + return false; + } + if (status == 0){ + /* the backend closed the connection without terminate signal 'T' */ + f_error = true; + DMSG0(ctx, DERROR, "Backend closed the connection.\n"); + JMSG0(ctx, is_fatal() ? M_FATAL : M_ERROR, "Backend closed the connection.\n"); + return false; + } + nbytes -= status; + rbytes += status; + } + } + + return true; +} + +/** + * @brief + * + * @param ctx + * @param buf + * @param nbytes + * @return true + * @return false + */ +bool PTCOMM::sendbackend_data(bpContext *ctx, POOLMEM *buf, int32_t nbytes) +{ + int status; + int wbytes = 0; + + _timeout.tv_sec = PTCOMM_DEFAULT_TIMEOUT; + _timeout.tv_usec = 0; + + while (nbytes) + { + fd_set rfds; + fd_set wfds; + + FD_ZERO(&rfds); + FD_ZERO(&wfds); + FD_SET(efd, &rfds); + FD_SET(wfd, &wfds); + + status = select(maxfd, &rfds, &wfds, NULL, &_timeout); + if (status == 0) + { + // this means timeout waiting + f_error = true; + DMSG1(ctx, DERROR, "BPIPE write timeout=%d.\n", _timeout.tv_sec); + JMSG1(ctx, is_fatal() ? M_FATAL : M_ERROR, "BPIPE write timeout=%d.\n", _timeout.tv_sec); + return false; + } + + // check if any data on error channel + if (FD_ISSET(efd, &rfds)) + { + // do read of error channel + f_error = true; + status = read(efd, errmsg.c_str(), errmsg.size()); + if (status < 0) + { + /* show any error during message read */ + berrno be; + DMSG(ctx, DERROR, "BPIPE read error on error channel: ERR=%s\n", be.bstrerror()); + JMSG(ctx, is_fatal() ? M_FATAL : M_ERROR, "BPIPE read error on error channel: ERR=%s\n", be.bstrerror()); + } else { + // got data on error channel, report it + DMSG1(ctx, DERROR, "Backend reported error: %s\n", errmsg.c_str()); + JMSG1(ctx, is_fatal() ? M_FATAL : M_ERROR, "Backend reported error: %s\n", errmsg.c_str()); + } + } + + // check if data descriptor is ready + if (FD_ISSET(wfd, &wfds)) + { + // do write of data + status = write(wfd, buf + wbytes, nbytes); + if (status < 0) + { + /* show any error during data write */ + berrno be; + f_error = true; + DMSG(ctx, DERROR, "BPIPE write error: ERR=%s\n", be.bstrerror()); + JMSG(ctx, is_fatal() ? M_FATAL : M_ERROR, "BPIPE write error: ERR=%s\n", be.bstrerror()); + return false; + } + nbytes -= status; + wbytes += status; + } + } + + return true; +} + +/** + * @brief Reads a protocol header from backend and return payload length. + * + * This method should be used at the start of every read from backend. + * It handles a full protocol chatting, i.e. error, warning and information + * messages besides EOD or termination. + * + * @param ctx - for Bacula debug jobinfo messages + * @param cmd - an expected command to read: `C` or `D` + * @return int32_t - the size of the packet payload + */ +int32_t PTCOMM::recvbackend_header(bpContext *ctx, char cmd) +{ + if (is_closed()){ + DMSG0(ctx, DERROR, "BPIPE to backend is closed, cannot receive data.\n"); + JMSG0(ctx, is_fatal() ? M_FATAL : M_ERROR, "BPIPE to backend is closed, cannot receive data.\n"); + return -1; + } + + PTHEADER header; + bool workdone = false; + + f_eod = f_error = f_fatal = false; + int32_t nbytes = sizeof(PTHEADER); + + while (!workdone){ + if (!recvbackend_data(ctx, (char*)&header, nbytes)) + { + DMSG0(ctx, DERROR, "PTCOMM cannot get packet header from backend.\n"); + JMSG0(ctx, M_FATAL, "PTCOMM cannot get packet header from backend.\n"); + f_eod = f_error = f_fatal = true; + return -1; + } + DMSG(ctx, DDEBUG, "RECV: %c\n", header.status); + + /* check for protocol status */ + if (header.status == 'F'){ + /* signal EOD */ + f_eod = true; + return 0; + } + + if (header.status == 'T'){ + /* backend signaled a connection termination */ + terminate(ctx); + return 0; + } + + // other packet commands require data + header.length[6] = 0; /* end of string */ + + // convert packet length from ASCII to binary + int32_t msglen = atoi(header.length); + + if (header.status == 'C' || header.status == 'D') + { + if (header.status != cmd) + { + DMSG2(ctx, DERROR, "Protocol error. Expected packet: %c got: %c\n", cmd, header.status); + JMSG2(ctx, M_FATAL, "Protocol error. Expected packet: %c got: %c\n", cmd, header.status); + return -1; + } + + // this means no additional handling required + return msglen; + } + + // need a space for nul and newline char at the end of the message + errmsg.check_size(msglen + 2); + + // read the rest of the package + if (!recvbackend_data(ctx, errmsg.c_str(), msglen)) + { + DMSG0(ctx, DERROR, "PTCOMM cannot get message from backend.\n"); + JMSG0(ctx, M_FATAL, "PTCOMM cannot get message from backend.\n"); + return -1; + } + + // ensure error message is terminated with newline and + // terminated with standard c-string nul + errmsg.c_str()[msglen] = errmsg.c_str()[msglen - 1] != '\n' ? '\n' : '\0'; + errmsg.c_str()[msglen + 1] = '\0'; + + switch (header.status) + { + /* backend signal errors */ + case 'E': + case 'A': + /* setup error flags */ + f_error = true; + f_fatal = header.status == 'A'; + + /* show error to Bacula */ + DMSG(ctx, DERROR, "Backend Error: %s", errmsg.c_str()); + JMSG(ctx, f_fatal ? M_FATAL : M_ERROR, "%s", errmsg.c_str()); + workdone = true; + break; + + // handle warning and info messages below + case 'W': + // handle warning message + DMSG(ctx, DERROR, "%s", errmsg.c_str()); + JMSG(ctx, M_WARNING, "%s", errmsg.c_str()); + continue; + + case 'I': + // handle information message + DMSG(ctx, DINFO, "%s", errmsg.c_str()); + JMSG(ctx, M_INFO, "%s", errmsg.c_str()); + continue; + + default: + DMSG1(ctx, DERROR, "Protocol error. Unknown packet: %c got: %c\n", header.status); + JMSG1(ctx, M_FATAL, "Protocol error. Unknown packet: %c got: %c\n", header.status); + return -1; + } + } + + return -1; +} + +/** + * @brief Handles a receive (read) packet header. + * + * @param ctx bpContext - for Bacula debug jobinfo messages + * @param cmd + * @return int32_t + */ +int32_t PTCOMM::handle_read_header(bpContext *ctx, char cmd) +{ + // first read is the packet header where we will have info about data + // which is sent to us; the packet header is 8 chars/bytes length fixed + // nbytes shows how many bytes we expects to read + int32_t length = recvbackend_header(ctx, cmd); + if (length < 0) + { + // error + DMSG0(ctx, DERROR, "PTCOMM cannot get packet header from backend.\n"); + JMSG0(ctx, is_fatal() ? M_FATAL : M_ERROR, "PTCOMM cannot get packet header from backend.\n"); + f_eod = f_error = f_fatal = true; + return -1; + } + + return length; +} + +/** + * @brief Handles a payload (message) which comes after the header. + * + * @param ctx bpContext - for Bacula debug jobinfo messages + * @param buf - the POOLMEM buffer we will read data + * @param nbytes - the size of the fized buffer + * @return int32_t + * 0: when backend sent signal, i.e. EOD or Term + * -1: when we've got any error; the function will report it to Bacula when + * ctx is not NULL + * : the size of received message + */ +int32_t PTCOMM::handle_payload(bpContext *ctx, char *buf, int32_t nbytes) +{ + // handle raw data read as payload + if(!recvbackend_data(ctx, buf, nbytes)) + { + // error + DMSG0(ctx, DERROR, "PTCOMM cannot get packet payload from backend.\n"); + JMSG0(ctx, is_fatal() ? M_FATAL : M_ERROR, "PTCOMM cannot get packet payload from backend.\n"); + f_eod = f_error = f_fatal = true; + return -1; + } + + return nbytes; +} + +/** + * @brief Receive a packet from the backend. + * + * The caller expects a packet of a particular type (`cmd`) and we return + * from function only when we will receive this kind of packet or get any error. + * The `buf` will be extended if message extent current buffer size. + * + * @param ctx bpContext - for Bacula debug jobinfo messages + * @param cmd the packet type expected + * @param buf the POOL_MEM buffer we will read data + * @return int32_t + * 0: when backend sent signal, i.e. EOD or Term + * -1: when we've got any error; the function will report it to Bacula when + * ctx is not NULL + * : the size of received message + */ +int32_t PTCOMM::recvbackend(bpContext *ctx, char cmd, POOL_MEM &buf) +{ + // handle header + int32_t length = handle_read_header(ctx, cmd); + if (length < 0) + return -1; + + // handle data payload + if (length > 0) + { + // check requested buffer size + buf.check_size(length); + return handle_payload(ctx, buf.c_str(), length); + } + + return 0; +} + +/** + * @brief Receive a packet from the backend. + * + * The caller expects a packet of a particular type (`cmd`) and we return + * from function when we will receive this kind of packet or get any error. + * The `buf` is fixed size, so it won't be extended for larger messages. + * In this case you have to make more calls to get all data. + * + * @param ctx bpContext - for Bacula debug jobinfo messages + * @param cmd - the packet type expected + * @param buf - the POOLMEM buffer we will read data + * @param bufsize - the size of the fized buffer + * @return int32_t + * 0: when backend sent signal, i.e. EOD or Term + * -1: when we've got any error; the function will report it to Bacula when + * ctx is not NULL + * : the size of received message + */ +int32_t PTCOMM::recvbackend_fixed(bpContext *ctx, char cmd, char *buf, int32_t bufsize) +{ + int32_t length = remaininglen; + + if (!f_cont) + { + // handle header + length = handle_read_header(ctx, cmd); + if (length < 0) + return -1; + } + + // handle data payload + if (length > 0) + { + // we will need subsequent call to handle remaining data only when `buf` to short + f_cont = length > bufsize; + int32_t nbytes = f_cont * bufsize + (!f_cont) * length; + remaininglen = f_cont * (length - bufsize); + return handle_payload(ctx, buf, nbytes); + } + + return 0; +} + +/* + * Sends packet to the backend. + * The protocol allows sending no more than 999999 bytes of data in one packet. + * If you require to send more data you have to split it in more packages, and + * backend has to assemble it into a larger chunk of data. + * + * in: + * bpContext - for Bacula debug and jobinfo messages + * cmd - the packet status to send + * buf - the packet contents + * len - the length of the contents + * out: + * -1 - when encountered any error + * - the number of bytes sent, success + */ +int32_t PTCOMM::sendbackend(bpContext *ctx, char cmd, POOLMEM *buf, int32_t len) +{ + int status; + PTHEADER *header; + PTHEADER myheader; + + if (is_closed()){ + DMSG0(ctx, DERROR, "BPIPE to backend is closed, cannot send data.\n"); + JMSG0(ctx, is_fatal() ? M_FATAL : M_ERROR, "BPIPE to backend is closed, cannot send data.\n"); + return -1; + } + + if (len > 999999){ + /* message length too long, cannot send it */ + DMSG(ctx, DERROR, "Message length %i too long, cannot send data.\n", len); + JMSG(ctx, M_FATAL, "Message length %i too long, cannot send data.\n", len); + return -1; + } + +#ifdef NEED_REVIEW + // The code at NEED_REVIEW uses POOLMEM abufhead reserved space for + // packet header rendering in the same way as bsock.c do. The code was tested + // and is working fine. No memory leakage or corruption encountered. + // The only pros for this code is a single fwrite call for a whole message + // instead of two fwrites (header + data) for a standard method. + if (buf){ + // we will prepare POOLMEM for sending data so we can render header here + header = (PTHEADER*) (buf - sizeof(PTHEADER)); + } else { + // we will send header only + header = &myheader; + } +#else + header = &myheader; +#endif + header->status = cmd; + DMSG2(ctx, DDEBUG, "SENT: %c %s\n", header->status, buf ? buf : ""); + if (bsnprintf(header->length, sizeof(PTHEADER), "%06i", len) != 6){ + /* problem rendering packet header */ + DMSG0(ctx, DERROR, "Problem rendering packet header for command.\n"); + JMSG0(ctx, M_FATAL, "Problem rendering packet header for command.\n"); + return -1; + } + header->length[6] = '\n'; + +#ifdef NEED_REVIEW + status = sendbackend_data(ctx, (char*)header, len + sizeof(PTHEADER)); + status -= sizeof(PTHEADER); +#else + status = write(wfd, header, sizeof(PTHEADER)); + if (buf){ + /* we have some data or command to send */ + status = write(wfd, buf, len); + } +#endif + if (status < 0) + { + // error + DMSG0(ctx, DERROR, "PTCOMM cannot write packet to backend.\n"); + JMSG0(ctx, is_fatal() ? M_FATAL : M_ERROR, "PTCOMM cannot write packet to backend.\n"); + f_eod = f_error = f_fatal = true; + return -1; + } + +#ifdef NEED_REVIEW + // correct real payload data size + status -= sizeof(PTHEADER); +#endif + + return status; +} + +/** + * @brief Reads the next command message from the backend communication channel. + * + * It expects the command, so returned data will be null terminated string + * stripped on any unwanted junk, i.e. '\n' or 'space'. + * + * @param ctx bpContext - for Bacula debug and jobinfo messages + * @param buf buffer allocated for command + * @return int32_t + * -1 - when encountered any error + * 0 - when backend sent signal, i.e. EOD or Term + * - the number of bytes received, success + */ +int32_t PTCOMM::read_command(bpContext *ctx, POOL_MEM &buf) +{ + int32_t status = recvbackend(ctx, 'C', buf); + if (status > 0) + { + /* mark end of string because every command is a string */ + buf.c_str()[status] = '\0'; + /* strip any junk in command like '\n' or trailing spaces */ + strip_trailing_junk(buf.c_str()); + } + + return status; +} + +/* + * Reads the next data message from the backend. + * The number of bytes received will not exceed the buffer length even when + * backend will send more data. In this case next call to read_data() will + * return the next part of the message. + * + * in: + * bpContext - for Bacula debug and jobinfo messages + * buf - buffer allocated for data + * len - the size of the allocated buffer + * out: + * -1 - when encountered any error + * 0 - when backend sent signal, i.e. EOD or Term + * - the number of bytes received, success + * buf - the command string received from backend + */ +int32_t PTCOMM::read_data(bpContext *ctx, POOL_MEM &buf) +{ + int32_t status; + + if (extpipe > 0){ + status = read(extpipe, buf.c_str(), buf.size()); + } else { + status = recvbackend(ctx, 'D', buf); + } + + return status; +} + +/* + * Reads the next data message from the backend. + * The number of bytes received will not exceed the buffer length even when + * backend will send more data. In this case next call to read_data() will + * return the next part of the message. + * + * in: + * bpContext - for Bacula debug and jobinfo messages + * buf - buffer allocated for data + * len - the size of the allocated buffer + * out: + * -1 - when encountered any error + * 0 - when backend sent signal, i.e. EOD or Term + * - the number of bytes received, success + * buf - the command string received from backend + */ +int32_t PTCOMM::read_data_fixed(bpContext *ctx, char *buf, int32_t len) +{ + int32_t status; + + if (extpipe > 0){ + status = read(extpipe, buf, len); + } else { + status = recvbackend_fixed(ctx, 'D', buf, len); + } + + return status; +} + +/* + * Receive an acknowledge from backend (the EOD package). + * + * in: + * bpContext - for Bacula debug and jobinfo messages + * out: + * True when acknowledge received + * False when got any error + */ +bool PTCOMM::read_ack(bpContext *ctx) +{ + POOL_MEM buf(PM_FNAME); + + if (recvbackend(ctx, 'F', buf) == 0 && f_eod) + { + f_eod = false; + return true; + } + + return false; +} + +/* + * Sends a command to the backend. + * The command has to be a nul terminated string. + * + * in: + * bpContext - for Bacula debug and jobinfo messages + * buf - a message buffer contains command to send + * out: + * -1 - when encountered any error + * - the number of bytes sent, success + */ +int32_t PTCOMM::write_command(bpContext *ctx, POOLMEM *buf) +{ + int32_t len; + len = buf ? strlen(buf) : 0; + return sendbackend(ctx, 'C', buf, len); +} + +/* + * Sends a raw data to backend. + * The length of the data should not exceed max packet size which is 999999 Byes. + * + * in: + * bpContext - for Bacula debug and jobinfo messages + * buf - a message buffer contains data to send + * len - the length of the data to send + * out: + * -1 - when encountered any error + * - the number of bytes sent, success + */ +int32_t PTCOMM::write_data(bpContext *ctx, POOLMEM *buf, int32_t len) +{ + int32_t status; + + if (extpipe > 0){ + status = write(extpipe, buf, len); + } else { + status = sendbackend(ctx, 'D', buf, len); + } + return status; +} + +/* + * Sends acknowledge to the backend which consist of the following flow: + * -> EOD + * <- OK + * or + * <- Error + * + * in: + * bpContext - for Bacula debug and jobinfo messages + * out: + * True when acknowledge sent successful + * False when got any error + */ +bool PTCOMM::send_ack(bpContext *ctx) +{ + POOL_MEM buf(PM_FNAME); + + if (signal_eod(ctx) < 0){ + // error + return false; + } + + if (read_command(ctx, buf) < 0){ + // error + return false; + } + + // check if backend response with OK + if (bstrcmp(buf.c_str(), "OK")){ + // great ACk confirmed + return true; + } + + return false; +} + +/** + * @brief Send a handshake procedure to the backend using PLUGINNAME and PLUGINAPI. + * + * @param ctx bpContext - for Bacula debug and jobinfo messages + * @param pluginname - the plugin name part of the handshake + * @param pluginapi - the protocol version of the plugin + * @return true - when handshake successful + * @return false - when not + */ +bool PTCOMM::handshake(bpContext *ctx, const char *pluginname, const char * pluginapi) +{ + POOL_MEM cmd(PM_FNAME); + + Mmsg(cmd, "Hello %s %s\n", pluginname, pluginapi); + int32_t status = write_command(ctx, cmd); + if (status > 0){ + status = read_command(ctx, cmd); + if (status > 0){ + if (bstrcmp(cmd.c_str(), "Hello Bacula")){ + /* handshake successful */ + return true; + } else { + DMSG(ctx, DERROR, "Wrong backend response to Hello command, got: %s\n", cmd.c_str()); + JMSG(ctx, is_fatal() ? M_FATAL : M_ERROR, "Wrong backend response to Hello command, got: %s\n", cmd.c_str()); + } + } + } + + return false; +} diff --git a/bacula/src/plugins/fd/pluginlib/ptcomm.h b/bacula/src/plugins/fd/pluginlib/ptcomm.h new file mode 100644 index 000000000..515c68093 --- /dev/null +++ b/bacula/src/plugins/fd/pluginlib/ptcomm.h @@ -0,0 +1,286 @@ +/* + Bacula® - The Network Backup Solution + + Copyright (C) 2007-2017 Bacula Systems SA + All rights reserved. + + The main author of Bacula is Kern Sibbald, with contributions from many + others, a complete list can be found in the file AUTHORS. + + Licensees holding a valid Bacula Systems SA license may use this file + and others of this release in accordance with the proprietary license + agreement provided in the LICENSE file. Redistribution of any part of + this release is not permitted. + + Bacula® is a registered trademark of Kern Sibbald. +*/ +/** + * This is a process communication lowlevel library for Bacula plugin. + * Author: Radoslaw Korzeniewski, radekk@inteos.pl, Inteos Sp. z o.o. + */ + +#ifndef _PTCOMM_H_ +#define _PTCOMM_H_ + +#include "pluginlib.h" + +#define PTCOMM_DEFAULT_TIMEOUT 300 // timeout waiting for data 15 min, it should be enough + +/* + * The protocol packet header. + * Every packet exchanged between Plugin and Backend will have a special header + * which allow to perfectly synchronize data exchange mitigating the risk + * of a deadlock, where both ends will wait for a data and no one wants to + * send it to the other end. + * The protocol implements a single char packet status which could be: + * D - data packet + * C - command packet + * E - error packet + * F - EOD packet + * T - terminate connection + * W - warning message + * I - information message + * A - fatal error message (abort) + * The length is an ascii coded decimal trailed by a "newline" char - '\n'. + * So, a packet header could be rendered as: 'C000012\n' + */ +struct PTHEADER +{ + char status; + char length[7]; +}; + +/* + * This is a low-level transport communication class which handles all bits and + * bytes of the protocol. + * The class express a high-level methods for low-level transport protocol. + * It handles a communication channel (bpipe) with backend execution and + * termination. The external data exchange using named pipes or local files is + * handled by this class too. + */ +class PTCOMM : public SMARTALLOC +{ +private: + BPIPE *bpipe; // this is our bpipe to communicate with backend */ + int rfd; // + int wfd; // + int efd; // + int maxfd; // + POOL_MEM errmsg; // message buffer for error string */ + int extpipe; // set when data blast is performed using external pipe/file */ + POOL_MEM extpipename; // name of the external pipe/file for restore */ + bool f_eod; // the backend signaled EOD */ + bool f_error; // the backend signaled an error */ + bool f_fatal; // the backend signaled a fatal error */ + bool f_cont; // when we are reading next part of data packet */ + bool abort_on_error; // abort on error flag */ + int32_t remaininglen; // the number of bytes to read when `f_cont` is true + struct timeval _timeout; // + +protected: + bool recvbackend_data(bpContext *ctx, char *buf, int32_t nbytes); + bool sendbackend_data(bpContext *ctx, char *buf, int32_t nbytes); + + int32_t recvbackend_header(bpContext *ctx, char cmd); + int32_t handle_read_header(bpContext *ctx, char cmd); + int32_t handle_payload(bpContext *ctx, char *buf, int32_t nbytes); + + int32_t recvbackend(bpContext *ctx, char cmd, POOL_MEM &buf); + int32_t recvbackend_fixed(bpContext *ctx, char cmd, char *buf, int32_t bufsize); + + int32_t sendbackend(bpContext *ctx, char cmd, POOLMEM *buf, int32_t len); + +public: + PTCOMM() : + bpipe(NULL), + rfd(0), + wfd(0), + efd(0), + maxfd(0), + errmsg(PM_MESSAGE), + extpipe(-1), + extpipename(PM_FNAME), + f_eod(false), + f_error(false), + f_fatal(false), + f_cont(false), + abort_on_error(false), + remaininglen(0) + {} +#if __cplusplus > 201103L + PTCOMM(PTCOMM &) = delete; + PTCOMM(PTCOMM &&) = delete; +#endif + ~PTCOMM() { terminate(NULL); } + + bool handshake(bpContext *ctx, const char *pluginname, const char *pluginapi); + + int32_t read_command(bpContext *ctx, POOL_MEM &buf); + int32_t read_data(bpContext *ctx, POOL_MEM &buf); + int32_t read_data_fixed(bpContext *ctx, char *buf, int32_t len); + + int32_t write_command(bpContext *ctx, char *buf); + + /** + * @brief Sends a command to the backend. + * + * @param ctx bpContext - for Bacula debug and jobinfo messages + * @param buf a message buffer contains command to send + * @return int32_t + * -1 - when encountered any error + * - the number of bytes sent, success + */ + int32_t write_command(bpContext *ctx, POOL_MEM &buf) { return write_command(ctx, buf.addr()); } + int32_t write_data(bpContext *ctx, char *buf, int32_t len); + + bool read_ack(bpContext *ctx); + bool send_ack(bpContext *ctx); + + /** + * @brief Signals en error to the backend. + * + * The buf, when not NULL, can hold an error string sent do the backend. + * + * @param ctx - for Bacula debug and jobinfo messages + * @param buf - when not NULL should consist of an error string + * - when NULL, no error string sent to the backend + * @return int32_t + * -1 - when encountered any error + * - the number of bytes sent, success + */ + inline int32_t signal_error(bpContext *ctx, POOLMEM *buf) + { + int32_t len = buf ? strlen(buf) : 0; + return sendbackend(ctx, 'E', buf, len); + } + + POOLMEM *get_error(bpContext *ctx); + + /** + * @brief Signals EOD to backend. + * + * @param ctx bpContext - for Bacula debug and jobinfo messages + * @return int32_t + * -1 - when encountered any error + * - the number of bytes sent, success + */ + inline int32_t signal_eod(bpContext *ctx) { return sendbackend(ctx, 'F', NULL, 0); } + + /** + * @brief Signal end of communication to the backend. + * The backend should close the connection after receiving this packet. + * + * @param ctx bpContext - for Bacula debug and jobinfo messages + * @return int32_t + * -1 - when encountered any error + * - the number of bytes sent, success + */ + inline int32_t signal_term(bpContext *ctx) { return sendbackend(ctx, 'T', NULL, 0); } + + void terminate(bpContext *ctx); + + /** + * @brief Returns a backend PID if available. + * I'm using an arthrymetic way to make a conditional value return; + * + * @return int backend PID - when backend available; -1 - when backend is unavailable + */ + int get_backend_pid() { return (bpipe != NULL) * bpipe->worker_pid - (bpipe == NULL); } + + /** + * @brief Sets a BPIPE object for our main communication channel. + * + * @param bp object, we do not check for NULL here + */ + inline void set_bpipe(BPIPE *bp) + { + bpipe = bp; + rfd = fileno(bpipe->rfd); + wfd = fileno(bpipe->wfd); + efd = fileno(bpipe->efd); + maxfd = MAX(rfd, wfd); + maxfd = MAX(maxfd, efd) + 1; + } + + /** + * @brief Sets a FILE descriptor used as external pipe during backup and restore. + * + * @param ep a FILE* descriptor used during + */ + inline void set_extpipe(int ep) { extpipe = ep; } + + /** + * @brief Sets an external pipe name for restore. + * + * @param epname - external pipe name + */ + inline void set_extpipename(char *epname) { pm_strcpy(extpipename, epname); } + bool close_extpipe(bpContext *ctx); + + /** + * @brief Checks if connection is open and we can use a bpipe object for communication. + * + * @return true if connection is available + * @return false if connection is closed and we can't use bpipe object + */ + inline bool is_open() { return bpipe != NULL; } + + /** + * @brief Checks if connection is closed and we can't use a bpipe object for communication. + * + * @return true if connection is closed and we can't use bpipe object + * @return false if connection is available + */ + inline bool is_closed() { return bpipe == NULL; } + + /** + * @brief Checks if backend sent us some error, backend error message is flagged on f_error. + * + * @return true when last packet was an error + * @return false when no error packets was received + */ + inline bool is_error() { return f_error || f_fatal; } + + /** + * @brief Checks if backend sent us fatal error, backend error message is flagged on f_fatal. + * + * @return true when last packet was a fatal error + * @return false when no fatal error packets was received + */ + inline bool is_fatal() { return f_fatal || (f_error && abort_on_error); } + + /** + * @brief Checks if backend signaled EOD, eod from backend is flagged on f_eod. + * + * @return true when backend signaled EOD on last packet + * @return false when backend did not signal EOD + */ + inline bool is_eod() { return f_eod; } + + /** + * @brief Clears the EOD from backend flag, f_eod. + * The eod flag is set when EOD message received from backend and not cleared + * until next recvbackend() call. + */ + void clear_eod() { f_eod = false; } + + /** + * @brief Set the abort on error flag + */ + inline void set_abort_on_error() { abort_on_error = true; } + + /** + * @brief Clears the abort on error flag. + */ + inline void clear_abort_on_error() { abort_on_error = false; } + + /** + * @brief return abort on error flag status + * + * @return true if flag is set + * @return false if flag is not set + */ + bool is_abort_on_error() { return abort_on_error; } +}; + +#endif /* _PTCOMM_H_ */