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);
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 verify_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 verify_ctx*) malloc(sizeof(struct verify_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 verify_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 incr)
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') " /* Don't look Admin, Restore, Copy, Migration */
483 if (!db_sql_query(ua->db, query.c_str(), job_select_handler, jobids_check)) {
484 ua->error_msg("%s", db_strerror(ua->db));
487 /* For all jobs of this client, we exclude current jobs used for restore or
488 * accurate. This will prevent to prune the last full backup used for
489 * current backup & restore
491 memset(&jr, 0, sizeof(jr));
492 jr.JobLevel = L_INCREMENTAL; /* To find useful jobs, we do like an incremental */
493 foreach_alist(elt, jobids_check) {
494 jr.ClientId = elt->ClientId;
495 jr.FileSetId = elt->FileSetId;
496 db_accurate_get_jobids(ua->jcr, ua->db, &jr, &jobids);
499 /* Discard latest Verify level=InitCatalog job */
501 "SELECT JobId, JobTDate "
503 "WHERE JobTDate<%s AND ClientId=%s "
504 "AND Type='V' AND Level='V' "
505 "ORDER BY JobTDate DESC LIMIT 1",
508 if (!db_sql_query(ua->db, query.c_str(), db_list_handler, &jobids)) {
509 ua->error_msg("%s", db_strerror(ua->db));
512 /* If we found job to exclude from the DelCandidates list, we should
513 * also remove BaseJobs that can be linked
515 if (jobids.count > 0) {
516 Dmsg1(60, "jobids to exclude before basejobs = %s\n", jobids.list);
517 /* We also need to exclude all basejobs used */
518 db_get_used_base_jobids(ua->jcr, ua->db, jobids.list, &jobids);
520 /* Removing exceptions from the DelCandidates list */
521 Mmsg(query, "DELETE FROM DelCandidates WHERE JobId IN (%s)", jobids.list);
522 if (!db_sql_query(ua->db, query.c_str(), NULL, NULL)) {
523 ua->error_msg("%s", db_strerror(ua->db));
524 goto bail_out; /* Don't continue if the list isn't clean */
526 Dmsg1(60, "jobids to exclude = %s\n", jobids.list);
529 /* Prune garbage jobs (JobStatus not successful) */
531 "INSERT INTO DelCandidates "
532 "SELECT JobId,PurgedFiles,FileSetId,JobFiles,JobStatus "
534 "WHERE ( JobFiles=0 "
535 "OR JobStatus NOT IN ('T', 'W') "
538 "AND ClientId = %s ",
541 Dmsg1(150, "Query=%s\n", query.c_str());
542 if (!db_sql_query(ua->db, query.c_str(), NULL, NULL)) {
543 ua->error_msg("%s", db_strerror(ua->db));
546 /* We use DISTINCT because we can have two times the same job */
547 Mmsg(query, "SELECT DISTINCT DelCandidates.JobId,DelCandidates.PurgedFiles FROM DelCandidates");
548 Dmsg1(150, "Query=%s\n", query.c_str());
549 if (!db_sql_query(ua->db, query.c_str(), job_delete_handler, (void *)&del)) {
550 ua->error_msg("%s", db_strerror(ua->db));
553 purge_job_list_from_catalog(ua, del);
555 if (del.num_del > 0) {
556 ua->info_msg(_("Pruned %d %s for client %s from catalog.\n"), del.num_del,
557 del.num_del==1?_("Job"):_("Jobs"), client->name());
558 } else if (ua->verbose) {
559 ua->info_msg(_("No Jobs found to prune.\n"));
563 drop_temp_tables(ua);
568 if (del.PurgedFiles) {
569 free(del.PurgedFiles);
578 * Prune a given Volume
580 bool prune_volume(UAContext *ua, MEDIA_DBR *mr)
582 POOL_MEM query(PM_MESSAGE);
587 if (mr->Enabled == 2) {
588 return false; /* Cannot prune archived volumes */
591 memset(&del, 0, sizeof(del));
593 del.JobId = (JobId_t *)malloc(sizeof(JobId_t) * del.max_ids);
597 /* Prune only Volumes with status "Full", or "Used" */
598 if (strcmp(mr->VolStatus, "Full") == 0 ||
599 strcmp(mr->VolStatus, "Used") == 0) {
600 Dmsg2(050, "get prune list MediaId=%d Volume %s\n", (int)mr->MediaId, mr->VolumeName);
601 count = get_prune_list_for_volume(ua, mr, &del);
602 Dmsg1(050, "Num pruned = %d\n", count);
604 purge_job_list_from_catalog(ua, del);
606 ok = is_volume_purged(ua, mr);
617 * Get prune list for a volume
619 int get_prune_list_for_volume(UAContext *ua, MEDIA_DBR *mr, del_ctx *del)
621 POOL_MEM query(PM_MESSAGE);
624 char ed1[50], ed2[50];
626 if (mr->Enabled == 2) {
627 return 0; /* cannot prune Archived volumes */
631 * Now add to the list of JobIds for Jobs written to this Volume
633 edit_int64(mr->MediaId, ed1);
634 period = mr->VolRetention;
635 now = (utime_t)time(NULL);
636 edit_int64(now-period, ed2);
637 Mmsg(query, sel_JobMedia, ed1, ed2);
638 Dmsg3(250, "Now=%d period=%d now-period=%s\n", (int)now, (int)period,
641 Dmsg1(050, "Query=%s\n", query.c_str());
642 if (!db_sql_query(ua->db, query.c_str(), file_delete_handler, (void *)del)) {
644 ua->error_msg("%s", db_strerror(ua->db));
646 Dmsg0(050, "Count failed\n");
649 count = exclude_running_jobs_from_list(del);
656 * We have a list of jobs to prune or purge. If any of them is
657 * currently running, we set its JobId to zero which effectively
660 * Returns the number of jobs that can be prunned or purged.
663 int exclude_running_jobs_from_list(del_ctx *prune_list)
670 /* Do not prune any job currently running */
671 for (i=0; i < prune_list->num_ids; i++) {
674 if (jcr->JobId == prune_list->JobId[i]) {
675 Dmsg2(050, "skip running job JobId[%d]=%d\n", i, (int)prune_list->JobId[i]);
676 prune_list->JobId[i] = 0;
683 continue; /* don't increment count */
685 Dmsg2(050, "accept JobId[%d]=%d\n", i, (int)prune_list->JobId[i]);