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