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