2 Bacula® - The Network Backup Solution
4 Copyright (C) 2002-2009 Free Software Foundation Europe e.V.
6 The main author of Bacula is Kern Sibbald, with contributions from
7 many others, a complete list can be found in the file AUTHORS.
8 This program is Free Software; you can redistribute it and/or
9 modify it under the terms of version two of the GNU General Public
10 License as published by the Free Software Foundation and included
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 General Public License for more details.
18 You should have received a copy of the GNU 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
23 Bacula® is a registered trademark of Kern Sibbald.
24 The licensor of Bacula is the Free Software Foundation Europe
25 (FSFE), Fiduciary Program, Sumatrastrasse 25, 8006 Zürich,
26 Switzerland, email:ftf@fsfeurope.org.
30 * Bacula Director -- User Agent Database prune Command
31 * Applies retention periods
33 * Kern Sibbald, February MMII
40 /* Imported functions */
42 /* Forward referenced functions */
43 static bool grow_del_list(struct del_ctx *del);
46 * Called here to count entries to be deleted
48 int del_count_handler(void *ctx, int num_fields, char **row)
50 struct s_count_ctx *cnt = (struct s_count_ctx *)ctx;
53 cnt->count = str_to_int64(row[0]);
62 * Called here to make in memory list of JobIds to be
63 * deleted and the associated PurgedFiles flag.
64 * The in memory list will then be transversed
65 * to issue the SQL DELETE commands. Note, the list
66 * is allowed to get to MAX_DEL_LIST_LEN to limit the
67 * maximum malloc'ed memory.
69 int job_delete_handler(void *ctx, int num_fields, char **row)
71 struct del_ctx *del = (struct del_ctx *)ctx;
73 if (!grow_del_list(del)) {
76 del->JobId[del->num_ids] = (JobId_t)str_to_int64(row[0]);
77 Dmsg2(60, "job_delete_handler row=%d val=%d\n", del->num_ids, del->JobId[del->num_ids]);
78 del->PurgedFiles[del->num_ids++] = (char)str_to_int64(row[1]);
82 int file_delete_handler(void *ctx, int num_fields, char **row)
84 struct del_ctx *del = (struct del_ctx *)ctx;
86 if (!grow_del_list(del)) {
89 del->JobId[del->num_ids++] = (JobId_t)str_to_int64(row[0]);
90 // Dmsg2(150, "row=%d val=%d\n", del->num_ids-1, del->JobId[del->num_ids-1]);
95 * Prune records from database
97 * prune files (from) client=xxx [pool=yyy]
98 * prune jobs (from) client=xxx [pool=yyy]
102 int prunecmd(UAContext *ua, const char *cmd)
112 static const char *keywords[] = {
119 if (!open_client_db(ua)) {
123 /* First search args */
124 kw = find_arg_keyword(ua, keywords);
125 if (kw < 0 || kw > 3) {
126 /* no args, so ask user */
127 kw = do_keyword_prompt(ua, _("Choose item to prune"), keywords);
131 case 0: /* prune files */
132 client = get_client_resource(ua);
133 if (find_arg_with_value(ua, "pool") >= 0) {
134 pool = get_pool_resource(ua);
138 /* Pool File Retention takes precedence over client File Retention */
139 if (pool && pool->FileRetention > 0) {
140 if (!confirm_retention(ua, &pool->FileRetention, "File")) {
143 } else if (!client || !confirm_retention(ua, &client->FileRetention, "File")) {
146 prune_files(ua, client, pool);
148 case 1: /* prune jobs */
149 client = get_client_resource(ua);
150 if (find_arg_with_value(ua, "pool") >= 0) {
151 pool = get_pool_resource(ua);
155 /* Pool Job Retention takes precedence over client Job Retention */
156 if (pool && pool->JobRetention > 0) {
157 if (!confirm_retention(ua, &pool->JobRetention, "Job")) {
160 } else if (!client || !confirm_retention(ua, &client->JobRetention, "Job")) {
163 /* ****FIXME**** allow user to select JobType */
164 prune_jobs(ua, client, pool, JT_BACKUP);
166 case 2: /* prune volume */
167 if (!select_pool_and_media_dbr(ua, &pr, &mr)) {
170 if (mr.Enabled == 2) {
171 ua->error_msg(_("Cannot prune Volume \"%s\" because it is archived.\n"),
175 if (!confirm_retention(ua, &mr.VolRetention, "Volume")) {
178 prune_volume(ua, &mr);
180 case 3: /* prune stats */
181 dir = (DIRRES *)GetNextRes(R_DIRECTOR, NULL);
182 if (!dir->stats_retention) {
185 retention = dir->stats_retention;
186 if (!confirm_retention(ua, &retention, "Statistics")) {
189 prune_stats(ua, retention);
198 /* Prune Job stat records from the database.
201 int prune_stats(UAContext *ua, utime_t retention)
204 POOL_MEM query(PM_MESSAGE);
205 utime_t now = (utime_t)time(NULL);
208 Mmsg(query, "DELETE FROM JobHisto WHERE JobTDate < %s",
209 edit_int64(now - retention, ed1));
210 db_sql_query(ua->db, query.c_str(), NULL, NULL);
213 ua->info_msg(_("Pruned Jobs from JobHisto catalog.\n"));
219 * Prune File records from the database. For any Job which
220 * is older than the retention period, we unconditionally delete
221 * all File records for that Job. This is simple enough that no
222 * temporary tables are needed. We simply make an in memory list of
223 * the JobIds meeting the prune conditions, then delete all File records
224 * pointing to each of those JobIds.
226 * This routine assumes you want the pruning to be done. All checking
227 * must be done before calling this routine.
229 * Note: pool can possibly be NULL.
231 int prune_files(UAContext *ua, CLIENT *client, POOL *pool)
234 struct s_count_ctx cnt;
235 POOL_MEM query(PM_MESSAGE);
238 char ed1[50], ed2[50];
241 memset(&cr, 0, sizeof(cr));
242 memset(&del, 0, sizeof(del));
243 bstrncpy(cr.Name, client->name(), sizeof(cr.Name));
244 if (!db_create_client_record(ua->jcr, ua->db, &cr)) {
249 if (pool && pool->FileRetention > 0) {
250 period = pool->FileRetention;
252 period = client->FileRetention;
254 now = (utime_t)time(NULL);
256 // edit_utime(now-period, ed1, sizeof(ed1));
257 // Jmsg(ua->jcr, M_INFO, 0, _("Begin pruning Jobs older than %s secs.\n"), ed1);
258 Jmsg(ua->jcr, M_INFO, 0, _("Begin pruning Jobs.\n"));
259 /* Select Jobs -- for counting */
260 edit_int64(now - period, ed1);
261 Mmsg(query, count_select_job, ed1, edit_int64(cr.ClientId, ed2));
262 Dmsg3(050, "select now=%u period=%u sql=%s\n", (uint32_t)now,
263 (uint32_t)period, query.c_str());
265 if (!db_sql_query(ua->db, query.c_str(), del_count_handler, (void *)&cnt)) {
266 ua->error_msg("%s", db_strerror(ua->db));
267 Dmsg0(050, "Count failed\n");
271 if (cnt.count == 0) {
273 ua->warning_msg(_("No Files found to prune.\n"));
278 if (cnt.count < MAX_DEL_LIST_LEN) {
279 del.max_ids = cnt.count + 1;
281 del.max_ids = MAX_DEL_LIST_LEN;
285 del.JobId = (JobId_t *)malloc(sizeof(JobId_t) * del.max_ids);
287 /* Now process same set but making a delete list */
288 Mmsg(query, select_job, edit_int64(now - period, ed1),
289 edit_int64(cr.ClientId, ed2));
290 db_sql_query(ua->db, query.c_str(), file_delete_handler, (void *)&del);
292 purge_files_from_job_list(ua, del);
294 edit_uint64_with_commas(del.num_del, ed1);
295 ua->info_msg(_("Pruned Files from %s Jobs for client %s from catalog.\n"),
296 ed1, client->name());
307 static void drop_temp_tables(UAContext *ua)
310 for (i=0; drop_deltabs[i]; i++) {
311 db_sql_query(ua->db, drop_deltabs[i], NULL, (void *)NULL);
315 static bool create_temp_tables(UAContext *ua)
317 /* Create temp tables and indicies */
318 if (!db_sql_query(ua->db, create_deltabs[db_type], NULL, (void *)NULL)) {
319 ua->error_msg("%s", db_strerror(ua->db));
320 Dmsg0(050, "create DelTables table failed\n");
323 if (!db_sql_query(ua->db, create_delindex, NULL, (void *)NULL)) {
324 ua->error_msg("%s", db_strerror(ua->db));
325 Dmsg0(050, "create DelInx1 index failed\n");
331 static bool grow_del_list(struct del_ctx *del)
333 if (del->num_ids == MAX_DEL_LIST_LEN) {
337 if (del->num_ids == del->max_ids) {
338 del->max_ids = (del->max_ids * 3) / 2;
339 del->JobId = (JobId_t *)brealloc(del->JobId, sizeof(JobId_t) *
341 del->PurgedFiles = (char *)brealloc(del->PurgedFiles, del->max_ids);
346 struct accurate_check_ctx {
347 DBId_t ClientId; /* Id of client */
348 DBId_t FileSetId; /* Id of FileSet */
351 /* row: Job.Name, FileSet, Client.Name, FileSetId, ClientId */
352 static int job_select_handler(void *ctx, int num_fields, char **row)
354 alist *lst = (alist *)ctx;
355 struct accurate_check_ctx *res;
357 if (num_fields != 6) {
361 /* If this job doesn't exist anymore in the configuration, delete it */
362 if (GetResWithName(R_JOB, row[0]) == NULL) {
366 /* If this fileset doesn't exist anymore in the configuration, delete it */
367 if (GetResWithName(R_FILESET, row[1]) == NULL) {
371 /* If this client doesn't exist anymore in the configuration, delete it */
372 if (GetResWithName(R_CLIENT, row[2]) == NULL) {
376 /* Don't compute accurate things for Verify jobs */
377 if (*row[5] == 'V') {
381 res = (struct accurate_check_ctx*) malloc(sizeof(struct accurate_check_ctx));
382 res->FileSetId = str_to_int64(row[3]);
383 res->ClientId = str_to_int64(row[4]);
386 // Dmsg2(150, "row=%d val=%d\n", del->num_ids-1, del->JobId[del->num_ids-1]);
391 * Pruning Jobs is a bit more complicated than purging Files
392 * because we delete Job records only if there is a more current
393 * backup of the FileSet. Otherwise, we keep the Job record.
394 * In other words, we never delete the only Job record that
395 * contains a current backup of a FileSet. This prevents the
396 * Volume from being recycled and destroying a current backup.
398 * For Verify Jobs, we do not delete the last InitCatalog.
400 * For Restore Jobs there are no restrictions.
402 int prune_jobs(UAContext *ua, CLIENT *client, POOL *pool, int JobType)
405 POOL_MEM query(PM_MESSAGE);
408 char ed1[50], ed2[50];
409 alist *jobids_check=NULL;
410 struct accurate_check_ctx *elt;
415 memset(&cr, 0, sizeof(cr));
417 bstrncpy(cr.Name, client->name(), sizeof(cr.Name));
418 if (!db_create_client_record(ua->jcr, ua->db, &cr)) {
423 if (pool && pool->JobRetention > 0) {
424 period = pool->JobRetention;
426 period = client->JobRetention;
428 now = (utime_t)time(NULL);
430 /* Drop any previous temporary tables still there */
431 drop_temp_tables(ua);
433 /* Create temp tables and indicies */
434 if (!create_temp_tables(ua)) {
438 edit_utime(period, ed1, sizeof(ed1));
439 Jmsg(ua->jcr, M_INFO, 0, _("Begin pruning Jobs older than %s.\n"), ed1);
441 edit_int64(now - period, ed1); /* Jobs older than ed1 are good candidates */
442 edit_int64(cr.ClientId, ed2);
444 memset(&del, 0, sizeof(del));
446 del.JobId = (JobId_t *)malloc(sizeof(JobId_t) * del.max_ids);
447 del.PurgedFiles = (char *)malloc(del.max_ids);
450 * Select all files that are older than the JobRetention period
451 * and stuff them into the "DeletionCandidates" table.
454 "INSERT INTO DelCandidates "
455 "SELECT JobId,PurgedFiles,FileSetId,JobFiles,JobStatus "
457 "WHERE Type IN ('B', 'C', 'M', 'V', 'D', 'R', 'c', 'm') "
458 "AND JobTDate<%s AND ClientId=%s",
461 if (!db_sql_query(ua->db, query.c_str(), NULL, (void *)NULL)) {
463 ua->error_msg("%s", db_strerror(ua->db));
468 /* Now, for the selection, we discard some of them in order to be always
469 * able to restore files. (ie, last full, last diff, last incrs)
470 * Note: The DISTINCT could be more useful if we don't get FileSetId
472 jobids_check = New(alist(10, owned_by_alist));
474 "SELECT DISTINCT Job.Name, FileSet, Client.Name, Job.FileSetId, "
475 "Job.ClientId, Job.Type "
476 "FROM DelCandidates "
477 "JOIN Job USING (JobId) "
478 "JOIN Client USING (ClientId) "
479 "JOIN FileSet ON (Job.FileSetId = FileSet.FileSetId) "
480 "WHERE Type NOT IN ('D', 'R', 'c', 'm') " /* Discard Admin, Restore, Copy, Migration */
483 /* The job_select_handler will skip jobs or fileset that are no longer
484 * in the configuration file
486 if (!db_sql_query(ua->db, query.c_str(), job_select_handler, jobids_check)) {
487 ua->error_msg("%s", db_strerror(ua->db));
490 /* For all jobs of this client, we exclude current jobs used for restore or
491 * accurate. This will prevent to prune the last full backup used for
492 * current backup & restore
494 memset(&jr, 0, sizeof(jr));
495 /* To find useful jobs, we do like an incremental */
496 jr.JobLevel = L_INCREMENTAL;
497 foreach_alist(elt, jobids_check) {
498 jr.ClientId = elt->ClientId;
499 jr.FileSetId = elt->FileSetId;
500 db_accurate_get_jobids(ua->jcr, ua->db, &jr, &jobids);
503 /* Discard latest Verify level=InitCatalog job */
505 "SELECT JobId, JobTDate "
507 "WHERE JobTDate<%s AND ClientId=%s "
508 "AND Type='V' AND Level='V' "
509 "ORDER BY JobTDate DESC LIMIT 1",
512 if (!db_sql_query(ua->db, query.c_str(), db_list_handler, &jobids)) {
513 ua->error_msg("%s", db_strerror(ua->db));
516 /* If we found jobs to exclude from the DelCandidates list, we should
517 * also remove BaseJobs that can be linked with them
519 if (jobids.count > 0) {
520 Dmsg1(60, "jobids to exclude before basejobs = %s\n", jobids.list);
521 /* We also need to exclude all basejobs used */
522 db_get_used_base_jobids(ua->jcr, ua->db, jobids.list, &jobids);
524 /* Removing exceptions from the DelCandidates list */
525 Mmsg(query, "DELETE FROM DelCandidates WHERE JobId IN (%s)", jobids.list);
526 if (!db_sql_query(ua->db, query.c_str(), NULL, NULL)) {
527 ua->error_msg("%s", db_strerror(ua->db));
528 goto bail_out; /* Don't continue if the list isn't clean */
530 Dmsg1(60, "jobids to exclude = %s\n", jobids.list);
533 /* Prune garbage jobs (JobStatus not successful) */
535 "INSERT INTO DelCandidates "
536 "SELECT JobId,PurgedFiles,FileSetId,JobFiles,JobStatus "
538 "WHERE ( JobFiles=0 "
539 "OR JobStatus NOT IN ('T', 'W') "
542 "AND ClientId = %s ",
545 Dmsg1(150, "Query=%s\n", query.c_str());
546 if (!db_sql_query(ua->db, query.c_str(), NULL, NULL)) {
547 ua->error_msg("%s", db_strerror(ua->db));
550 /* We use DISTINCT because we can have two times the same job */
552 "SELECT DISTINCT DelCandidates.JobId,DelCandidates.PurgedFiles "
553 "FROM DelCandidates");
554 if (!db_sql_query(ua->db, query.c_str(), job_delete_handler, (void *)&del)) {
555 ua->error_msg("%s", db_strerror(ua->db));
558 purge_job_list_from_catalog(ua, del);
560 if (del.num_del > 0) {
561 ua->info_msg(_("Pruned %d %s for client %s from catalog.\n"), del.num_del,
562 del.num_del==1?_("Job"):_("Jobs"), client->name());
563 } else if (ua->verbose) {
564 ua->info_msg(_("No Jobs found to prune.\n"));
568 drop_temp_tables(ua);
573 if (del.PurgedFiles) {
574 free(del.PurgedFiles);
583 * Prune a given Volume
585 bool prune_volume(UAContext *ua, MEDIA_DBR *mr)
587 POOL_MEM query(PM_MESSAGE);
592 if (mr->Enabled == 2) {
593 return false; /* Cannot prune archived volumes */
596 memset(&del, 0, sizeof(del));
598 del.JobId = (JobId_t *)malloc(sizeof(JobId_t) * del.max_ids);
602 /* Prune only Volumes with status "Full", or "Used" */
603 if (strcmp(mr->VolStatus, "Full") == 0 ||
604 strcmp(mr->VolStatus, "Used") == 0) {
605 Dmsg2(050, "get prune list MediaId=%d Volume %s\n", (int)mr->MediaId, mr->VolumeName);
606 count = get_prune_list_for_volume(ua, mr, &del);
607 Dmsg1(050, "Num pruned = %d\n", count);
609 purge_job_list_from_catalog(ua, del);
611 ok = is_volume_purged(ua, mr);
622 * Get prune list for a volume
624 int get_prune_list_for_volume(UAContext *ua, MEDIA_DBR *mr, del_ctx *del)
626 POOL_MEM query(PM_MESSAGE);
629 char ed1[50], ed2[50];
631 if (mr->Enabled == 2) {
632 return 0; /* cannot prune Archived volumes */
636 * Now add to the list of JobIds for Jobs written to this Volume
638 edit_int64(mr->MediaId, ed1);
639 period = mr->VolRetention;
640 now = (utime_t)time(NULL);
641 edit_int64(now-period, ed2);
642 Mmsg(query, sel_JobMedia, ed1, ed2);
643 Dmsg3(250, "Now=%d period=%d now-period=%s\n", (int)now, (int)period,
646 Dmsg1(050, "Query=%s\n", query.c_str());
647 if (!db_sql_query(ua->db, query.c_str(), file_delete_handler, (void *)del)) {
649 ua->error_msg("%s", db_strerror(ua->db));
651 Dmsg0(050, "Count failed\n");
654 count = exclude_running_jobs_from_list(del);
661 * We have a list of jobs to prune or purge. If any of them is
662 * currently running, we set its JobId to zero which effectively
665 * Returns the number of jobs that can be prunned or purged.
668 int exclude_running_jobs_from_list(del_ctx *prune_list)
675 /* Do not prune any job currently running */
676 for (i=0; i < prune_list->num_ids; i++) {
679 if (jcr->JobId == prune_list->JobId[i]) {
680 Dmsg2(050, "skip running job JobId[%d]=%d\n", i, (int)prune_list->JobId[i]);
681 prune_list->JobId[i] = 0;
688 continue; /* don't increment count */
690 Dmsg2(050, "accept JobId[%d]=%d\n", i, (int)prune_list->JobId[i]);