2 Bacula(R) - The Network Backup Solution
4 Copyright (C) 2000-2017 Kern Sibbald
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.
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.
14 This notice must be preserved when any source code is
15 conveyed and/or propagated.
17 Bacula(R) is a registered trademark of Kern Sibbald.
20 * Bacula Director -- User Agent Database prune Command
21 * Applies retention periods
23 * Kern Sibbald, February MMII
29 /* Imported functions */
31 /* Forward referenced functions */
32 static bool grow_del_list(struct del_ctx *del);
33 static bool prune_expired_volumes(UAContext*);
36 * Called here to count entries to be deleted
38 int del_count_handler(void *ctx, int num_fields, char **row)
40 struct s_count_ctx *cnt = (struct s_count_ctx *)ctx;
43 cnt->count = str_to_int64(row[0]);
52 * Called here to make in memory list of JobIds to be
53 * deleted and the associated PurgedFiles flag.
54 * The in memory list will then be transversed
55 * to issue the SQL DELETE commands. Note, the list
56 * is allowed to get to MAX_DEL_LIST_LEN to limit the
57 * maximum malloc'ed memory.
59 int job_delete_handler(void *ctx, int num_fields, char **row)
61 struct del_ctx *del = (struct del_ctx *)ctx;
63 if (!grow_del_list(del)) {
66 del->JobId[del->num_ids] = (JobId_t)str_to_int64(row[0]);
67 Dmsg2(60, "job_delete_handler row=%d val=%d\n", del->num_ids, del->JobId[del->num_ids]);
68 del->PurgedFiles[del->num_ids++] = (char)str_to_int64(row[1]);
72 int file_delete_handler(void *ctx, int num_fields, char **row)
74 struct del_ctx *del = (struct del_ctx *)ctx;
76 if (!grow_del_list(del)) {
79 del->JobId[del->num_ids++] = (JobId_t)str_to_int64(row[0]);
80 // Dmsg2(150, "row=%d val=%d\n", del->num_ids-1, del->JobId[del->num_ids-1]);
85 * Prune records from database
87 * prune files (from) client=xxx [pool=yyy]
88 * prune jobs (from) client=xxx [pool=yyy]
92 int prunecmd(UAContext *ua, const char *cmd)
102 static const char *keywords[] = {
110 if (!open_new_client_db(ua)) {
114 /* First search args */
115 kw = find_arg_keyword(ua, keywords);
116 if (kw < 0 || kw > 4) {
117 /* no args, so ask user */
118 kw = do_keyword_prompt(ua, _("Choose item to prune"), keywords);
122 case 0: /* prune files */
123 /* We restrict the client list to ClientAcl, maybe something to change later */
124 if (!(client = get_client_resource(ua, JT_SYSTEM))) {
127 if (find_arg_with_value(ua, "pool") >= 0) {
128 pool = get_pool_resource(ua);
132 /* Pool File Retention takes precedence over client File Retention */
133 if (pool && pool->FileRetention > 0) {
134 if (!confirm_retention(ua, &pool->FileRetention, "File")) {
137 } else if (!confirm_retention(ua, &client->FileRetention, "File")) {
140 prune_files(ua, client, pool);
143 case 1: /* prune jobs */
144 /* We restrict the client list to ClientAcl, maybe something to change later */
145 if (!(client = get_client_resource(ua, JT_SYSTEM))) {
148 if (find_arg_with_value(ua, "pool") >= 0) {
149 pool = get_pool_resource(ua);
153 /* Pool Job Retention takes precedence over client Job Retention */
154 if (pool && pool->JobRetention > 0) {
155 if (!confirm_retention(ua, &pool->JobRetention, "Job")) {
158 } else if (!confirm_retention(ua, &client->JobRetention, "Job")) {
161 /* ****FIXME**** allow user to select JobType */
162 prune_jobs(ua, client, pool, JT_BACKUP);
165 case 2: /* prune volume */
167 /* Look for All expired volumes, mostly designed for runscript */
168 if (find_arg(ua, "expired") >= 0) {
169 return prune_expired_volumes(ua);
172 if (!select_pool_and_media_dbr(ua, &pr, &mr)) {
175 if (mr.Enabled == 2) {
176 ua->error_msg(_("Cannot prune Volume \"%s\" because it is archived.\n"),
180 if (!confirm_retention(ua, &mr.VolRetention, "Volume")) {
183 prune_volume(ua, &mr);
185 case 3: /* prune stats */
186 dir = (DIRRES *)GetNextRes(R_DIRECTOR, NULL);
187 if (!dir->stats_retention) {
190 retention = dir->stats_retention;
191 if (!confirm_retention(ua, &retention, "Statistics")) {
194 prune_stats(ua, retention);
196 case 4: /* prune snapshots */
206 /* Prune Job stat records from the database.
209 int prune_stats(UAContext *ua, utime_t retention)
212 POOL_MEM query(PM_MESSAGE);
213 utime_t now = (utime_t)time(NULL);
216 Mmsg(query, "DELETE FROM JobHisto WHERE JobTDate < %s",
217 edit_int64(now - retention, ed1));
218 db_sql_query(ua->db, query.c_str(), NULL, NULL);
221 ua->info_msg(_("Pruned Jobs from JobHisto catalog.\n"));
227 * Use pool and client specified by user to select jobs to prune
228 * returns add_from string to add in FROM clause
229 * add_where string to add in WHERE clause
231 bool prune_set_filter(UAContext *ua, CLIENT *client, POOL *pool, utime_t period,
232 POOL_MEM *add_from, POOL_MEM *add_where)
235 char ed1[50], ed2[MAX_ESCAPE_NAME_LENGTH];
236 POOL_MEM tmp(PM_MESSAGE);
238 now = (utime_t)time(NULL);
239 edit_int64(now - period, ed1);
240 Dmsg3(150, "now=%lld period=%lld JobTDate=%s\n", now, period, ed1);
241 Mmsg(tmp, " AND JobTDate < %s ", ed1);
242 pm_strcat(*add_where, tmp.c_str());
246 db_escape_string(ua->jcr, ua->db, ed2,
247 client->name(), strlen(client->name()));
248 Mmsg(tmp, " AND Client.Name = '%s' ", ed2);
249 pm_strcat(*add_where, tmp.c_str());
250 pm_strcat(*add_from, " JOIN Client USING (ClientId) ");
254 db_escape_string(ua->jcr, ua->db, ed2,
255 pool->name(), strlen(pool->name()));
256 Mmsg(tmp, " AND Pool.Name = '%s' ", ed2);
257 pm_strcat(*add_where, tmp.c_str());
258 /* Use ON() instead of USING for some old SQLite */
259 pm_strcat(*add_from, " JOIN Pool ON (Job.PoolId = Pool.PoolId) ");
261 Dmsg2(150, "f=%s w=%s\n", add_from->c_str(), add_where->c_str());
267 * Prune File records from the database. For any Job which
268 * is older than the retention period, we unconditionally delete
269 * all File records for that Job. This is simple enough that no
270 * temporary tables are needed. We simply make an in memory list of
271 * the JobIds meeting the prune conditions, then delete all File records
272 * pointing to each of those JobIds.
274 * This routine assumes you want the pruning to be done. All checking
275 * must be done before calling this routine.
277 * Note: client or pool can possibly be NULL (not both).
279 int prune_files(UAContext *ua, CLIENT *client, POOL *pool)
282 struct s_count_ctx cnt;
283 POOL_MEM query(PM_MESSAGE);
284 POOL_MEM sql_where(PM_MESSAGE);
285 POOL_MEM sql_from(PM_MESSAGE);
289 memset(&del, 0, sizeof(del));
291 if (pool && pool->FileRetention > 0) {
292 period = pool->FileRetention;
295 period = client->FileRetention;
297 } else { /* should specify at least pool or client */
302 /* Specify JobTDate and Pool.Name= and/or Client.Name= in the query */
303 if (!prune_set_filter(ua, client, pool, period, &sql_from, &sql_where)) {
307 // edit_utime(now-period, ed1, sizeof(ed1));
308 // Jmsg(ua->jcr, M_INFO, 0, _("Begin pruning Jobs older than %s secs.\n"), ed1);
309 Jmsg(ua->jcr, M_INFO, 0, _("Begin pruning Files.\n"));
310 /* Select Jobs -- for counting */
312 "SELECT COUNT(1) FROM Job %s WHERE PurgedFiles=0 %s",
313 sql_from.c_str(), sql_where.c_str());
314 Dmsg1(100, "select sql=%s\n", query.c_str());
316 if (!db_sql_query(ua->db, query.c_str(), del_count_handler, (void *)&cnt)) {
317 ua->error_msg("%s", db_strerror(ua->db));
318 Dmsg0(100, "Count failed\n");
322 if (cnt.count == 0) {
324 ua->warning_msg(_("No Files found to prune.\n"));
329 if (cnt.count < MAX_DEL_LIST_LEN) {
330 del.max_ids = cnt.count + 1;
332 del.max_ids = MAX_DEL_LIST_LEN;
336 del.JobId = (JobId_t *)malloc(sizeof(JobId_t) * del.max_ids);
338 /* Now process same set but making a delete list */
339 Mmsg(query, "SELECT JobId FROM Job %s WHERE PurgedFiles=0 %s",
340 sql_from.c_str(), sql_where.c_str());
341 Dmsg1(100, "select sql=%s\n", query.c_str());
342 db_sql_query(ua->db, query.c_str(), file_delete_handler, (void *)&del);
344 purge_files_from_job_list(ua, del);
346 edit_uint64_with_commas(del.num_del, ed1);
347 ua->info_msg(_("Pruned Files from %s Jobs for client %s from catalog.\n"),
348 ed1, client->name());
359 static void drop_temp_tables(UAContext *ua)
362 for (i=0; drop_deltabs[i]; i++) {
363 db_sql_query(ua->db, drop_deltabs[i], NULL, (void *)NULL);
367 static bool create_temp_tables(UAContext *ua)
369 /* Create temp tables and indicies */
370 if (!db_sql_query(ua->db, create_deltabs[ua->db->bdb_get_type_index()], NULL, (void *)NULL)) {
371 ua->error_msg("%s", db_strerror(ua->db));
372 Dmsg0(100, "create DelTables table failed\n");
375 if (!db_sql_query(ua->db, create_delindex, NULL, (void *)NULL)) {
376 ua->error_msg("%s", db_strerror(ua->db));
377 Dmsg0(100, "create DelInx1 index failed\n");
383 static bool grow_del_list(struct del_ctx *del)
385 if (del->num_ids == MAX_DEL_LIST_LEN) {
389 if (del->num_ids == del->max_ids) {
390 del->max_ids = (del->max_ids * 3) / 2;
391 del->JobId = (JobId_t *)brealloc(del->JobId, sizeof(JobId_t) *
393 del->PurgedFiles = (char *)brealloc(del->PurgedFiles, del->max_ids);
398 struct accurate_check_ctx {
399 DBId_t ClientId; /* Id of client */
400 DBId_t FileSetId; /* Id of FileSet */
403 /* row: Job.Name, FileSet, Client.Name, FileSetId, ClientId, Type */
404 static int job_select_handler(void *ctx, int num_fields, char **row)
406 alist *lst = (alist *)ctx;
407 struct accurate_check_ctx *res;
408 ASSERT(num_fields == 6);
410 /* Quick fix for #5507, avoid locking res_head after db_lock() */
413 /* If this job doesn't exist anymore in the configuration, delete it */
414 if (GetResWithName(R_JOB, row[0]) == NULL) {
418 /* If this fileset doesn't exist anymore in the configuration, delete it */
419 if (GetResWithName(R_FILESET, row[1]) == NULL) {
423 /* If this client doesn't exist anymore in the configuration, delete it */
424 if (GetResWithName(R_CLIENT, row[2]) == NULL) {
429 /* Don't compute accurate things for Verify jobs */
430 if (*row[5] == 'V') {
434 res = (struct accurate_check_ctx*) malloc(sizeof(struct accurate_check_ctx));
435 res->FileSetId = str_to_int64(row[3]);
436 res->ClientId = str_to_int64(row[4]);
439 // Dmsg2(150, "row=%d val=%d\n", del->num_ids-1, del->JobId[del->num_ids-1]);
444 * Pruning Jobs is a bit more complicated than purging Files
445 * because we delete Job records only if there is a more current
446 * backup of the FileSet. Otherwise, we keep the Job record.
447 * In other words, we never delete the only Job record that
448 * contains a current backup of a FileSet. This prevents the
449 * Volume from being recycled and destroying a current backup.
451 * For Verify Jobs, we do not delete the last InitCatalog.
453 * For Restore Jobs there are no restrictions.
455 int prune_jobs(UAContext *ua, CLIENT *client, POOL *pool, int JobType)
457 POOL_MEM query(PM_MESSAGE);
458 POOL_MEM sql_where(PM_MESSAGE);
459 POOL_MEM sql_from(PM_MESSAGE);
462 alist *jobids_check=NULL;
463 struct accurate_check_ctx *elt;
464 db_list_ctx jobids, tempids;
467 memset(&del, 0, sizeof(del));
469 if (pool && pool->JobRetention > 0) {
470 period = pool->JobRetention;
473 period = client->JobRetention;
475 } else { /* should specify at least pool or client */
480 if (!prune_set_filter(ua, client, pool, period, &sql_from, &sql_where)) {
484 /* Drop any previous temporary tables still there */
485 drop_temp_tables(ua);
487 /* Create temp tables and indicies */
488 if (!create_temp_tables(ua)) {
492 edit_utime(period, ed1, sizeof(ed1));
493 Jmsg(ua->jcr, M_INFO, 0, _("Begin pruning Jobs older than %s.\n"), ed1);
496 del.JobId = (JobId_t *)malloc(sizeof(JobId_t) * del.max_ids);
497 del.PurgedFiles = (char *)malloc(del.max_ids);
500 * Select all files that are older than the JobRetention period
501 * and add them into the "DeletionCandidates" table.
504 "INSERT INTO DelCandidates "
505 "SELECT JobId,PurgedFiles,FileSetId,JobFiles,JobStatus "
506 "FROM Job %s " /* JOIN Pool/Client */
507 "WHERE Type IN ('B', 'C', 'M', 'V', 'D', 'R', 'c', 'm', 'g') "
508 " %s ", /* Pool/Client + JobTDate */
509 sql_from.c_str(), sql_where.c_str());
511 Dmsg1(100, "select sql=%s\n", query.c_str());
512 if (!db_sql_query(ua->db, query.c_str(), NULL, (void *)NULL)) {
514 ua->error_msg("%s", db_strerror(ua->db));
519 /* Now, for the selection, we discard some of them in order to be always
520 * able to restore files. (ie, last full, last diff, last incrs)
521 * Note: The DISTINCT could be more useful if we don't get FileSetId
523 jobids_check = New(alist(10, owned_by_alist));
525 "SELECT DISTINCT Job.Name, FileSet, Client.Name, Job.FileSetId, "
526 "Job.ClientId, Job.Type "
527 "FROM DelCandidates "
528 "JOIN Job USING (JobId) "
529 "JOIN Client USING (ClientId) "
530 "JOIN FileSet ON (Job.FileSetId = FileSet.FileSetId) "
531 "WHERE Job.Type IN ('B') " /* Look only Backup jobs */
532 "AND Job.JobStatus IN ('T', 'W') " /* Look only useful jobs */
535 /* The job_select_handler will skip jobs or filesets that are no longer
536 * in the configuration file. Interesting ClientId/FileSetId will be
537 * added to jobids_check (currently disabled in 6.0.7b)
539 if (!db_sql_query(ua->db, query.c_str(), job_select_handler, jobids_check)) {
540 ua->error_msg("%s", db_strerror(ua->db));
543 /* For this selection, we exclude current jobs used for restore or
544 * accurate. This will prevent to prune the last full backup used for
545 * current backup & restore
547 memset(&jr, 0, sizeof(jr));
548 /* To find useful jobs, we do like an incremental */
549 jr.JobLevel = L_INCREMENTAL;
550 foreach_alist(elt, jobids_check) {
551 jr.ClientId = elt->ClientId; /* should be always the same */
552 jr.FileSetId = elt->FileSetId;
553 db_get_accurate_jobids(ua->jcr, ua->db, &jr, &tempids);
557 /* Discard latest Verify level=InitCatalog job
558 * TODO: can have multiple fileset
561 "SELECT JobId, JobTDate "
562 "FROM Job %s " /* JOIN Client/Pool */
563 "WHERE Type='V' AND Level='V' "
564 " %s " /* Pool, JobTDate, Client */
565 "ORDER BY JobTDate DESC LIMIT 1",
566 sql_from.c_str(), sql_where.c_str());
568 if (!db_sql_query(ua->db, query.c_str(), db_list_handler, &jobids)) {
569 ua->error_msg("%s", db_strerror(ua->db));
572 /* If we found jobs to exclude from the DelCandidates list, we should
573 * also remove BaseJobs that can be linked with them
575 if (jobids.count > 0) {
576 Dmsg1(60, "jobids to exclude before basejobs = %s\n", jobids.list);
577 /* We also need to exclude all basejobs used */
578 db_get_used_base_jobids(ua->jcr, ua->db, jobids.list, &jobids);
580 /* Removing useful jobs from the DelCandidates list */
581 Mmsg(query, "DELETE FROM DelCandidates "
582 "WHERE JobId IN (%s) " /* JobId used in accurate */
583 "AND JobFiles!=0", /* Discard when JobFiles=0 */
586 if (!db_sql_query(ua->db, query.c_str(), NULL, NULL)) {
587 ua->error_msg("%s", db_strerror(ua->db));
588 goto bail_out; /* Don't continue if the list isn't clean */
590 Dmsg1(60, "jobids to exclude = %s\n", jobids.list);
593 /* We use DISTINCT because we can have two times the same job */
595 "SELECT DISTINCT DelCandidates.JobId,DelCandidates.PurgedFiles "
596 "FROM DelCandidates");
597 if (!db_sql_query(ua->db, query.c_str(), job_delete_handler, (void *)&del)) {
598 ua->error_msg("%s", db_strerror(ua->db));
601 purge_job_list_from_catalog(ua, del);
603 if (del.num_del > 0) {
604 ua->info_msg(_("Pruned %d %s for client %s from catalog.\n"), del.num_del,
605 del.num_del==1?_("Job"):_("Jobs"), client->name());
606 } else if (ua->verbose) {
607 ua->info_msg(_("No Jobs found to prune.\n"));
611 drop_temp_tables(ua);
616 if (del.PurgedFiles) {
617 free(del.PurgedFiles);
627 * Prune a expired Volumes
629 static bool prune_expired_volumes(UAContext *ua)
632 POOL_MEM query(PM_MESSAGE);
633 POOL_MEM filter(PM_MESSAGE);
640 /* We can restrict to a specific pool */
641 if ((i = find_arg_with_value(ua, "pool")) >= 0) {
643 memset(&pdbr, 0, sizeof(pdbr));
644 bstrncpy(pdbr.Name, ua->argv[i], sizeof(pdbr.Name));
645 if (!db_get_pool_record(ua->jcr, ua->db, &pdbr)) {
646 ua->error_msg("%s", db_strerror(ua->db));
649 Mmsg(query, " AND PoolId = %lld ", (int64_t) pdbr.PoolId);
650 pm_strcat(filter, query.c_str());
653 /* We can restrict by MediaType */
654 if (((i = find_arg_with_value(ua, "mediatype")) >= 0) &&
655 (strlen(ua->argv[i]) <= MAX_NAME_LENGTH))
657 char ed1[MAX_ESCAPE_NAME_LENGTH];
658 db_escape_string(ua->jcr, ua->db, ed1,
659 ua->argv[i], strlen(ua->argv[i]));
660 Mmsg(query, " AND MediaType = '%s' ", ed1);
661 pm_strcat(filter, query.c_str());
665 if ((i = find_arg_with_value(ua, "limit")) >= 0) {
666 if (is_an_integer(ua->argv[i])) {
667 Mmsg(query, " LIMIT %s ", ua->argv[i]);
668 pm_strcat(filter, query.c_str());
670 ua->error_msg(_("Expecting limit argument as integer\n"));
675 lst = New(alist(5, owned_by_alist));
677 Mmsg(query, expired_volumes[db_get_type_index(ua->db)], filter.c_str());
678 db_sql_query(ua->db, query.c_str(), db_string_list_handler, &lst);
680 foreach_alist(val, lst) {
682 memset(&mr, 0, sizeof(mr));
683 bstrncpy(mr.VolumeName, val, sizeof(mr.VolumeName));
684 db_get_media_record(ua->jcr, ua->db, &mr);
685 Mmsg(query, _("Volume \"%s\""), val);
686 if (confirm_retention(ua, &mr.VolRetention, query.c_str())) {
687 prune_volume(ua, &mr);
690 ua->send_msg(_("%d expired volume%s found\n"),
703 * Prune a given Volume
705 bool prune_volume(UAContext *ua, MEDIA_DBR *mr)
707 POOL_MEM query(PM_MESSAGE);
712 if (mr->Enabled == 2) {
713 return false; /* Cannot prune archived volumes */
716 memset(&del, 0, sizeof(del));
718 del.JobId = (JobId_t *)malloc(sizeof(JobId_t) * del.max_ids);
722 /* Prune only Volumes with status "Full", or "Used" */
723 if (strcmp(mr->VolStatus, "Full") == 0 ||
724 strcmp(mr->VolStatus, "Used") == 0) {
725 Dmsg2(100, "get prune list MediaId=%lu Volume %s\n", mr->MediaId, mr->VolumeName);
726 count = get_prune_list_for_volume(ua, mr, &del);
727 Dmsg1(100, "Num pruned = %d\n", count);
729 ua->info_msg(_("Found %d Job(s) associated with the Volume \"%s\" that will be pruned\n"),
730 count, mr->VolumeName);
731 purge_job_list_from_catalog(ua, del);
734 ua->info_msg(_("Found no Job associated with the Volume \"%s\" to prune\n"),
737 ok = is_volume_purged(ua, mr);
748 * Get prune list for a volume
750 int get_prune_list_for_volume(UAContext *ua, MEDIA_DBR *mr, del_ctx *del)
752 POOL_MEM query(PM_MESSAGE);
755 char ed1[50], ed2[50];
757 if (mr->Enabled == 2) {
758 return 0; /* cannot prune Archived volumes */
762 * Now add to the list of JobIds for Jobs written to this Volume
764 edit_int64(mr->MediaId, ed1);
765 period = mr->VolRetention;
766 now = (utime_t)time(NULL);
767 edit_int64(now-period, ed2);
768 Mmsg(query, sel_JobMedia, ed1, ed2);
769 Dmsg3(250, "Now=%d period=%d now-period=%s\n", (int)now, (int)period,
772 Dmsg1(100, "Query=%s\n", query.c_str());
773 if (!db_sql_query(ua->db, query.c_str(), file_delete_handler, (void *)del)) {
775 ua->error_msg("%s", db_strerror(ua->db));
777 Dmsg0(100, "Count failed\n");
780 count = exclude_running_jobs_from_list(del);
787 * We have a list of jobs to prune or purge. If any of them is
788 * currently running, we set its JobId to zero which effectively
791 * Returns the number of jobs that can be prunned or purged.
794 int exclude_running_jobs_from_list(del_ctx *prune_list)
801 /* Do not prune any job currently running */
802 for (i=0; i < prune_list->num_ids; i++) {
805 if (jcr->JobId == prune_list->JobId[i]) {
806 Dmsg2(100, "skip running job JobId[%d]=%d\n", i, (int)prune_list->JobId[i]);
807 prune_list->JobId[i] = 0;
814 continue; /* don't increment count */
816 Dmsg2(100, "accept JobId[%d]=%d\n", i, (int)prune_list->JobId[i]);