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