1 /*
2 Bacula(R) - The Network Backup Solution
3
4 Copyright (C) 2000-2020 Kern Sibbald
5
6 The original author of Bacula is Kern Sibbald, with contributions
7 from many others, a complete list can be found in the file AUTHORS.
8
9 You may use this file and others of this release according to the
10 license defined in the LICENSE file, which includes the Affero General
11 Public License, v3.0 ("AGPLv3") and some additional permissions and
12 terms pursuant to its AGPLv3 Section 7.
13
14 This notice must be preserved when any source code is
15 conveyed and/or propagated.
16
17 Bacula(R) is a registered trademark of Kern Sibbald.
18 */
19 /*
20 *
21 * Bacula Director -- User Agent Database prune Command
22 * Applies retention periods
23 *
24 * Kern Sibbald, February MMII
25 *
26 */
27
28 #include "bacula.h"
29 #include "dird.h"
30
31 /* Imported functions */
32
33 /* Forward referenced functions */
34 static bool grow_del_list(struct del_ctx *del);
35 static bool prune_expired_volumes(UAContext*);
36 static bool prune_selected_volumes(UAContext *ua);
37
38 /*
39 * Called here to count entries to be deleted
40 */
del_count_handler(void * ctx,int num_fields,char ** row)41 int del_count_handler(void *ctx, int num_fields, char **row)
42 {
43 struct s_count_ctx *cnt = (struct s_count_ctx *)ctx;
44
45 if (row[0]) {
46 cnt->count = str_to_int64(row[0]);
47 } else {
48 cnt->count = 0;
49 }
50 return 0;
51 }
52
53
54 /*
55 * Called here to make in memory list of JobIds to be
56 * deleted and the associated PurgedFiles flag.
57 * The in memory list will then be transversed
58 * to issue the SQL DELETE commands. Note, the list
59 * is allowed to get to MAX_DEL_LIST_LEN to limit the
60 * maximum malloc'ed memory.
61 */
job_delete_handler(void * ctx,int num_fields,char ** row)62 int job_delete_handler(void *ctx, int num_fields, char **row)
63 {
64 struct del_ctx *del = (struct del_ctx *)ctx;
65
66 if (!grow_del_list(del)) {
67 return 1;
68 }
69 del->JobId[del->num_ids] = (JobId_t)str_to_int64(row[0]);
70 Dmsg2(60, "job_delete_handler row=%d val=%d\n", del->num_ids, del->JobId[del->num_ids]);
71 del->PurgedFiles[del->num_ids++] = (char)str_to_int64(row[1]);
72 return 0;
73 }
74
file_delete_handler(void * ctx,int num_fields,char ** row)75 int file_delete_handler(void *ctx, int num_fields, char **row)
76 {
77 struct del_ctx *del = (struct del_ctx *)ctx;
78
79 if (!grow_del_list(del)) {
80 return 1;
81 }
82 del->JobId[del->num_ids++] = (JobId_t)str_to_int64(row[0]);
83 // Dmsg2(150, "row=%d val=%d\n", del->num_ids-1, del->JobId[del->num_ids-1]);
84 return 0;
85 }
86
87 /* Prune jobs or files for all combinations of Client/Pool that we
88 * can find in the Job table. Doing so, the pruning will not prune a
89 * job that is needed to restore the client. As the command will detect
90 * all parameters automatically, it is very convenient to schedule it a
91 * couple of times per day.
92 */
prune_all_clients_and_pools(UAContext * ua,int kw)93 static int prune_all_clients_and_pools(UAContext *ua, int kw)
94 {
95 alist results(owned_by_alist, 100);
96 POOL_MEM label;
97 CLIENT *client;
98 POOL *pool;
99
100 /* Get the combination of all Client/Pool in the Job table (respecting the ACLs) */
101 if (!db_get_client_pool(ua->jcr, ua->db, &results)) {
102 ua->error_msg(_("Unable to list Client/Pool. ERR=%s\n"), ua->db->errmsg);
103 return false;
104 }
105 while (!results.empty()) {
106 /* Each "record" is made of two values in results */
107 char *pool_s = (char *)results.pop();
108 char *client_s = (char *)results.pop();
109 Dmsg2(100, "Trying to prune %s/%s\n", client_s, pool_s);
110
111 if (!pool_s || !client_s) { /* Just in case */
112 ua->error_msg(_("Unable to list Client/Pool %s/%s\n"),
113 NPRTB(client_s), NPRTB(pool_s));
114 bfree_and_null(pool_s);
115 bfree_and_null(client_s);
116 return false;
117 }
118
119 /* Make sure the client and the pool are still defined */
120 client = (CLIENT *)GetResWithName(R_CLIENT, client_s);
121 pool = (POOL *)GetResWithName(R_POOL, pool_s);
122 if (!client || !pool) {
123 Dmsg2(10, "Skip pruning of %s/%s, one resource is missing\n", client_s, pool_s);
124 }
125 free(client_s);
126 free(pool_s);
127 if (!client || !pool) {
128 continue;
129 }
130
131 /* Display correct messages and do the actual pruning */
132 if (kw == 0) {
133 ua->info_msg(_("Pruning Files for Client %s with Pool %s...\n"),
134 client->name(), pool->name());
135 if (pool->FileRetention > 0) {
136 Mmsg(label, "Pool %s File", pool->name());
137 if (!confirm_retention(ua, &pool->FileRetention, label.c_str())) {
138 return false;
139 }
140 } else {
141 Mmsg(label, "Client %s File", client->name());
142 if (!confirm_retention(ua, &client->FileRetention, label.c_str())) {
143 return false;
144 }
145 }
146 prune_files(ua, client, pool);
147 }
148 if (kw == 1) {
149 ua->info_msg(_("Pruning Jobs for Client %s with Pool %s...\n"),
150 client->name(), pool->name());
151 if (pool->JobRetention > 0) {
152 Mmsg(label, "Pool %s Job", pool->name());
153 if (!confirm_retention(ua, &pool->JobRetention, label.c_str())) {
154 return false;
155 }
156 } else {
157 Mmsg(label, "Client %s Job", client->name());
158 if (!confirm_retention(ua, &client->JobRetention, label.c_str())) {
159 return false;
160 }
161 }
162 prune_jobs(ua, client, pool, JT_BACKUP);
163 }
164 }
165 return true;
166 }
167
do_prune_events(JCR * jcr,BDB * db,int retention)168 int do_prune_events(JCR *jcr, BDB *db, int retention)
169 {
170 char ed1[50];
171 POOL_MEM query(PM_MESSAGE);
172 utime_t now = (utime_t)time(NULL);
173
174 db_lock(db);
175 Mmsg(query, "DELETE FROM Events WHERE EventsTime < '%s'",
176 bstrutime(ed1, sizeof(ed1), now - retention));
177 db_sql_query(db, query.c_str(), NULL, NULL);
178 db_unlock(db);
179 return 0;
180 }
181
182 /* Prune Events records from the database.
183 * We cannot change the retention time, only a modification
184 * of the configuration can do that.
185 */
prune_events(UAContext * ua,int retention)186 static int prune_events(UAContext *ua, int retention)
187 {
188 char ed1[50];
189 ua->info_msg(_("The current Events retention period is: %s\n"),
190 edit_utime(retention, ed1, sizeof(ed1)));
191
192 if (find_arg(ua, _("yes")) < 0) {
193 if (!get_yesno(ua, _("Continue? (yes/no): "))) {
194 return 0;
195 }
196
197 if (ua->pint32_val != 1) {
198 return 0;
199 }
200 }
201
202 do_prune_events(ua->jcr, ua->db, retention);
203 ua->info_msg(_("Pruned Events from catalog.\n"));
204
205 return true;
206 }
207
208 /*
209 * Prune records from database
210 *
211 * prune files (from) client=xxx [pool=yyy]
212 * prune jobs (from) client=xxx [pool=yyy]
213 * prune volume=xxx
214 * prune stats
215 */
prunecmd(UAContext * ua,const char * cmd)216 int prunecmd(UAContext *ua, const char *cmd)
217 {
218 DIRRES *dir;
219 CLIENT *client;
220 POOL *pool;
221 POOL_DBR pr;
222 MEDIA_DBR mr;
223 utime_t retention;
224 int kw;
225
226 static const char *keywords[] = {
227 NT_("Files"),
228 NT_("Jobs"),
229 NT_("Volume"),
230 NT_("Stats"),
231 NT_("Snapshots"),
232 NT_("Events"),
233 NULL};
234
235 if (!open_new_client_db(ua)) {
236 return false;
237 }
238
239 /* First search args */
240 kw = find_arg_keyword(ua, keywords);
241 if (kw < 0 || kw > 5) {
242 /* no args, so ask user */
243 kw = do_keyword_prompt(ua, _("Choose item to prune"), keywords);
244 }
245
246 /* prune files/jobs all (prune all Client/Pool automatically) */
247 if ((kw == 0 || kw == 1) && find_arg(ua, _("all")) > 0) {
248 return prune_all_clients_and_pools(ua, kw);
249 }
250
251 switch (kw) {
252 case 0: /* prune files */
253 /* We restrict the client list to ClientAcl, maybe something to change later */
254 if (!(client = get_client_resource(ua, JT_SYSTEM))) {
255 return false;
256 }
257 if (find_arg_with_value(ua, "pool") >= 0) {
258 pool = get_pool_resource(ua);
259 } else {
260 pool = NULL;
261 }
262 /* Pool File Retention takes precedence over client File Retention */
263 if (pool && pool->FileRetention > 0) {
264 if (!confirm_retention(ua, &pool->FileRetention, "File")) {
265 return false;
266 }
267 } else if (!confirm_retention(ua, &client->FileRetention, "File")) {
268 return false;
269 }
270 prune_files(ua, client, pool);
271 return true;
272
273 case 1: /* prune jobs */
274 /* We restrict the client list to ClientAcl, maybe something to change later */
275 if (!(client = get_client_resource(ua, JT_SYSTEM))) {
276 return false;
277 }
278 if (find_arg_with_value(ua, "pool") >= 0) {
279 pool = get_pool_resource(ua);
280 } else {
281 pool = NULL;
282 }
283 /* Pool Job Retention takes precedence over client Job Retention */
284 if (pool && pool->JobRetention > 0) {
285 if (!confirm_retention(ua, &pool->JobRetention, "Job")) {
286 return false;
287 }
288 } else if (!confirm_retention(ua, &client->JobRetention, "Job")) {
289 return false;
290 }
291 /* ****FIXME**** allow user to select JobType */
292 prune_jobs(ua, client, pool, JT_BACKUP);
293 return 1;
294
295 case 2: /* prune volume */
296
297 /* Look for All expired volumes, mostly designed for runscript */
298 if (find_arg(ua, "expired") >= 0) {
299 return prune_expired_volumes(ua);
300 }
301 prune_selected_volumes(ua);
302 return true;
303 case 3: /* prune stats */
304 dir = (DIRRES *)GetNextRes(R_DIRECTOR, NULL);
305 if (!dir->stats_retention) {
306 return false;
307 }
308 retention = dir->stats_retention;
309 if (!confirm_retention(ua, &retention, "Statistics")) {
310 return false;
311 }
312 prune_stats(ua, retention);
313 return true;
314 case 4: /* prune snapshots */
315 prune_snapshot(ua);
316 return true;
317 case 5: /* prune events */
318 dir = (DIRRES *)GetNextRes(R_DIRECTOR, NULL);
319 if (!dir->stats_retention) {
320 return false;
321 }
322 retention = dir->events_retention;
323 prune_events(ua, retention);
324 return true;
325 default:
326 break;
327 }
328
329 return true;
330 }
331
332 /* Prune Job stat records from the database.
333 *
334 */
prune_stats(UAContext * ua,utime_t retention)335 int prune_stats(UAContext *ua, utime_t retention)
336 {
337 char ed1[50];
338 POOL_MEM query(PM_MESSAGE);
339 utime_t now = (utime_t)time(NULL);
340
341 db_lock(ua->db);
342 Mmsg(query, "DELETE FROM JobHisto WHERE JobTDate < %s",
343 edit_int64(now - retention, ed1));
344 db_sql_query(ua->db, query.c_str(), NULL, NULL);
345 db_unlock(ua->db);
346
347 ua->info_msg(_("Pruned Jobs from JobHisto catalog.\n"));
348
349 return true;
350 }
351
352 /*
353 * Use pool and client specified by user to select jobs to prune
354 * returns add_from string to add in FROM clause
355 * add_where string to add in WHERE clause
356 */
prune_set_filter(UAContext * ua,CLIENT * client,POOL * pool,utime_t period,POOL_MEM * add_from,POOL_MEM * add_where)357 bool prune_set_filter(UAContext *ua, CLIENT *client, POOL *pool, utime_t period,
358 POOL_MEM *add_from, POOL_MEM *add_where)
359 {
360 utime_t now;
361 char ed1[50], ed2[MAX_ESCAPE_NAME_LENGTH];
362 POOL_MEM tmp(PM_MESSAGE);
363
364 now = (utime_t)time(NULL);
365 edit_int64(now - period, ed1);
366 Dmsg3(150, "now=%lld period=%lld JobTDate=%s\n", now, period, ed1);
367 Mmsg(tmp, " AND JobTDate < %s ", ed1);
368 pm_strcat(*add_where, tmp.c_str());
369
370 db_lock(ua->db);
371 if (client) {
372 db_escape_string(ua->jcr, ua->db, ed2,
373 client->name(), strlen(client->name()));
374 Mmsg(tmp, " AND Client.Name = '%s' ", ed2);
375 pm_strcat(*add_where, tmp.c_str());
376 pm_strcat(*add_from, " JOIN Client USING (ClientId) ");
377 }
378
379 if (pool) {
380 db_escape_string(ua->jcr, ua->db, ed2,
381 pool->name(), strlen(pool->name()));
382 Mmsg(tmp, " AND Pool.Name = '%s' ", ed2);
383 pm_strcat(*add_where, tmp.c_str());
384 /* Use ON() instead of USING for some old SQLite */
385 pm_strcat(*add_from, " JOIN Pool ON (Job.PoolId = Pool.PoolId) ");
386 }
387 Dmsg2(150, "f=%s w=%s\n", add_from->c_str(), add_where->c_str());
388 db_unlock(ua->db);
389 return true;
390 }
391
392 /*
393 * Prune File records from the database. For any Job which
394 * is older than the retention period, we unconditionally delete
395 * all File records for that Job. This is simple enough that no
396 * temporary tables are needed. We simply make an in memory list of
397 * the JobIds meeting the prune conditions, then delete all File records
398 * pointing to each of those JobIds.
399 *
400 * This routine assumes you want the pruning to be done. All checking
401 * must be done before calling this routine.
402 *
403 * Note: client or pool can possibly be NULL (not both).
404 */
prune_files(UAContext * ua,CLIENT * client,POOL * pool)405 int prune_files(UAContext *ua, CLIENT *client, POOL *pool)
406 {
407 struct del_ctx del;
408 struct s_count_ctx cnt;
409 POOL_MEM query(PM_MESSAGE);
410 POOL_MEM sql_where(PM_MESSAGE);
411 POOL_MEM sql_from(PM_MESSAGE);
412 utime_t period;
413 char ed1[50];
414
415 memset(&del, 0, sizeof(del));
416
417 if (pool && pool->FileRetention > 0) {
418 period = pool->FileRetention;
419
420 } else if (client) {
421 period = client->FileRetention;
422
423 } else { /* should specify at least pool or client */
424 return false;
425 }
426
427 db_lock(ua->db);
428 /* Specify JobTDate and Pool.Name= and/or Client.Name= in the query */
429 if (!prune_set_filter(ua, client, pool, period, &sql_from, &sql_where)) {
430 goto bail_out;
431 }
432
433 // edit_utime(now-period, ed1, sizeof(ed1));
434 // Jmsg(ua->jcr, M_INFO, 0, _("Begin pruning Jobs older than %s secs.\n"), ed1)
435 if (ua->jcr->getJobType() != JT_CONSOLE) {
436 Jmsg(ua->jcr, M_INFO, 0, _("Begin pruning Files.\n"));
437 }
438 /* Select Jobs -- for counting */
439 Mmsg(query,
440 "SELECT COUNT(1) FROM Job %s WHERE PurgedFiles=0 %s",
441 sql_from.c_str(), sql_where.c_str());
442 Dmsg1(100, "select sql=%s\n", query.c_str());
443 cnt.count = 0;
444 if (!db_sql_query(ua->db, query.c_str(), del_count_handler, (void *)&cnt)) {
445 ua->error_msg("%s", db_strerror(ua->db));
446 Dmsg0(100, "Count failed\n");
447 goto bail_out;
448 }
449
450 if (cnt.count == 0) {
451 if (ua->verbose) {
452 ua->warning_msg(_("No Files found to prune.\n"));
453 }
454 goto bail_out;
455 }
456
457 if (cnt.count < MAX_DEL_LIST_LEN) {
458 del.max_ids = cnt.count + 1;
459 } else {
460 del.max_ids = MAX_DEL_LIST_LEN;
461 }
462 del.tot_ids = 0;
463
464 del.JobId = (JobId_t *)malloc(sizeof(JobId_t) * del.max_ids);
465
466 /* Now process same set but making a delete list */
467 Mmsg(query, "SELECT JobId FROM Job %s WHERE PurgedFiles=0 %s",
468 sql_from.c_str(), sql_where.c_str());
469 Dmsg1(100, "select sql=%s\n", query.c_str());
470 db_sql_query(ua->db, query.c_str(), file_delete_handler, (void *)&del);
471
472 purge_files_from_job_list(ua, del);
473
474 edit_uint64_with_commas(del.num_del, ed1);
475 ua->info_msg(_("Pruned Files from %s Jobs for client %s from catalog.\n"),
476 ed1, client->name());
477
478 bail_out:
479 db_unlock(ua->db);
480 if (del.JobId) {
481 free(del.JobId);
482 }
483 return 1;
484 }
485
486
drop_temp_tables(UAContext * ua)487 static void drop_temp_tables(UAContext *ua)
488 {
489 int i;
490 for (i=0; drop_deltabs[i]; i++) {
491 db_sql_query(ua->db, drop_deltabs[i], NULL, (void *)NULL);
492 }
493 }
494
create_temp_tables(UAContext * ua)495 static bool create_temp_tables(UAContext *ua)
496 {
497 /* Create temp tables and indicies */
498 if (!db_sql_query(ua->db, create_deltabs[ua->db->bdb_get_type_index()], NULL, (void *)NULL)) {
499 ua->error_msg("%s", db_strerror(ua->db));
500 Dmsg0(100, "create DelTables table failed\n");
501 return false;
502 }
503 if (!db_sql_query(ua->db, create_delindex, NULL, (void *)NULL)) {
504 ua->error_msg("%s", db_strerror(ua->db));
505 Dmsg0(100, "create DelInx1 index failed\n");
506 return false;
507 }
508 return true;
509 }
510
grow_del_list(struct del_ctx * del)511 static bool grow_del_list(struct del_ctx *del)
512 {
513 if (del->num_ids == MAX_DEL_LIST_LEN) {
514 return false;
515 }
516
517 if (del->num_ids == del->max_ids) {
518 del->max_ids = (del->max_ids * 3) / 2;
519 del->JobId = (JobId_t *)brealloc(del->JobId, sizeof(JobId_t) *
520 del->max_ids);
521 del->PurgedFiles = (char *)brealloc(del->PurgedFiles, del->max_ids);
522 }
523 return true;
524 }
525
526 struct accurate_check_ctx {
527 DBId_t ClientId; /* Id of client */
528 DBId_t FileSetId; /* Id of FileSet */
529 };
530
531 /* row: Job.Name, FileSet, Client.Name, FileSetId, ClientId, Type */
job_select_handler(void * ctx,int num_fields,char ** row)532 static int job_select_handler(void *ctx, int num_fields, char **row)
533 {
534 alist *lst = (alist *)ctx;
535 struct accurate_check_ctx *res;
536 ASSERT(num_fields == 6);
537
538 /* Quick fix for #5507, avoid locking res_head after db_lock() */
539
540 #ifdef bug5507
541 /* If this job doesn't exist anymore in the configuration, delete it */
542 if (GetResWithName(R_JOB, row[0]) == NULL) {
543 return 0;
544 }
545
546 /* If this fileset doesn't exist anymore in the configuration, delete it */
547 if (GetResWithName(R_FILESET, row[1]) == NULL) {
548 return 0;
549 }
550
551 /* If this client doesn't exist anymore in the configuration, delete it */
552 if (GetResWithName(R_CLIENT, row[2]) == NULL) {
553 return 0;
554 }
555 #endif
556
557 /* Don't compute accurate things for Verify jobs */
558 if (*row[5] == 'V') {
559 return 0;
560 }
561
562 res = (struct accurate_check_ctx*) malloc(sizeof(struct accurate_check_ctx));
563 res->FileSetId = str_to_int64(row[3]);
564 res->ClientId = str_to_int64(row[4]);
565 lst->append(res);
566
567 // Dmsg2(150, "row=%d val=%d\n", del->num_ids-1, del->JobId[del->num_ids-1]);
568 return 0;
569 }
570
571 /*
572 * Pruning Jobs is a bit more complicated than purging Files
573 * because we delete Job records only if there is a more current
574 * backup of the FileSet. Otherwise, we keep the Job record.
575 * In other words, we never delete the only Job record that
576 * contains a current backup of a FileSet. This prevents the
577 * Volume from being recycled and destroying a current backup.
578 *
579 * For Verify Jobs, we do not delete the last InitCatalog.
580 *
581 * For Restore Jobs there are no restrictions.
582 */
prune_jobs(UAContext * ua,CLIENT * client,POOL * pool,int JobType)583 int prune_jobs(UAContext *ua, CLIENT *client, POOL *pool, int JobType)
584 {
585 POOL_MEM query(PM_MESSAGE);
586 POOL_MEM sql_where(PM_MESSAGE);
587 POOL_MEM sql_from(PM_MESSAGE);
588 utime_t period;
589 char ed1[50];
590 alist *jobids_check=NULL;
591 struct accurate_check_ctx *elt;
592 db_list_ctx jobids, tempids;
593 JOB_DBR jr;
594 struct del_ctx del;
595 memset(&del, 0, sizeof(del));
596
597 if (pool && pool->JobRetention > 0) {
598 period = pool->JobRetention;
599
600 } else if (client) {
601 period = client->JobRetention;
602
603 } else { /* should specify at least pool or client */
604 return false;
605 }
606
607 db_lock(ua->db);
608 if (!prune_set_filter(ua, client, pool, period, &sql_from, &sql_where)) {
609 goto bail_out;
610 }
611
612 /* Drop any previous temporary tables still there */
613 drop_temp_tables(ua);
614
615 /* Create temp tables and indicies */
616 if (!create_temp_tables(ua)) {
617 goto bail_out;
618 }
619
620 if (ua->jcr->getJobType() != JT_CONSOLE) {
621 edit_utime(period, ed1, sizeof(ed1));
622 Jmsg(ua->jcr, M_INFO, 0, _("Begin pruning Jobs older than %s.\n"), ed1);
623 }
624
625 del.max_ids = 100;
626 del.JobId = (JobId_t *)malloc(sizeof(JobId_t) * del.max_ids);
627 del.PurgedFiles = (char *)malloc(del.max_ids);
628
629 /*
630 * Select all files that are older than the JobRetention period
631 * and add them into the "DeletionCandidates" table.
632 */
633 Mmsg(query,
634 "INSERT INTO DelCandidates(JobId, PurgedFiles, FileSetId, JobFiles, JobStatus) "
635 "SELECT JobId,PurgedFiles,FileSetId,JobFiles,JobStatus "
636 "FROM Job %s " /* JOIN Pool/Client */
637 "WHERE Type IN ('B', 'C', 'M', 'V', 'D', 'R', 'c', 'm', 'g') "
638 " %s ", /* Pool/Client + JobTDate */
639 sql_from.c_str(), sql_where.c_str());
640
641 Dmsg1(100, "select sql=%s\n", query.c_str());
642 if (!db_sql_query(ua->db, query.c_str(), NULL, (void *)NULL)) {
643 if (ua->verbose) {
644 ua->error_msg("%s", db_strerror(ua->db));
645 }
646 goto bail_out;
647 }
648
649 /* Now, for the selection, we discard some of them in order to be always
650 * able to restore files. (ie, last full, last diff, last incrs)
651 * Note: The DISTINCT could be more useful if we don't get FileSetId
652 */
653 jobids_check = New(alist(10, owned_by_alist));
654 Mmsg(query,
655 "SELECT DISTINCT Job.Name, FileSet, Client.Name, Job.FileSetId, "
656 "Job.ClientId, Job.Type "
657 "FROM DelCandidates "
658 "JOIN Job USING (JobId) "
659 "JOIN Client USING (ClientId) "
660 "JOIN FileSet ON (Job.FileSetId = FileSet.FileSetId) "
661 "WHERE Job.Type IN ('B') " /* Look only Backup jobs */
662 "AND Job.JobStatus IN ('T', 'W') " /* Look only useful jobs */
663 );
664
665 /* The job_select_handler will skip jobs or filesets that are no longer
666 * in the configuration file. Interesting ClientId/FileSetId will be
667 * added to jobids_check (currently disabled in 6.0.7b)
668 */
669 if (!db_sql_query(ua->db, query.c_str(), job_select_handler, jobids_check)) {
670 ua->error_msg("%s", db_strerror(ua->db));
671 }
672
673 /* For this selection, we exclude current jobs used for restore or
674 * accurate. This will prevent to prune the last full backup used for
675 * current backup & restore
676 */
677 memset(&jr, 0, sizeof(jr));
678 /* To find useful jobs, we do like an incremental */
679 jr.JobLevel = L_INCREMENTAL;
680 foreach_alist(elt, jobids_check) {
681 jr.ClientId = elt->ClientId; /* should be always the same */
682 jr.FileSetId = elt->FileSetId;
683 db_get_accurate_jobids(ua->jcr, ua->db, &jr, &tempids);
684 jobids.add(tempids);
685 }
686
687 /* Discard latest Verify level=InitCatalog job
688 * TODO: can have multiple fileset
689 */
690 Mmsg(query,
691 "SELECT JobId, JobTDate "
692 "FROM Job %s " /* JOIN Client/Pool */
693 "WHERE Type='V' AND Level='V' "
694 " %s " /* Pool, JobTDate, Client */
695 "ORDER BY JobTDate DESC LIMIT 1",
696 sql_from.c_str(), sql_where.c_str());
697
698 if (!db_sql_query(ua->db, query.c_str(), db_list_handler, &jobids)) {
699 ua->error_msg("%s", db_strerror(ua->db));
700 }
701
702 /* If we found jobs to exclude from the DelCandidates list, we should
703 * also remove BaseJobs that can be linked with them
704 */
705 if (jobids.count > 0) {
706 Dmsg1(60, "jobids to exclude before basejobs = %s\n", jobids.list);
707 /* We also need to exclude all basejobs used */
708 db_get_used_base_jobids(ua->jcr, ua->db, jobids.list, &jobids);
709
710 /* Removing useful jobs from the DelCandidates list */
711 Mmsg(query, "DELETE FROM DelCandidates "
712 "WHERE JobId IN (%s) " /* JobId used in accurate */
713 "AND JobFiles!=0", /* Discard when JobFiles=0 */
714 jobids.list);
715
716 if (!db_sql_query(ua->db, query.c_str(), NULL, NULL)) {
717 ua->error_msg("%s", db_strerror(ua->db));
718 goto bail_out; /* Don't continue if the list isn't clean */
719 }
720 Dmsg1(60, "jobids to exclude = %s\n", jobids.list);
721 }
722
723 /* We use DISTINCT because we can have two times the same job */
724 Mmsg(query,
725 "SELECT DISTINCT DelCandidates.JobId,DelCandidates.PurgedFiles "
726 "FROM DelCandidates");
727 if (!db_sql_query(ua->db, query.c_str(), job_delete_handler, (void *)&del)) {
728 ua->error_msg("%s", db_strerror(ua->db));
729 }
730
731 purge_job_list_from_catalog(ua, del);
732
733 if (del.num_del > 0) {
734 ua->info_msg(_("Pruned %d %s for client %s from catalog.\n"), del.num_del,
735 del.num_del==1?_("Job"):_("Jobs"), client->name());
736 } else if (ua->verbose) {
737 ua->info_msg(_("No Jobs found to prune.\n"));
738 }
739
740 bail_out:
741 drop_temp_tables(ua);
742 db_unlock(ua->db);
743 if (del.JobId) {
744 free(del.JobId);
745 }
746 if (del.PurgedFiles) {
747 free(del.PurgedFiles);
748 }
749 if (jobids_check) {
750 delete jobids_check;
751 }
752 return 1;
753 }
754
prune_selected_volumes(UAContext * ua)755 static bool prune_selected_volumes(UAContext *ua)
756 {
757 int nb=0;
758 uint32_t *results=NULL;
759 MEDIA_DBR mr;
760 POOL_DBR pr;
761 JCR *jcr = ua->jcr;
762 POOL_MEM tmp;
763
764 mr.Recycle=1; /* Look for volumes to prune and recycle */
765
766 if (!scan_storage_cmd(ua, ua->cmd, false, /* fromallpool*/
767 NULL /* drive */,
768 &mr, &pr,
769 NULL /* action */,
770 NULL /* storage */,
771 &nb, &results))
772 {
773 goto bail_out;
774 }
775 for (int i = 0; i < nb; i++) {
776 mr.clear();
777 mr.MediaId = results[i];
778 if (!db_get_media_record(jcr, jcr->db, &mr)) {
779 ua->error_msg(_("Unable to get Media record for MediaId %d.\n"), mr.MediaId);
780 continue;
781 }
782 if (mr.Enabled == 2 || strcmp(mr.VolStatus, "Archive") == 0) {
783 ua->error_msg(_("Cannot prune Volume \"%s\" because it is archived.\n"),
784 mr.VolumeName);
785 continue;
786 }
787 if (strcmp(mr.VolStatus, "Full") != 0 &&
788 strcmp(mr.VolStatus, "Used") != 0 )
789 {
790 ua->error_msg(_("Cannot prune Volume \"%s\" because the volume status is \"%s\" and should be Full or Used.\n"), mr.VolumeName, mr.VolStatus);
791 continue;
792 }
793
794 Mmsg(tmp, "Volume \"%s\"", mr.VolumeName);
795 if (!confirm_retention(ua, &mr.VolRetention, tmp.c_str())) {
796 goto bail_out;
797 }
798 prune_volume(ua, &mr);
799 }
800
801 bail_out:
802 if (results) {
803 free(results);
804 }
805 return true;
806 }
807
808 /*
809 * Prune a expired Volumes
810 */
prune_expired_volumes(UAContext * ua)811 static bool prune_expired_volumes(UAContext *ua)
812 {
813 bool ok=false;
814 POOL_MEM query(PM_MESSAGE);
815 POOL_MEM filter(PM_MESSAGE);
816 alist *lst=NULL;
817 int nb=0, i=0;
818 char *val;
819 MEDIA_DBR mr;
820
821 db_lock(ua->db);
822 /* We can restrict to a specific pool */
823 if ((i = find_arg_with_value(ua, "pool")) >= 0) {
824 POOL_DBR pdbr;
825 bmemset(&pdbr, 0, sizeof(pdbr));
826 bstrncpy(pdbr.Name, ua->argv[i], sizeof(pdbr.Name));
827 if (!db_get_pool_record(ua->jcr, ua->db, &pdbr)) {
828 ua->error_msg("%s", db_strerror(ua->db));
829 goto bail_out;
830 }
831 Mmsg(query, " AND PoolId = %lld ", (int64_t) pdbr.PoolId);
832 pm_strcat(filter, query.c_str());
833 }
834
835 /* We can restrict by MediaType */
836 if (((i = find_arg_with_value(ua, "mediatype")) >= 0) &&
837 (strlen(ua->argv[i]) <= MAX_NAME_LENGTH))
838 {
839 char ed1[MAX_ESCAPE_NAME_LENGTH];
840 db_escape_string(ua->jcr, ua->db, ed1,
841 ua->argv[i], strlen(ua->argv[i]));
842 Mmsg(query, " AND MediaType = '%s' ", ed1);
843 pm_strcat(filter, query.c_str());
844 }
845
846 /* Use a limit */
847 if ((i = find_arg_with_value(ua, "limit")) >= 0) {
848 if (is_an_integer(ua->argv[i])) {
849 Mmsg(query, " LIMIT %s ", ua->argv[i]);
850 pm_strcat(filter, query.c_str());
851 } else {
852 ua->error_msg(_("Expecting limit argument as integer\n"));
853 goto bail_out;
854 }
855 }
856
857 lst = New(alist(5, owned_by_alist));
858
859 Mmsg(query, expired_volumes[db_get_type_index(ua->db)], filter.c_str());
860 db_sql_query(ua->db, query.c_str(), db_string_list_handler, &lst);
861
862 foreach_alist(val, lst) {
863 nb++;
864 bmemset(&mr, 0, sizeof(mr));
865 bstrncpy(mr.VolumeName, val, sizeof(mr.VolumeName));
866 db_get_media_record(ua->jcr, ua->db, &mr);
867 Mmsg(query, _("Volume \"%s\""), val);
868 if (confirm_retention(ua, &mr.VolRetention, query.c_str())) {
869 prune_volume(ua, &mr);
870 }
871 }
872 ua->send_msg(_("%d expired volume%s found\n"),
873 nb, nb>1?"s":"");
874 ok = true;
875
876 bail_out:
877 db_unlock(ua->db);
878 if (lst) {
879 delete lst;
880 }
881 return ok;
882 }
883
884 /*
885 * Prune a given Volume
886 */
prune_volume(UAContext * ua,MEDIA_DBR * mr)887 bool prune_volume(UAContext *ua, MEDIA_DBR *mr)
888 {
889 POOL_MEM query(PM_MESSAGE);
890 struct del_ctx del;
891 bool ok = false;
892 int count;
893
894 if (mr->Enabled == 2) {
895 return false; /* Cannot prune archived volumes */
896 }
897
898 memset(&del, 0, sizeof(del));
899 del.max_ids = 10000;
900 del.JobId = (JobId_t *)malloc(sizeof(JobId_t) * del.max_ids);
901
902 db_lock(ua->db);
903
904 /* Prune only Volumes with status "Full", or "Used" */
905 if (strcmp(mr->VolStatus, "Full") == 0 ||
906 strcmp(mr->VolStatus, "Used") == 0) {
907 Dmsg2(100, "get prune list MediaId=%lu Volume %s\n", mr->MediaId, mr->VolumeName);
908 count = get_prune_list_for_volume(ua, mr, &del);
909 Dmsg1(100, "Num pruned = %d\n", count);
910 if (count != 0) {
911 ua->info_msg(_("Found %d Job(s) associated with the Volume \"%s\" that will be pruned\n"),
912 count, mr->VolumeName);
913 purge_job_list_from_catalog(ua, del);
914
915 } else {
916 ua->info_msg(_("Found no Job associated with the Volume \"%s\" to prune\n"),
917 mr->VolumeName);
918 }
919 ok = is_volume_purged(ua, mr);
920 if (ok) {
921 ua->send_events("DV0001", EVENTS_TYPE_VOLUME, "volume \"%s\" was purged", mr->VolumeName);
922 }
923 } else {
924 ua->info_msg(_("Volume \"%s\" has VolStatus \"%s\" and cannot be pruned.\n"), mr->VolumeName, mr->VolStatus);
925 }
926
927 db_unlock(ua->db);
928 if (del.JobId) {
929 free(del.JobId);
930 }
931 return ok;
932 }
933
934 /*
935 * Get prune list for a volume
936 */
get_prune_list_for_volume(UAContext * ua,MEDIA_DBR * mr,del_ctx * del)937 int get_prune_list_for_volume(UAContext *ua, MEDIA_DBR *mr, del_ctx *del)
938 {
939 POOL_MEM query(PM_MESSAGE);
940 int count = 0;
941 utime_t now, period;
942 char ed1[50], ed2[50];
943
944 if (mr->Enabled == 2) {
945 return 0; /* cannot prune Archived volumes */
946 }
947
948 /* Will not prune volume based on the VolRetention, it was
949 * disabled explicitely by the user
950 */
951 if (mr->VolRetention == 0) {
952 return 0;
953 }
954
955 /*
956 * Now add to the list of JobIds for Jobs written to this Volume
957 */
958 edit_int64(mr->MediaId, ed1);
959 period = mr->VolRetention;
960 now = (utime_t)time(NULL);
961 edit_int64(now-period, ed2);
962 Mmsg(query, sel_JobMedia, ed1, ed2);
963 Dmsg3(250, "Now=%d period=%d now-period=%s\n", (int)now, (int)period,
964 ed2);
965
966 Dmsg1(100, "Query=%s\n", query.c_str());
967 if (!db_sql_query(ua->db, query.c_str(), file_delete_handler, (void *)del)) {
968 if (ua->verbose) {
969 ua->error_msg("%s", db_strerror(ua->db));
970 }
971 Dmsg0(100, "Count failed\n");
972 goto bail_out;
973 }
974 count = exclude_running_jobs_from_list(del);
975
976 bail_out:
977 return count;
978 }
979
980 /*
981 * We have a list of jobs to prune or purge. If any of them is
982 * currently running, we set its JobId to zero which effectively
983 * excludes it.
984 *
985 * Returns the number of jobs that can be prunned or purged.
986 *
987 */
exclude_running_jobs_from_list(del_ctx * prune_list)988 int exclude_running_jobs_from_list(del_ctx *prune_list)
989 {
990 int count = 0;
991 JCR *jcr;
992 bool skip;
993 int i;
994
995 /* Do not prune any job currently running */
996 for (i=0; i < prune_list->num_ids; i++) {
997 skip = false;
998 foreach_jcr(jcr) {
999 if (jcr->JobId == prune_list->JobId[i]) {
1000 Dmsg2(100, "skip running job JobId[%d]=%d\n", i, (int)prune_list->JobId[i]);
1001 prune_list->JobId[i] = 0;
1002 skip = true;
1003 break;
1004 }
1005 }
1006 endeach_jcr(jcr);
1007 if (skip) {
1008 continue; /* don't increment count */
1009 }
1010 Dmsg2(100, "accept JobId[%d]=%d\n", i, (int)prune_list->JobId[i]);
1011 count++;
1012 }
1013 return count;
1014 }
1015