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