]> git.sur5r.net Git - bacula/bacula/blob - bacula/src/filed/fd_plugins.c
kes Make first cut attempt to correct SQL that computes the current
[bacula/bacula] / bacula / src / filed / fd_plugins.c
1 /*
2    Bacula® - The Network Backup Solution
3
4    Copyright (C) 2007-2008 Free Software Foundation Europe e.V.
5
6    The main author of Bacula is Kern Sibbald, with contributions from
7    many others, a complete list can be found in the file AUTHORS.
8    This program is Free Software; you can redistribute it and/or
9    modify it under the terms of version two of the GNU General Public
10    License as published by the Free Software Foundation, which is 
11    listed in the file LICENSE.
12
13    This program is distributed in the hope that it will be useful, but
14    WITHOUT ANY WARRANTY; without even the implied warranty of
15    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
16    General Public License for more details.
17
18    You should have received a copy of the GNU General Public License
19    along with this program; if not, write to the Free Software
20    Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
21    02110-1301, USA.
22
23    Bacula® is a registered trademark of Kern Sibbald.
24    The licensor of Bacula is the Free Software Foundation Europe
25    (FSFE), Fiduciary Program, Sumatrastrasse 25, 8006 Zürich,
26    Switzerland, email:ftf@fsfeurope.org.
27 */
28 /*
29  * Main program to test loading and running Bacula plugins.
30  *   Destined to become Bacula pluginloader, ...
31  *
32  * Kern Sibbald, October 2007
33  */
34 #include "bacula.h"
35 #include "filed.h"
36
37 const int dbglvl = 50;
38 #ifdef HAVE_WIN32
39 const char *plugin_type = "-fd.dll";
40 #else
41 const char *plugin_type = "-fd.so";
42 #endif
43
44 extern int save_file(JCR *jcr, FF_PKT *ff_pkt, bool top_level);
45
46 /* Function pointers to be set here */
47 extern DLL_IMP_EXP int     (*plugin_bopen)(JCR *jcr, const char *fname, int flags, mode_t mode);
48 extern DLL_IMP_EXP int     (*plugin_bclose)(JCR *jcr);
49 extern DLL_IMP_EXP ssize_t (*plugin_bread)(JCR *jcr, void *buf, size_t count);
50 extern DLL_IMP_EXP ssize_t (*plugin_bwrite)(JCR *jcr, void *buf, size_t count);
51 extern DLL_IMP_EXP boffset_t (*plugin_blseek)(JCR *jcr, boffset_t offset, int whence);
52
53
54 /* Forward referenced functions */
55 static bRC baculaGetValue(bpContext *ctx, bVariable var, void *value);
56 static bRC baculaSetValue(bpContext *ctx, bVariable var, void *value);
57 static bRC baculaRegisterEvents(bpContext *ctx, ...);
58 static bRC baculaJobMsg(bpContext *ctx, const char *file, int line,
59   int type, time_t mtime, const char *fmt, ...);
60 static bRC baculaDebugMsg(bpContext *ctx, const char *file, int line,
61   int level, const char *fmt, ...);
62
63 static int     my_plugin_bopen(JCR *jcr, const char *fname, int flags, mode_t mode);
64 static int     my_plugin_bclose(JCR *jcr);
65 static ssize_t my_plugin_bread(JCR *jcr, void *buf, size_t count);
66 static ssize_t my_plugin_bwrite(JCR *jcr, void *buf, size_t count);
67 static boffset_t my_plugin_blseek(JCR *jcr, boffset_t offset, int whence);
68
69
70 /* Bacula info */
71 static bInfo binfo = {
72    sizeof(bFuncs),
73    FD_PLUGIN_INTERFACE_VERSION 
74 };
75
76 /* Bacula entry points */
77 static bFuncs bfuncs = {
78    sizeof(bFuncs),
79    FD_PLUGIN_INTERFACE_VERSION,
80    baculaRegisterEvents,
81    baculaGetValue,
82    baculaSetValue,
83    baculaJobMsg,
84    baculaDebugMsg
85 };
86
87
88 /*
89  * Create a plugin event 
90  */
91 void generate_plugin_event(JCR *jcr, bEventType eventType, void *value)     
92 {
93    bEvent event;
94    Plugin *plugin;
95    int i = 0;
96
97    if (!plugin_list || !jcr->plugin_ctx_list) {
98       return;                         /* Return if no plugins loaded */
99    }
100
101    bpContext *plugin_ctx_list = (bpContext *)jcr->plugin_ctx_list;
102    event.eventType = eventType;
103
104    Dmsg2(dbglvl, "plugin_ctx=%p JobId=%d\n", jcr->plugin_ctx_list, jcr->JobId);
105
106    /* Pass event to every plugin */
107    foreach_alist(plugin, plugin_list) {
108       bRC rc;
109       rc = plug_func(plugin)->handlePluginEvent(&plugin_ctx_list[i++], &event, value);
110       if (rc != bRC_OK) {
111          break;
112       }
113    }
114
115    return;
116 }
117
118 /*   
119  * Sequence of calls for a backup:
120  * 1. plugin_save() here is called with ff_pkt
121  * 2. we find the plugin requested on the command string
122  * 3. we generate a bEventBackupCommand event to the specified plugin
123  *    and pass it the command string.
124  * 4. we make a startPluginBackup call to the plugin, which gives
125  *    us the data we need in save_pkt
126  * 5. we call Bacula's save_file() subroutine to save the specified
127  *    file.  The plugin will be called at pluginIO() to supply the
128  *    file data.
129  *
130  * Sequence of calls for restore:
131  *   See subroutine plugin_name_stream() below.
132  */
133 int plugin_save(JCR *jcr, FF_PKT *ff_pkt, bool top_level)
134 {
135    Plugin *plugin;
136    int i = 0;
137    int len;
138    char *p;
139    char *cmd = ff_pkt->top_fname;
140    struct save_pkt sp;
141    bEvent event;
142
143    if (!plugin_list || !jcr->plugin_ctx_list) {
144       return 1;                            /* Return if no plugins loaded */
145    }
146
147    bpContext *plugin_ctx_list = (bpContext *)jcr->plugin_ctx_list;
148    event.eventType = bEventBackupCommand;
149
150    /* Handle plugin command here backup */
151    Dmsg1(100, "plugin cmd=%s\n", cmd);
152    if (!(p = strchr(cmd, ':'))) {
153       Jmsg1(jcr, M_ERROR, 0, "Malformed plugin command: %s\n", cmd);
154       goto bail_out;
155    }
156    len = p - cmd;
157    if (len <= 0) {
158       goto bail_out;
159    }
160
161    foreach_alist(plugin, plugin_list) {
162       Dmsg3(100, "plugin=%s cmd=%s len=%d\n", plugin->file, cmd, len);
163       if (strncmp(plugin->file, cmd, len) != 0) {
164          i++;
165          continue;
166       }
167       Dmsg1(100, "Command plugin = %s\n", cmd);
168       /* Send the backup command */
169       if (plug_func(plugin)->handlePluginEvent(&plugin_ctx_list[i], &event, cmd) != bRC_OK) {
170          goto bail_out;
171       }
172       /* Loop getting filenames to backup then saving them */
173       while (!job_canceled(jcr)) { 
174          memset(&sp, 0, sizeof(sp));
175          sp.type = FT_REG;
176          sp.portable = true;
177          sp.cmd = cmd;
178          Dmsg3(000, "startBackup st_size=%p st_blocks=%p sp=%p\n", &sp.statp.st_size, &sp.statp.st_blocks,
179                 &sp);
180          /* Get the file save parameters */
181          if (plug_func(plugin)->startBackupFile(&plugin_ctx_list[i], &sp) != bRC_OK) {
182             goto bail_out;
183          }
184          jcr->plugin_ctx = &plugin_ctx_list[i];
185          jcr->plugin = plugin;
186          jcr->plugin_sp = &sp;
187          ff_pkt = jcr->ff;
188          ff_pkt->fname = sp.fname;
189          ff_pkt->type = sp.type;
190          memcpy(&ff_pkt->statp, &sp.statp, sizeof(ff_pkt->statp));
191          Dmsg1(000, "Save_file: file=%s\n", ff_pkt->fname);
192          save_file(jcr, ff_pkt, true);
193          if (plug_func(plugin)->endBackupFile(&plugin_ctx_list[i]) != bRC_More) {
194             goto bail_out;
195          }
196       }
197    }
198    Jmsg1(jcr, M_ERROR, 0, "Command plugin \"%s\" not found.\n", cmd);
199
200 bail_out:
201    return 1;
202 }
203
204 /* 
205  * Send plugin name start/end record to SD
206  */
207 bool send_plugin_name(JCR *jcr, BSOCK *sd, bool start)
208 {
209    int stat;
210    struct save_pkt *sp = (struct save_pkt *)jcr->plugin_sp;
211   
212    Dmsg1(000, "send_plugin_name=%s\n", sp->cmd);
213    /* Send stream header */
214    if (!sd->fsend("%ld %d 0", jcr->JobFiles+1, STREAM_PLUGIN_NAME)) {
215      Jmsg1(jcr, M_FATAL, 0, _("Network send error to SD. ERR=%s\n"),
216            sd->bstrerror());
217      return false;
218    }
219    Dmsg1(000, "send: %s\n", sd->msg);
220
221    if (start) {
222       /* Send data -- not much */
223       stat = sd->fsend("%ld 1 %d %s%c", jcr->JobFiles+1, sp->portable, sp->cmd, 0);
224    } else {
225       /* Send end of data */
226       stat = sd->fsend("0 0");
227    }
228    if (!stat) {
229       Jmsg1(jcr, M_FATAL, 0, _("Network send error to SD. ERR=%s\n"),
230             sd->bstrerror());
231          return false;
232    }
233    Dmsg1(000, "send: %s\n", sd->msg);
234    sd->signal(BNET_EOD);            /* indicate end of plugin name data */
235    return true;
236 }
237
238 /*
239  * Plugin name stream found during restore.  The record passed in
240  *  argument name was generated in send_plugin_name() above.
241  */
242 void plugin_name_stream(JCR *jcr, char *name)    
243 {
244    char *p = name;
245    char *cmd;
246    bool start, portable;
247    Plugin *plugin;
248    int len;
249    int i = 0;
250    bpContext *plugin_ctx_list = (bpContext *)jcr->plugin_ctx_list;
251    if (!plugin_ctx_list) {
252       goto bail_out;
253    }
254
255    Dmsg1(100, "Read plugin stream string=%s\n", name);
256    skip_nonspaces(&p);             /* skip over jcr->JobFiles */
257    skip_spaces(&p);
258    start = *p == '1';
259    if (start) {
260       /* Start of plugin data */
261       skip_nonspaces(&p);          /* skip start/end flag */
262       skip_spaces(&p);
263       portable = *p == '1';
264       skip_nonspaces(&p);          /* skip portable flag */
265       skip_spaces(&p);
266       cmd = p;
267    } else {
268       /*
269        * End of plugin data, notify plugin, then clear flags   
270        */
271       plugin = (Plugin *)jcr->plugin;
272       plug_func(plugin)->endRestoreFile(&plugin_ctx_list[i]);
273       jcr->plugin_ctx = NULL;
274       jcr->plugin = NULL;
275       goto bail_out;
276    }
277       
278    /*
279     * After this point, we are dealing with a restore start
280     */
281
282    Dmsg1(100, "plugin restore cmd=%s\n", cmd);
283    if (!(p = strchr(cmd, ':'))) {
284       Jmsg1(jcr, M_ERROR, 0, "Malformed plugin command: %s\n", cmd);
285       goto bail_out;
286    }
287    len = p - cmd;
288    if (len <= 0) {
289       goto bail_out;
290    }
291
292    /*
293     * Search for correct plugin as specified on the command 
294     */
295    foreach_alist(plugin, plugin_list) {
296       bEvent event;
297       Dmsg3(100, "plugin=%s cmd=%s len=%d\n", plugin->file, cmd, len);
298       if (strncmp(plugin->file, cmd, len) != 0) {
299          i++;
300          continue;
301       }
302       Dmsg1(100, "Restore Command plugin = %s\n", cmd);
303       event.eventType = bEventRestoreCommand;     
304       if (plug_func(plugin)->handlePluginEvent(&plugin_ctx_list[i], 
305             &event, cmd) != bRC_OK) {
306          goto bail_out;
307       }
308       jcr->plugin_ctx = &plugin_ctx_list[i];
309       jcr->plugin = plugin;
310       goto bail_out;
311    }
312 bail_out:
313    return;
314 }
315
316 /*
317  * Tell the plugin to create the file.  Return values are
318  *
319  *  CF_ERROR    -- error
320  *  CF_SKIP     -- skip processing this file
321  *  CF_EXTRACT  -- extract the file (i.e.call i/o routines)
322  *  CF_CREATED  -- created, but no content to extract (typically directories)
323  *
324  */
325 int plugin_create_file(JCR *jcr, ATTR *attr, BFILE *bfd, int replace)
326 {
327    bpContext *plugin_ctx = (bpContext *)jcr->plugin_ctx;
328    Plugin *plugin = (Plugin *)jcr->plugin;
329    struct restore_pkt rp;
330    struct io_pkt io;
331
332    if (!set_cmd_plugin(bfd, jcr)) {
333       return CF_ERROR;
334    }
335    rp.stream = attr->stream;
336    rp.data_stream = attr->data_stream;
337    rp.type = attr->type;
338    rp.file_index = attr->file_index;
339    rp.LinkFI = attr->LinkFI;
340    rp.uid = attr->uid;
341    rp.statp = attr->statp;                /* structure assignment */
342    rp.attrEx = attr->attrEx;
343    rp.ofname = attr->ofname;
344    rp.olname = attr->olname;
345    rp.where = jcr->where;
346    rp.RegexWhere = jcr->RegexWhere;
347    rp.replace = jcr->replace;
348    if (plug_func(plugin)->createFile(plugin_ctx, &rp) != bRC_OK) {
349       return CF_ERROR;
350    }
351    io.func = IO_OPEN;
352    io.count = 0;
353    io.buf = NULL;
354    io.mode = 0777 & attr->statp.st_mode;
355    io.flags = O_WRONLY;
356    if (plug_func(plugin)->pluginIO(plugin_ctx, &io) != bRC_OK) {
357       return CF_ERROR;
358    }
359    return CF_EXTRACT;
360 }
361
362 /*
363  * Reset the file attributes after all file I/O is done -- this allows
364  *  the previous access time/dates to be set properly, and it also allows
365  *  us to properly set directory permissions.
366  */
367 bool plugin_set_attributes(JCR *jcr, ATTR *attr, BFILE *ofd)
368 {
369    return true;
370 }
371
372 /*
373  * This entry point is called internally by Bacula to ensure
374  *  that the plugin IO calls come into this code.
375  */
376 void load_fd_plugins(const char *plugin_dir)
377 {
378    Plugin *plugin;
379
380    if (!plugin_dir) {
381       return;
382    }
383
384    plugin_list = New(alist(10, not_owned_by_alist));
385    if (!load_plugins((void *)&binfo, (void *)&bfuncs, plugin_dir, plugin_type)) {
386       /* Either none found, or some error */
387       if (plugin_list->size() == 0) {
388          delete plugin_list;
389          plugin_list = NULL;
390          return;
391       }
392    }
393
394    /* Plug entry points called from findlib */
395    plugin_bopen  = my_plugin_bopen;
396    plugin_bclose = my_plugin_bclose;
397    plugin_bread  = my_plugin_bread;
398    plugin_bwrite = my_plugin_bwrite;
399    plugin_blseek = my_plugin_blseek;
400    foreach_alist(plugin, plugin_list) {
401       Jmsg(NULL, M_INFO, 0, _("Loaded plugin: %s\n"), plugin->file);
402    }
403
404 }
405
406 /*
407  * Create a new instance of each plugin for this Job
408  *   Note, plugin_list can exist but jcr->plugin_ctx_list can
409  *   be NULL if no plugins were loaded.
410  */
411 void new_plugins(JCR *jcr)
412 {
413    Plugin *plugin;
414    int i = 0;
415
416    if (!plugin_list) {
417       return;
418    }
419
420    int num = plugin_list->size();
421
422    if (num == 0) {
423       return;
424    }
425
426    jcr->plugin_ctx_list = (void *)malloc(sizeof(bpContext) * num);
427
428    bpContext *plugin_ctx_list = (bpContext *)jcr->plugin_ctx_list;
429    Dmsg2(dbglvl, "Instantiate plugin_ctx=%p JobId=%d\n", plugin_ctx_list, jcr->JobId);
430    foreach_alist(plugin, plugin_list) {
431       /* Start a new instance of each plugin */
432       plugin_ctx_list[i].bContext = (void *)jcr;
433       plugin_ctx_list[i].pContext = NULL;
434       plug_func(plugin)->newPlugin(&plugin_ctx_list[i++]);
435    }
436 }
437
438 /*
439  * Free the plugin instances for this Job
440  */
441 void free_plugins(JCR *jcr)
442 {
443    Plugin *plugin;
444    int i = 0;
445
446    if (!plugin_list || !jcr->plugin_ctx_list) {
447       return;                         /* no plugins, nothing to do */
448    }
449
450    bpContext *plugin_ctx_list = (bpContext *)jcr->plugin_ctx_list;
451    Dmsg2(dbglvl, "Free instance plugin_ctx=%p JobId=%d\n", plugin_ctx_list, jcr->JobId);
452    foreach_alist(plugin, plugin_list) {
453       /* Free the plugin instance */
454       plug_func(plugin)->freePlugin(&plugin_ctx_list[i++]);
455    }
456    free(plugin_ctx_list);
457    jcr->plugin_ctx_list = NULL;
458 }
459
460 static int my_plugin_bopen(JCR *jcr, const char *fname, int flags, mode_t mode)
461 {
462    Plugin *plugin = (Plugin *)jcr->plugin;
463    bpContext *plugin_ctx = (bpContext *)jcr->plugin_ctx;
464    struct io_pkt io;
465    Dmsg0(000, "plugin_bopen\n");
466    io.func = IO_OPEN;
467    io.count = 0;
468    io.buf = NULL;
469    io.mode = mode;
470    io.flags = flags;
471    plug_func(plugin)->pluginIO(plugin_ctx, &io);
472    return io.status;
473 }
474
475 static int my_plugin_bclose(JCR *jcr)
476 {
477    Plugin *plugin = (Plugin *)jcr->plugin;
478    bpContext *plugin_ctx = (bpContext *)jcr->plugin_ctx;
479    struct io_pkt io;
480    Dmsg0(000, "plugin_bclose\n");
481    io.func = IO_CLOSE;
482    io.count = 0;
483    io.buf = NULL;
484    plug_func(plugin)->pluginIO(plugin_ctx, &io);
485    return io.status;
486 }
487
488 static ssize_t my_plugin_bread(JCR *jcr, void *buf, size_t count)
489 {
490    Plugin *plugin = (Plugin *)jcr->plugin;
491    bpContext *plugin_ctx = (bpContext *)jcr->plugin_ctx;
492    struct io_pkt io;
493    Dmsg0(000, "plugin_bread\n");
494    io.func = IO_READ;
495    io.count = count;
496    io.buf = (char *)buf;
497    plug_func(plugin)->pluginIO(plugin_ctx, &io);
498    return (ssize_t)io.status;
499 }
500
501 static ssize_t my_plugin_bwrite(JCR *jcr, void *buf, size_t count)
502 {
503    Plugin *plugin = (Plugin *)jcr->plugin;
504    bpContext *plugin_ctx = (bpContext *)jcr->plugin_ctx;
505    struct io_pkt io;
506    Dmsg0(000, "plugin_bwrite\n");
507    io.func = IO_WRITE;
508    io.count = count;
509    io.buf = (char *)buf;
510    plug_func(plugin)->pluginIO(plugin_ctx, &io);
511    return (ssize_t)io.status;
512 }
513
514 static boffset_t my_plugin_blseek(JCR *jcr, boffset_t offset, int whence)
515 {
516    Plugin *plugin = (Plugin *)jcr->plugin;
517    bpContext *plugin_ctx = (bpContext *)jcr->plugin_ctx;
518    struct io_pkt io;
519    Dmsg0(000, "plugin_bseek\n");
520    io.func = IO_SEEK;
521    io.offset = offset;
522    io.whence = whence;
523    plug_func(plugin)->pluginIO(plugin_ctx, &io);
524    return (boffset_t)io.offset;
525 }
526
527 /* ==============================================================
528  *
529  * Callbacks from the plugin
530  *
531  * ==============================================================
532  */
533 static bRC baculaGetValue(bpContext *ctx, bVariable var, void *value)
534 {
535    JCR *jcr = (JCR *)(ctx->bContext);
536 // Dmsg1(dbglvl, "bacula: baculaGetValue var=%d\n", var);
537    if (!value) {
538       return bRC_Error;
539    }
540 // Dmsg1(dbglvl, "Bacula: jcr=%p\n", jcr); 
541    switch (var) {
542    case bVarJobId:
543       *((int *)value) = jcr->JobId;
544       Dmsg1(dbglvl, "Bacula: return bVarJobId=%d\n", jcr->JobId);
545       break;
546    case bVarFDName:
547       *((char **)value) = my_name;
548       Dmsg1(dbglvl, "Bacula: return my_name=%s\n", my_name);
549       break;
550    case bVarLevel:
551    case bVarType:
552    case bVarClient:
553    case bVarJobName:
554    case bVarJobStatus:
555    case bVarSinceTime:
556       break;
557    }
558    return bRC_OK;
559 }
560
561 static bRC baculaSetValue(bpContext *ctx, bVariable var, void *value)
562 {
563    Dmsg1(dbglvl, "bacula: baculaSetValue var=%d\n", var);
564    return bRC_OK;
565 }
566
567 static bRC baculaRegisterEvents(bpContext *ctx, ...)
568 {
569    va_list args;
570    uint32_t event;
571
572    va_start(args, ctx);
573    while ((event = va_arg(args, uint32_t))) {
574       Dmsg1(dbglvl, "Plugin wants event=%u\n", event);
575    }
576    va_end(args);
577    return bRC_OK;
578 }
579
580 static bRC baculaJobMsg(bpContext *ctx, const char *file, int line,
581   int type, time_t mtime, const char *fmt, ...)
582 {
583    va_list arg_ptr;
584    char buf[2000];
585    JCR *jcr = (JCR *)(ctx->bContext);
586
587    va_start(arg_ptr, fmt);
588    bvsnprintf(buf, sizeof(buf), fmt, arg_ptr);
589    va_end(arg_ptr);
590    Jmsg(jcr, type, mtime, "%s", buf);
591    return bRC_OK;
592 }
593
594 static bRC baculaDebugMsg(bpContext *ctx, const char *file, int line,
595   int level, const char *fmt, ...)
596 {
597    va_list arg_ptr;
598    char buf[2000];
599
600    va_start(arg_ptr, fmt);
601    bvsnprintf(buf, sizeof(buf), fmt, arg_ptr);
602    va_end(arg_ptr);
603    d_msg(file, line, level, "%s", buf);
604    return bRC_OK;
605 }
606
607 #ifdef TEST_PROGRAM
608
609 int     (*plugin_bopen)(JCR *jcr, const char *fname, int flags, mode_t mode) = NULL;
610 int     (*plugin_bclose)(JCR *jcr) = NULL;
611 ssize_t (*plugin_bread)(JCR *jcr, void *buf, size_t count) = NULL;
612 ssize_t (*plugin_bwrite)(JCR *jcr, void *buf, size_t count) = NULL;
613 boffset_t (*plugin_blseek)(JCR *jcr, boffset_t offset, int whence) = NULL;
614
615 int save_file(JCR *jcr, FF_PKT *ff_pkt, bool top_level)
616 {
617    return 0;
618 }
619
620 bool set_cmd_plugin(BFILE *bfd, JCR *jcr)
621 {
622    return true;
623 }
624
625 int main(int argc, char *argv[])
626 {
627    char plugin_dir[1000];
628    JCR mjcr1, mjcr2;
629    JCR *jcr1 = &mjcr1;
630    JCR *jcr2 = &mjcr2;
631
632    strcpy(my_name, "test-fd");
633     
634    getcwd(plugin_dir, sizeof(plugin_dir)-1);
635    load_fd_plugins(plugin_dir);
636
637    jcr1->JobId = 111;
638    new_plugins(jcr1);
639
640    jcr2->JobId = 222;
641    new_plugins(jcr2);
642
643    generate_plugin_event(jcr1, bEventJobStart, (void *)"Start Job 1");
644    generate_plugin_event(jcr1, bEventJobEnd);
645    generate_plugin_event(jcr2, bEventJobStart, (void *)"Start Job 2");
646    free_plugins(jcr1);
647    generate_plugin_event(jcr2, bEventJobEnd);
648    free_plugins(jcr2);
649
650    unload_plugins();
651
652    Dmsg0(dbglvl, "bacula: OK ...\n");
653    close_memory_pool();
654    sm_dump(false);
655    return 0;
656 }
657
658 #endif /* TEST_PROGRAM */