1 /*
2    BAREOS® - Backup Archiving REcovery Open Sourced
3 
4    Copyright (C) 2002-2009 Free Software Foundation Europe e.V.
5    Copyright (C) 2011-2016 Planets Communications B.V.
6    Copyright (C) 2013-2016 Bareos GmbH & Co. KG
7 
8    This program is Free Software; you can redistribute it and/or
9    modify it under the terms of version three of the GNU Affero General Public
10    License as published by the Free Software Foundation and included
11    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    Affero General Public License for more details.
17 
18    You should have received a copy of the GNU Affero 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 /*
24  * Kern Sibbald, February MMII
25  */
26 /**
27  * @file
28  * User Agent Database prune Command
29  *
30  * Applies retention periods
31  */
32 
33 #include "include/bareos.h"
34 #include "dird.h"
35 #include "dird/dird_globals.h"
36 #include "dird/ua_input.h"
37 #include "cats/sql.h"
38 #include "dird/ua_db.h"
39 #include "dird/ua_select.h"
40 #include "dird/ua_prune.h"
41 #include "dird/ua_purge.h"
42 #include "lib/edit.h"
43 
44 namespace directordaemon {
45 
46 /* Forward referenced functions */
47 static bool PruneDirectory(UaContext *ua, ClientResource *client);
48 static bool PruneStats(UaContext *ua, utime_t retention);
49 static bool GrowDelList(struct del_ctx *del);
50 
51 /**
52  * Called here to count entries to be deleted
53  */
DelCountHandler(void * ctx,int num_fields,char ** row)54 int DelCountHandler(void *ctx, int num_fields, char **row)
55 {
56    struct s_count_ctx *cnt = (struct s_count_ctx *)ctx;
57 
58    if (row[0]) {
59       cnt->count = str_to_int64(row[0]);
60    } else {
61       cnt->count = 0;
62    }
63    return 0;
64 }
65 
66 /**
67  * Called here to make in memory list of JobIds to be
68  *  deleted and the associated PurgedFiles flag.
69  *  The in memory list will then be transversed
70  *  to issue the SQL DELETE commands.  Note, the list
71  *  is allowed to get to MAX_DEL_LIST_LEN to limit the
72  *  maximum malloc'ed memory.
73  */
JobDeleteHandler(void * ctx,int num_fields,char ** row)74 int JobDeleteHandler(void *ctx, int num_fields, char **row)
75 {
76    struct del_ctx *del = (struct del_ctx *)ctx;
77 
78    if (!GrowDelList(del)) {
79       return 1;
80    }
81    del->JobId[del->num_ids] = (JobId_t)str_to_int64(row[0]);
82    Dmsg2(60, "JobDeleteHandler row=%d val=%d\n", del->num_ids, del->JobId[del->num_ids]);
83    del->PurgedFiles[del->num_ids++] = (char)str_to_int64(row[1]);
84    return 0;
85 }
86 
FileDeleteHandler(void * ctx,int num_fields,char ** row)87 int FileDeleteHandler(void *ctx, int num_fields, char **row)
88 {
89    struct del_ctx *del = (struct del_ctx *)ctx;
90 
91    if (!GrowDelList(del)) {
92       return 1;
93    }
94    del->JobId[del->num_ids++] = (JobId_t)str_to_int64(row[0]);
95 // Dmsg2(150, "row=%d val=%d\n", del->num_ids-1, del->JobId[del->num_ids-1]);
96    return 0;
97 }
98 
99 /**
100  * Prune records from database
101  */
PruneCmd(UaContext * ua,const char * cmd)102 bool PruneCmd(UaContext *ua, const char *cmd)
103 {
104    ClientResource *client;
105    PoolResource *pool;
106    PoolDbRecord pr;
107    MediaDbRecord mr;
108    utime_t retention;
109    int kw;
110    const char *permission_denied_message = _("Permission denied: need full %s permission.\n");
111    static const char *keywords[] = {
112       NT_("Files"),
113       NT_("Jobs"),
114       NT_("Volume"),
115       NT_("Stats"),
116       NT_("Directory"),
117       NULL
118    };
119 
120    /*
121     * All prune commands might target jobs that reside on different storages.
122     * Instead of checking all of them,
123     * we require full permission on jobs and storages.
124     * Client and Pool permissions are checked at the individual subcommands.
125     */
126    if (ua->AclHasRestrictions(Job_ACL)) {
127       ua->ErrorMsg(permission_denied_message, "job");
128       return false;
129    }
130    if (ua->AclHasRestrictions(Storage_ACL)) {
131       ua->ErrorMsg(permission_denied_message, "storage");
132       return false;
133    }
134 
135    if (!OpenClientDb(ua, true)) {
136       return false;
137    }
138 
139    memset(&pr, 0, sizeof(pr));
140    memset(&mr, 0, sizeof(mr));
141 
142    /*
143     * First search args
144     */
145    kw = FindArgKeyword(ua, keywords);
146    if (kw < 0 || kw > 4) {
147       /*
148        * No args, so ask user
149        */
150       kw = DoKeywordPrompt(ua, _("Choose item to prune"), keywords);
151    }
152 
153    switch (kw) {
154    case 0: /* prune files */
155 
156       if (!(client = get_client_resource(ua))) {
157          return false;
158       }
159 
160       if ((FindArgWithValue(ua, NT_("pool")) >= 0) || ua->AclHasRestrictions(Pool_ACL)) {
161          pool = get_pool_resource(ua);
162       } else {
163          pool = NULL;
164       }
165 
166       /*
167        * Pool File Retention takes precedence over client File Retention
168        */
169       if (pool && pool->FileRetention > 0) {
170          if (!ConfirmRetention(ua, &pool->FileRetention, "File")) {
171             return false;
172          }
173       } else if (!ConfirmRetention(ua, &client->FileRetention, "File")) {
174          return false;
175       }
176 
177       PruneFiles(ua, client, pool);
178 
179       return true;
180    case 1: { /* prune jobs */
181       int jobtype = -1;
182 
183       if (!(client = get_client_resource(ua))) {
184          return false;
185       }
186 
187       if ((FindArgWithValue(ua, NT_("pool")) >= 0) || ua->AclHasRestrictions(Pool_ACL)) {
188          pool = get_pool_resource(ua);
189       } else {
190          pool = NULL;
191       }
192 
193       /*
194        * Ask what jobtype to prune.
195        */
196       if (!GetUserJobTypeSelection(ua, &jobtype)) {
197          return false;
198       }
199 
200       /*
201        * Verify that result jobtype is valid (this should always be the case).
202        */
203       if (jobtype < 0) {
204          return false;
205       }
206 
207       /*
208        * Pool Job Retention takes precedence over client Job Retention
209        */
210       if (pool && pool->JobRetention > 0) {
211          if (!ConfirmRetention(ua, &pool->JobRetention, "Job")) {
212             return false;
213          }
214       } else if (!ConfirmRetention(ua, &client->JobRetention, "Job")) {
215          return false;
216       }
217 
218       return PruneJobs(ua, client, pool, jobtype);
219    }
220    case 2: /* prune volume */
221 
222       if (ua->AclHasRestrictions(Client_ACL)) {
223          ua->ErrorMsg(permission_denied_message, "client");
224          return false;
225       }
226 
227       if (!SelectPoolAndMediaDbr(ua, &pr, &mr)) {
228          return false;
229       }
230 
231       if (mr.Enabled == VOL_ARCHIVED) {
232          ua->ErrorMsg(_("Cannot prune Volume \"%s\" because it is archived.\n"), mr.VolumeName);
233          return false;
234       }
235 
236       if (!ConfirmRetention(ua, &mr.VolRetention, "Volume")) {
237          return false;
238       }
239 
240       return PruneVolume(ua, &mr);
241    case 3: /* prune stats */
242       if (!me->stats_retention) {
243          return false;
244       }
245 
246       retention = me->stats_retention;
247 
248       if (ua->AclHasRestrictions(Client_ACL)) {
249          ua->ErrorMsg(permission_denied_message, "client");
250          return false;
251       }
252       if (ua->AclHasRestrictions(Pool_ACL)) {
253          ua->ErrorMsg(permission_denied_message, "pool");
254          return false;
255       }
256 
257       if (!ConfirmRetention(ua, &retention, "Statistics")) {
258          return false;
259       }
260 
261       return PruneStats(ua, retention);
262    case 4: /* prune directory */
263 
264       if (ua->AclHasRestrictions(Pool_ACL)) {
265          ua->ErrorMsg(permission_denied_message, "pool");
266          return false;
267       }
268 
269       if ((FindArgWithValue(ua, NT_("client")) >= 0) || ua->AclHasRestrictions(Client_ACL)) {
270          if (!(client = get_client_resource(ua))) {
271             return false;
272          }
273       } else {
274          client = NULL;
275       }
276 
277       return PruneDirectory(ua, client);
278    default:
279       break;
280    }
281 
282    return true;
283 }
284 
285 /**
286  * Prune Directory meta data records from the database.
287  */
PruneDirectory(UaContext * ua,ClientResource * client)288 static bool PruneDirectory(UaContext *ua, ClientResource *client)
289 {
290    int i, len;
291    ClientDbRecord cr;
292    char *prune_topdir = NULL;
293    PoolMem query(PM_MESSAGE),
294             temp(PM_MESSAGE);
295    bool recursive = false;
296    bool retval = false;
297 
298    /*
299     * See if a client was selected.
300     */
301    if (!client) {
302       if (!GetYesno(ua, _("No client restriction given really remove "
303                            "directory for all clients (yes/no): ")) || !ua->pint32_val) {
304          if (!(client = get_client_resource(ua))) {
305             return false;
306          }
307       }
308    }
309 
310    /*
311     * See if we need to recursively remove all directories under a certain path.
312     */
313    recursive = FindArg(ua, NT_("recursive")) >= 0;
314 
315    /*
316     * Get the directory to prune.
317     */
318    i = FindArgWithValue(ua, NT_("directory"));
319    if (i >= 0) {
320       PmStrcpy(temp, ua->argv[i]);
321    } else {
322       if (recursive) {
323          if (!GetCmd(ua, _("Please enter the full path prefix to remove: "), false)) {
324             return false;
325          }
326       } else {
327          if (!GetCmd(ua, _("Please enter the full path to remove: "), false)) {
328             return false;
329          }
330       }
331       PmStrcpy(temp, ua->cmd);
332    }
333 
334    /*
335     * See if the directory ends in a / and escape it for usage in a database query.
336     */
337    len = strlen(temp.c_str());
338    if (*(temp.c_str() + len - 1) != '/') {
339       PmStrcat(temp, "/");
340       len++;
341    }
342    prune_topdir = (char *)malloc(len * 2 + 1);
343    ua->db->EscapeString(ua->jcr, prune_topdir, temp.c_str(), len);
344 
345    /*
346     * Remove all files in particular directory.
347     */
348    if (recursive) {
349       Mmsg(query, "DELETE FROM file WHERE pathid IN ("
350                   "SELECT pathid FROM path "
351                   "WHERE path LIKE '%s%%'"
352                   ")", prune_topdir);
353    } else {
354       Mmsg(query, "DELETE FROM file WHERE pathid IN ("
355                   "SELECT pathid FROM path "
356                   "WHERE path LIKE '%s'"
357                   ")", prune_topdir);
358    }
359 
360    if (client) {
361       char ed1[50];
362 
363       memset(&cr, 0, sizeof(cr));
364       bstrncpy(cr.Name, client->name(), sizeof(cr.Name));
365       if (!ua->db->CreateClientRecord(ua->jcr, &cr)) {
366          goto bail_out;
367       }
368 
369       Mmsg(temp, " AND JobId IN ("
370                  "SELECT JobId FROM Job "
371                  "WHERE ClientId=%s"
372                  ")", edit_int64(cr.ClientId, ed1));
373 
374       PmStrcat(query, temp.c_str());
375    }
376 
377    DbLock(ua->db);
378    ua->db->SqlQuery(query.c_str());
379    DbUnlock(ua->db);
380 
381    /*
382     * If we removed the entries from the file table without limiting it to a
383     * certain client we created orphaned path entries as no one is referencing
384     * them anymore.
385     */
386    if (!client) {
387       if (!GetYesno(ua, _("Cleanup orphaned path records (yes/no):")) || !ua->pint32_val) {
388          retval = true;
389          goto bail_out;
390       }
391 
392       if (recursive) {
393          Mmsg(query, "DELETE FROM path "
394                      "WHERE path LIKE '%s%%'", prune_topdir);
395       } else {
396          Mmsg(query, "DELETE FROM path "
397                      "WHERE path LIKE '%s'", prune_topdir);
398       }
399 
400       DbLock(ua->db);
401       ua->db->SqlQuery(query.c_str());
402       DbUnlock(ua->db);
403    }
404 
405    retval = true;
406 
407 bail_out:
408    if (prune_topdir) {
409       free(prune_topdir);
410    }
411 
412    return retval;
413 }
414 
415 /**
416  * Prune Job stat records from the database.
417  */
PruneStats(UaContext * ua,utime_t retention)418 static bool PruneStats(UaContext *ua, utime_t retention)
419 {
420    char ed1[50];
421    char dt[MAX_TIME_LENGTH];
422    PoolMem query(PM_MESSAGE);
423    utime_t now = (utime_t)time(NULL);
424 
425    DbLock(ua->db);
426    Mmsg(query, "DELETE FROM JobHisto WHERE JobTDate < %s", edit_int64(now - retention, ed1));
427    ua->db->SqlQuery(query.c_str());
428    DbUnlock(ua->db);
429 
430    ua->InfoMsg(_("Pruned Jobs from JobHisto in catalog.\n"));
431 
432    bstrutime(dt, sizeof(dt), now - retention);
433 
434    DbLock(ua->db);
435    Mmsg(query, "DELETE FROM DeviceStats WHERE SampleTime < '%s'", dt);
436    ua->db->SqlQuery(query.c_str());
437    DbUnlock(ua->db);
438 
439    ua->InfoMsg(_("Pruned Statistics from DeviceStats in catalog.\n"));
440 
441    DbLock(ua->db);
442    Mmsg(query, "DELETE FROM JobStats WHERE SampleTime < '%s'", dt);
443    ua->db->SqlQuery(query.c_str());
444    DbUnlock(ua->db);
445 
446    ua->InfoMsg(_("Pruned Statistics from JobStats in catalog.\n"));
447 
448    return true;
449 }
450 
451 /**
452  * Use pool and client specified by user to select jobs to prune
453  * returns add_from string to add in FROM clause
454  *         add_where string to add in WHERE clause
455  */
prune_set_filter(UaContext * ua,ClientResource * client,PoolResource * pool,utime_t period,PoolMem * add_from,PoolMem * add_where)456 static bool prune_set_filter(UaContext *ua, ClientResource *client,
457                              PoolResource *pool, utime_t period,
458                              PoolMem *add_from, PoolMem *add_where)
459 {
460    utime_t now;
461    char ed1[50], ed2[MAX_ESCAPE_NAME_LENGTH];
462    PoolMem tmp(PM_MESSAGE);
463 
464    now = (utime_t)time(NULL);
465    edit_int64(now - period, ed1);
466    Dmsg3(150, "now=%lld period=%lld JobTDate=%s\n", now, period, ed1);
467    Mmsg(tmp, " AND JobTDate < %s ", ed1);
468    PmStrcat(*add_where, tmp.c_str());
469 
470    DbLock(ua->db);
471    if (client) {
472       ua->db->EscapeString(ua->jcr, ed2, client->name(), strlen(client->name()));
473       Mmsg(tmp, " AND Client.Name = '%s' ", ed2);
474       PmStrcat(*add_where, tmp.c_str());
475       PmStrcat(*add_from, " JOIN Client USING (ClientId) ");
476    }
477 
478    if (pool) {
479       ua->db->EscapeString(ua->jcr, ed2, pool->name(), strlen(pool->name()));
480       Mmsg(tmp, " AND Pool.Name = '%s' ", ed2);
481       PmStrcat(*add_where, tmp.c_str());
482       /* Use ON() instead of USING for some old SQLite */
483       PmStrcat(*add_from, " JOIN Pool USING(PoolId) ");
484    }
485    Dmsg2(150, "f=%s w=%s\n", add_from->c_str(), add_where->c_str());
486    DbUnlock(ua->db);
487    return true;
488 }
489 
490 /**
491  * Prune File records from the database. For any Job which
492  * is older than the retention period, we unconditionally delete
493  * all File records for that Job.  This is simple enough that no
494  * temporary tables are needed. We simply make an in memory list of
495  * the JobIds meeting the prune conditions, then delete all File records
496  * pointing to each of those JobIds.
497  *
498  * This routine assumes you want the pruning to be done. All checking
499  *  must be done before calling this routine.
500  *
501  * Note: client or pool can possibly be NULL (not both).
502  */
PruneFiles(UaContext * ua,ClientResource * client,PoolResource * pool)503 bool PruneFiles(UaContext *ua, ClientResource *client, PoolResource *pool)
504 {
505    struct del_ctx del;
506    struct s_count_ctx cnt;
507    PoolMem query(PM_MESSAGE);
508    PoolMem sql_where(PM_MESSAGE);
509    PoolMem sql_from(PM_MESSAGE);
510    utime_t period;
511    char ed1[50];
512 
513    memset(&del, 0, sizeof(del));
514 
515    if (pool && pool->FileRetention > 0) {
516       period = pool->FileRetention;
517 
518    } else if (client) {
519       period = client->FileRetention;
520 
521    } else {                     /* should specify at least pool or client */
522       return false;
523    }
524 
525    DbLock(ua->db);
526    /* Specify JobTDate and Pool.Name= and/or Client.Name= in the query */
527    if (!prune_set_filter(ua, client, pool, period, &sql_from, &sql_where)) {
528       goto bail_out;
529    }
530 
531 //   edit_utime(now-period, ed1, sizeof(ed1));
532 //   Jmsg(ua->jcr, M_INFO, 0, _("Begin pruning Jobs older than %s secs.\n"), ed1);
533    Jmsg(ua->jcr, M_INFO, 0, _("Begin pruning Files.\n"));
534    /* Select Jobs -- for counting */
535    Mmsg(query,
536         "SELECT COUNT(1) FROM Job %s WHERE PurgedFiles=0 %s",
537         sql_from.c_str(), sql_where.c_str());
538    Dmsg1(050, "select sql=%s\n", query.c_str());
539    cnt.count = 0;
540    if (!ua->db->SqlQuery(query.c_str(), DelCountHandler, (void *)&cnt)) {
541       ua->ErrorMsg("%s", ua->db->strerror());
542       Dmsg0(050, "Count failed\n");
543       goto bail_out;
544    }
545 
546    if (cnt.count == 0) {
547       if (ua->verbose) {
548          ua->WarningMsg(_("No Files found to prune.\n"));
549       }
550       goto bail_out;
551    }
552 
553    if (cnt.count < MAX_DEL_LIST_LEN) {
554       del.max_ids = cnt.count + 1;
555    } else {
556       del.max_ids = MAX_DEL_LIST_LEN;
557    }
558    del.tot_ids = 0;
559 
560    del.JobId = (JobId_t *)malloc(sizeof(JobId_t) * del.max_ids);
561 
562    /* Now process same set but making a delete list */
563    Mmsg(query, "SELECT JobId FROM Job %s WHERE PurgedFiles=0 %s",
564         sql_from.c_str(), sql_where.c_str());
565    Dmsg1(050, "select sql=%s\n", query.c_str());
566    ua->db->SqlQuery(query.c_str(), FileDeleteHandler, (void *)&del);
567 
568    PurgeFilesFromJobList(ua, del);
569 
570    edit_uint64_with_commas(del.num_del, ed1);
571    ua->InfoMsg(_("Pruned Files from %s Jobs for client %s from catalog.\n"),
572       ed1, client->name());
573 
574 bail_out:
575    DbUnlock(ua->db);
576    if (del.JobId) {
577       free(del.JobId);
578    }
579    return 1;
580 }
581 
DropTempTables(UaContext * ua)582 static void DropTempTables(UaContext *ua)
583 {
584    ua->db->SqlQuery(BareosDb::SQL_QUERY_drop_deltabs);
585 }
586 
CreateTempTables(UaContext * ua)587 static bool CreateTempTables(UaContext *ua)
588 {
589    /* Create temp tables and indicies */
590    if (!ua->db->SqlQuery(BareosDb::SQL_QUERY_create_deltabs)) {
591       ua->ErrorMsg("%s", ua->db->strerror());
592       Dmsg0(050, "create DelTables table failed\n");
593       return false;
594    }
595 
596    if (!ua->db->SqlQuery(BareosDb::SQL_QUERY_create_delindex)) {
597        ua->ErrorMsg("%s", ua->db->strerror());
598        Dmsg0(050, "create DelInx1 index failed\n");
599        return false;
600    }
601 
602    return true;
603 }
604 
GrowDelList(struct del_ctx * del)605 static bool GrowDelList(struct del_ctx *del)
606 {
607    if (del->num_ids == MAX_DEL_LIST_LEN) {
608       return false;
609    }
610 
611    if (del->num_ids == del->max_ids) {
612       del->max_ids = (del->max_ids * 3) / 2;
613       del->JobId = (JobId_t *)brealloc(del->JobId, sizeof(JobId_t) * del->max_ids);
614       del->PurgedFiles = (char *)brealloc(del->PurgedFiles, del->max_ids);
615    }
616 
617    return true;
618 }
619 
620 struct accurate_check_ctx {
621    DBId_t ClientId;                   /* Id of client */
622    DBId_t FileSetId;                  /* Id of FileSet */
623 };
624 
625 /**
626  * row: Job.Name, FileSet, Client.Name, FileSetId, ClientId, Type
627  */
JobSelectHandler(void * ctx,int num_fields,char ** row)628 static int JobSelectHandler(void *ctx, int num_fields, char **row)
629 {
630    alist *lst = (alist *)ctx;
631    struct accurate_check_ctx *res;
632    ASSERT(num_fields == 6);
633 
634    /*
635     * If this job doesn't exist anymore in the configuration, delete it.
636     */
637    if (my_config->GetResWithName(R_JOB, row[0], false) == NULL) {
638       return 0;
639    }
640 
641    /*
642     * If this fileset doesn't exist anymore in the configuration, delete it.
643     */
644    if (my_config->GetResWithName(R_FILESET, row[1], false) == NULL) {
645       return 0;
646    }
647 
648    /*
649     * If this client doesn't exist anymore in the configuration, delete it.
650     */
651    if (my_config->GetResWithName(R_CLIENT, row[2], false) == NULL) {
652       return 0;
653    }
654 
655    /*
656     * Don't compute accurate things for Verify jobs
657     */
658    if (*row[5] == 'V') {
659       return 0;
660    }
661 
662    res = (struct accurate_check_ctx*) malloc(sizeof(struct accurate_check_ctx));
663    res->FileSetId = str_to_int64(row[3]);
664    res->ClientId = str_to_int64(row[4]);
665    lst->append(res);
666 
667 // Dmsg2(150, "row=%d val=%d\n", del->num_ids-1, del->JobId[del->num_ids-1]);
668    return 0;
669 }
670 
671 /**
672  * Pruning Jobs is a bit more complicated than purging Files
673  * because we delete Job records only if there is a more current
674  * backup of the FileSet. Otherwise, we keep the Job record.
675  * In other words, we never delete the only Job record that
676  * contains a current backup of a FileSet. This prevents the
677  * Volume from being recycled and destroying a current backup.
678  *
679  * For Verify Jobs, we do not delete the last InitCatalog.
680  *
681  * For Restore Jobs there are no restrictions.
682  */
PruneBackupJobs(UaContext * ua,ClientResource * client,PoolResource * pool)683 static bool PruneBackupJobs(UaContext *ua, ClientResource *client, PoolResource *pool)
684 {
685    PoolMem query(PM_MESSAGE);
686    PoolMem sql_where(PM_MESSAGE);
687    PoolMem sql_from(PM_MESSAGE);
688    utime_t period;
689    char ed1[50];
690    alist *jobids_check=NULL;
691    struct accurate_check_ctx *elt = nullptr;
692    db_list_ctx jobids, tempids;
693    JobDbRecord jr;
694    struct del_ctx del;
695    memset(&del, 0, sizeof(del));
696 
697    if (pool && pool->JobRetention > 0) {
698       period = pool->JobRetention;
699    } else if (client) {
700       period = client->JobRetention;
701    } else {                     /* should specify at least pool or client */
702       return false;
703    }
704 
705    DbLock(ua->db);
706    if (!prune_set_filter(ua, client, pool, period, &sql_from, &sql_where)) {
707       goto bail_out;
708    }
709 
710    /* Drop any previous temporary tables still there */
711    DropTempTables(ua);
712 
713    /* Create temp tables and indicies */
714    if (!CreateTempTables(ua)) {
715       goto bail_out;
716    }
717 
718    edit_utime(period, ed1, sizeof(ed1));
719    Jmsg(ua->jcr, M_INFO, 0, _("Begin pruning Jobs older than %s.\n"), ed1);
720 
721    del.max_ids = 100;
722    del.JobId = (JobId_t *)malloc(sizeof(JobId_t) * del.max_ids);
723    del.PurgedFiles = (char *)malloc(del.max_ids);
724 
725    /*
726     * Select all files that are older than the JobRetention period
727     * and add them into the "DeletionCandidates" table.
728     */
729    Mmsg(query,
730         "INSERT INTO DelCandidates "
731           "SELECT JobId,PurgedFiles,FileSetId,JobFiles,JobStatus "
732             "FROM Job %s "      /* JOIN Pool/Client */
733            "WHERE Type IN ('B', 'C', 'M', 'V',  'D', 'R', 'c', 'm', 'g') "
734              " %s ",            /* Pool/Client + JobTDate */
735         sql_from.c_str(), sql_where.c_str());
736 
737    Dmsg1(050, "select sql=%s\n", query.c_str());
738    if (!ua->db->SqlQuery(query.c_str())) {
739       if (ua->verbose) {
740          ua->ErrorMsg("%s", ua->db->strerror());
741       }
742       goto bail_out;
743    }
744 
745    /*
746     * Now, for the selection, we discard some of them in order to be always
747     * able to restore files. (ie, last full, last diff, last incrs)
748     * Note: The DISTINCT could be more useful if we don't get FileSetId
749     */
750    jobids_check = New(alist(10, owned_by_alist));
751    Mmsg(query,
752 "SELECT DISTINCT Job.Name, FileSet, Client.Name, Job.FileSetId, "
753                 "Job.ClientId, Job.Type "
754   "FROM DelCandidates "
755        "JOIN Job USING (JobId) "
756        "JOIN Client USING (ClientId) "
757        "JOIN FileSet ON (Job.FileSetId = FileSet.FileSetId) "
758  "WHERE Job.Type IN ('B') "               /* Look only Backup jobs */
759    "AND Job.JobStatus IN ('T', 'W') "     /* Look only useful jobs */
760       );
761 
762    /*
763     * The JobSelectHandler will skip jobs or filesets that are no longer
764     * in the configuration file. Interesting ClientId/FileSetId will be
765     * added to jobids_check.
766     */
767    if (!ua->db->SqlQuery(query.c_str(), JobSelectHandler, jobids_check)) {
768       ua->ErrorMsg("%s", ua->db->strerror());
769    }
770 
771    /*
772     * For this selection, we exclude current jobs used for restore or
773     * accurate. This will prevent to prune the last full backup used for
774     * current backup & restore
775     */
776    memset(&jr, 0, sizeof(jr));
777 
778    /*
779     * To find useful jobs, we do like an incremental
780     */
781    jr.JobLevel = L_INCREMENTAL;
782    foreach_alist(elt, jobids_check) {
783       jr.ClientId = elt->ClientId;   /* should be always the same */
784       jr.FileSetId = elt->FileSetId;
785       ua->db->AccurateGetJobids(ua->jcr, &jr, &tempids);
786       jobids.add(tempids);
787    }
788 
789    /*
790     * Discard latest Verify level=InitCatalog job
791     * TODO: can have multiple fileset
792     */
793    Mmsg(query,
794         "SELECT JobId, JobTDate "
795           "FROM Job %s "                         /* JOIN Client/Pool */
796          "WHERE Type='V'    AND Level='V' "
797               " %s "                             /* Pool, JobTDate, Client */
798          "ORDER BY JobTDate DESC LIMIT 1",
799         sql_from.c_str(), sql_where.c_str());
800 
801    if (!ua->db->SqlQuery(query.c_str(), DbListHandler, &jobids)) {
802       ua->ErrorMsg("%s", ua->db->strerror());
803    }
804 
805    /* If we found jobs to exclude from the DelCandidates list, we should
806     * also remove BaseJobs that can be linked with them
807     */
808    if (jobids.count > 0) {
809       Dmsg1(60, "jobids to exclude before basejobs = %s\n", jobids.list);
810       /* We also need to exclude all basejobs used */
811       ua->db->GetUsedBaseJobids(ua->jcr, jobids.list, &jobids);
812 
813       /* Removing useful jobs from the DelCandidates list */
814       Mmsg(query, "DELETE FROM DelCandidates "
815                    "WHERE JobId IN (%s) "        /* JobId used in accurate */
816                      "AND JobFiles!=0",          /* Discard when JobFiles=0 */
817            jobids.list);
818 
819       if (!ua->db->SqlQuery(query.c_str())) {
820          ua->ErrorMsg("%s", ua->db->strerror());
821          goto bail_out;         /* Don't continue if the list isn't clean */
822       }
823       Dmsg1(60, "jobids to exclude = %s\n", jobids.list);
824    }
825 
826    /* We use DISTINCT because we can have two times the same job */
827    Mmsg(query,
828         "SELECT DISTINCT DelCandidates.JobId,DelCandidates.PurgedFiles "
829           "FROM DelCandidates");
830    if (!ua->db->SqlQuery(query.c_str(), JobDeleteHandler, (void *)&del)) {
831       ua->ErrorMsg("%s", ua->db->strerror());
832    }
833 
834    PurgeJobListFromCatalog(ua, del);
835 
836    if (del.num_del > 0) {
837       ua->InfoMsg(_("Pruned %d %s for client %s from catalog.\n"), del.num_del,
838          del.num_del==1?_("Job"):_("Jobs"), client->name());
839     } else if (ua->verbose) {
840        ua->InfoMsg(_("No Jobs found to prune.\n"));
841     }
842 
843 bail_out:
844    DropTempTables(ua);
845    DbUnlock(ua->db);
846    if (del.JobId) {
847       free(del.JobId);
848    }
849    if (del.PurgedFiles) {
850       free(del.PurgedFiles);
851    }
852    if (jobids_check) {
853       delete jobids_check;
854    }
855    return 1;
856 }
857 
858 /**
859  * Dispatch to the right prune jobs function.
860  */
PruneJobs(UaContext * ua,ClientResource * client,PoolResource * pool,int JobType)861 bool PruneJobs(UaContext *ua, ClientResource *client, PoolResource *pool, int JobType)
862 {
863    switch (JobType) {
864    case JT_BACKUP:
865       return PruneBackupJobs(ua, client, pool);
866    default:
867       return true;
868    }
869 }
870 
871 /**
872  * Prune a given Volume
873  */
PruneVolume(UaContext * ua,MediaDbRecord * mr)874 bool PruneVolume(UaContext *ua, MediaDbRecord *mr)
875 {
876    PoolMem query(PM_MESSAGE);
877    struct del_ctx del;
878    bool ok = false;
879    int count;
880 
881    if (mr->Enabled == VOL_ARCHIVED) {
882       return false;                   /* Cannot prune archived volumes */
883    }
884 
885    memset(&del, 0, sizeof(del));
886    del.max_ids = 10000;
887    del.JobId = (JobId_t *)malloc(sizeof(JobId_t) * del.max_ids);
888 
889    DbLock(ua->db);
890 
891    /* Prune only Volumes with status "Full", or "Used" */
892    if (bstrcmp(mr->VolStatus, "Full") ||
893        bstrcmp(mr->VolStatus, "Used")) {
894       Dmsg2(050, "get prune list MediaId=%d Volume %s\n", (int)mr->MediaId, mr->VolumeName);
895       count = GetPruneListForVolume(ua, mr, &del);
896       Dmsg1(050, "Num pruned = %d\n", count);
897       if (count != 0) {
898          PurgeJobListFromCatalog(ua, del);
899       }
900       ok = IsVolumePurged(ua, mr);
901    }
902 
903    DbUnlock(ua->db);
904    if (del.JobId) {
905       free(del.JobId);
906    }
907    return ok;
908 }
909 
910 /**
911  * Get prune list for a volume
912  */
GetPruneListForVolume(UaContext * ua,MediaDbRecord * mr,del_ctx * del)913 int GetPruneListForVolume(UaContext *ua, MediaDbRecord *mr, del_ctx *del)
914 {
915    PoolMem query(PM_MESSAGE);
916    int count = 0;
917    utime_t now, period;
918    char ed1[50], ed2[50];
919 
920    if (mr->Enabled == VOL_ARCHIVED) {
921       return 0;                    /* cannot prune Archived volumes */
922    }
923 
924    /*
925     * Now add to the  list of JobIds for Jobs written to this Volume
926     */
927    period = mr->VolRetention;
928    now = (utime_t)time(NULL);
929    ua->db->FillQuery(query, BareosDb::SQL_QUERY_sel_JobMedia, edit_int64(mr->MediaId, ed1), edit_int64(now-period, ed2));
930 
931    Dmsg3(250, "Now=%d period=%d now-period=%s\n", (int)now, (int)period, ed2);
932    Dmsg1(050, "Query=%s\n", query.c_str());
933 
934    if (!ua->db->SqlQuery(query.c_str(), FileDeleteHandler, (void *)del)) {
935       if (ua->verbose) {
936          ua->ErrorMsg("%s", ua->db->strerror());
937       }
938       Dmsg0(050, "Count failed\n");
939       goto bail_out;
940    }
941    count = ExcludeRunningJobsFromList(del);
942 
943 bail_out:
944    return count;
945 }
946 
947 /**
948  * We have a list of jobs to prune or purge. If any of them is
949  *   currently running, we set its JobId to zero which effectively
950  *   excludes it.
951  *
952  * Returns the number of jobs that can be prunned or purged.
953  */
ExcludeRunningJobsFromList(del_ctx * prune_list)954 int ExcludeRunningJobsFromList(del_ctx *prune_list)
955 {
956    int count = 0;
957    JobControlRecord *jcr;
958    bool skip;
959    int i;
960 
961    /* Do not prune any job currently running */
962    for (i=0; i < prune_list->num_ids; i++) {
963       skip = false;
964       foreach_jcr(jcr) {
965          if (jcr->JobId == prune_list->JobId[i]) {
966             Dmsg2(050, "skip running job JobId[%d]=%d\n", i, (int)prune_list->JobId[i]);
967             prune_list->JobId[i] = 0;
968             skip = true;
969             break;
970          }
971       }
972       endeach_jcr(jcr);
973       if (skip) {
974          continue;  /* don't increment count */
975       }
976       Dmsg2(050, "accept JobId[%d]=%d\n", i, (int)prune_list->JobId[i]);
977       count++;
978    }
979    return count;
980 }
981 } /* namespace directordaemon */
982