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, Type */
352 static int job_select_handler(void *ctx, int num_fields, char **row)
354 alist *lst = (alist *)ctx;
355 struct accurate_check_ctx *res;
356 ASSERT(num_fields == 6);
358 /* If this job doesn't exist anymore in the configuration, delete it */
359 if (GetResWithName(R_JOB, row[0]) == NULL) {
363 /* If this fileset doesn't exist anymore in the configuration, delete it */
364 if (GetResWithName(R_FILESET, row[1]) == NULL) {
368 /* If this client doesn't exist anymore in the configuration, delete it */
369 if (GetResWithName(R_CLIENT, row[2]) == NULL) {
373 /* Don't compute accurate things for Verify jobs */
374 if (*row[5] == 'V') {
378 res = (struct accurate_check_ctx*) malloc(sizeof(struct accurate_check_ctx));
379 res->FileSetId = str_to_int64(row[3]);
380 res->ClientId = str_to_int64(row[4]);
383 // Dmsg2(150, "row=%d val=%d\n", del->num_ids-1, del->JobId[del->num_ids-1]);
388 * Pruning Jobs is a bit more complicated than purging Files
389 * because we delete Job records only if there is a more current
390 * backup of the FileSet. Otherwise, we keep the Job record.
391 * In other words, we never delete the only Job record that
392 * contains a current backup of a FileSet. This prevents the
393 * Volume from being recycled and destroying a current backup.
395 * For Verify Jobs, we do not delete the last InitCatalog.
397 * For Restore Jobs there are no restrictions.
399 int prune_jobs(UAContext *ua, CLIENT *client, POOL *pool, int JobType)
402 POOL_MEM query(PM_MESSAGE);
405 char ed1[50], ed2[50];
406 alist *jobids_check=NULL;
407 struct accurate_check_ctx *elt;
408 db_list_ctx jobids, tempids;
412 memset(&cr, 0, sizeof(cr));
414 bstrncpy(cr.Name, client->name(), sizeof(cr.Name));
415 if (!db_create_client_record(ua->jcr, ua->db, &cr)) {
420 if (pool && pool->JobRetention > 0) {
421 period = pool->JobRetention;
423 period = client->JobRetention;
425 now = (utime_t)time(NULL);
427 /* Drop any previous temporary tables still there */
428 drop_temp_tables(ua);
430 /* Create temp tables and indicies */
431 if (!create_temp_tables(ua)) {
435 edit_utime(period, ed1, sizeof(ed1));
436 Jmsg(ua->jcr, M_INFO, 0, _("Begin pruning Jobs older than %s.\n"), ed1);
438 edit_int64(now - period, ed1); /* Jobs older than ed1 are good candidates */
439 edit_int64(cr.ClientId, ed2);
441 memset(&del, 0, sizeof(del));
443 del.JobId = (JobId_t *)malloc(sizeof(JobId_t) * del.max_ids);
444 del.PurgedFiles = (char *)malloc(del.max_ids);
447 * Select all files that are older than the JobRetention period
448 * and stuff them into the "DeletionCandidates" table.
451 "INSERT INTO DelCandidates "
452 "SELECT JobId,PurgedFiles,FileSetId,JobFiles,JobStatus "
454 "WHERE Type IN ('B', 'C', 'M', 'V', 'D', 'R', 'c', 'm') "
455 "AND JobTDate<%s AND ClientId=%s",
458 if (!db_sql_query(ua->db, query.c_str(), NULL, (void *)NULL)) {
460 ua->error_msg("%s", db_strerror(ua->db));
465 /* Now, for the selection, we discard some of them in order to be always
466 * able to restore files. (ie, last full, last diff, last incrs)
467 * Note: The DISTINCT could be more useful if we don't get FileSetId
469 jobids_check = New(alist(10, owned_by_alist));
471 "SELECT DISTINCT Job.Name, FileSet, Client.Name, Job.FileSetId, "
472 "Job.ClientId, Job.Type "
473 "FROM DelCandidates "
474 "JOIN Job USING (JobId) "
475 "JOIN Client USING (ClientId) "
476 "JOIN FileSet ON (Job.FileSetId = FileSet.FileSetId) "
477 "WHERE Type NOT IN ('D', 'R', 'c', 'm') " /* Discard Admin, Restore, Copy, Migration */
480 /* The job_select_handler will skip jobs or filesets that are no longer
481 * in the configuration file. Interesting ClientId/FileSetId will be
482 * added to jobids_check
484 if (!db_sql_query(ua->db, query.c_str(), job_select_handler, jobids_check)) {
485 ua->error_msg("%s", db_strerror(ua->db));
488 /* For this selection, we exclude current jobs used for restore or
489 * accurate. This will prevent to prune the last full backup used for
490 * current backup & restore
492 memset(&jr, 0, sizeof(jr));
493 /* To find useful jobs, we do like an incremental */
494 jr.JobLevel = L_INCREMENTAL;
495 foreach_alist(elt, jobids_check) {
496 jr.ClientId = elt->ClientId;
497 jr.FileSetId = elt->FileSetId;
498 db_accurate_get_jobids(ua->jcr, ua->db, &jr, &tempids);
502 /* Discard latest Verify level=InitCatalog job */
504 "SELECT JobId, JobTDate "
506 "WHERE JobTDate<%s AND ClientId=%s "
507 "AND Type='V' AND Level='V' "
508 "ORDER BY JobTDate DESC LIMIT 1",
511 if (!db_sql_query(ua->db, query.c_str(), db_list_handler, &jobids)) {
512 ua->error_msg("%s", db_strerror(ua->db));
515 /* If we found jobs to exclude from the DelCandidates list, we should
516 * also remove BaseJobs that can be linked with them
518 if (jobids.count > 0) {
519 Dmsg1(60, "jobids to exclude before basejobs = %s\n", jobids.list);
520 /* We also need to exclude all basejobs used */
521 db_get_used_base_jobids(ua->jcr, ua->db, jobids.list, &jobids);
523 /* Removing exceptions from the DelCandidates list */
524 Mmsg(query, "DELETE FROM DelCandidates WHERE JobId IN (%s)", jobids.list);
525 if (!db_sql_query(ua->db, query.c_str(), NULL, NULL)) {
526 ua->error_msg("%s", db_strerror(ua->db));
527 goto bail_out; /* Don't continue if the list isn't clean */
529 Dmsg1(60, "jobids to exclude = %s\n", jobids.list);
532 /* Prune garbage jobs (JobStatus not successful) */
534 "INSERT INTO DelCandidates "
535 "SELECT JobId,PurgedFiles,FileSetId,JobFiles,JobStatus "
537 "WHERE ( JobFiles=0 "
538 "OR JobStatus NOT IN ('T', 'W') "
541 "AND ClientId = %s ",
544 Dmsg1(150, "Query=%s\n", query.c_str());
545 if (!db_sql_query(ua->db, query.c_str(), NULL, NULL)) {
546 ua->error_msg("%s", db_strerror(ua->db));
549 /* We use DISTINCT because we can have two times the same job */
551 "SELECT DISTINCT DelCandidates.JobId,DelCandidates.PurgedFiles "
552 "FROM DelCandidates");
553 if (!db_sql_query(ua->db, query.c_str(), job_delete_handler, (void *)&del)) {
554 ua->error_msg("%s", db_strerror(ua->db));
557 purge_job_list_from_catalog(ua, del);
559 if (del.num_del > 0) {
560 ua->info_msg(_("Pruned %d %s for client %s from catalog.\n"), del.num_del,
561 del.num_del==1?_("Job"):_("Jobs"), client->name());
562 } else if (ua->verbose) {
563 ua->info_msg(_("No Jobs found to prune.\n"));
567 drop_temp_tables(ua);
572 if (del.PurgedFiles) {
573 free(del.PurgedFiles);
582 * Prune a given Volume
584 bool prune_volume(UAContext *ua, MEDIA_DBR *mr)
586 POOL_MEM query(PM_MESSAGE);
591 if (mr->Enabled == 2) {
592 return false; /* Cannot prune archived volumes */
595 memset(&del, 0, sizeof(del));
597 del.JobId = (JobId_t *)malloc(sizeof(JobId_t) * del.max_ids);
601 /* Prune only Volumes with status "Full", or "Used" */
602 if (strcmp(mr->VolStatus, "Full") == 0 ||
603 strcmp(mr->VolStatus, "Used") == 0) {
604 Dmsg2(050, "get prune list MediaId=%d Volume %s\n", (int)mr->MediaId, mr->VolumeName);
605 count = get_prune_list_for_volume(ua, mr, &del);
606 Dmsg1(050, "Num pruned = %d\n", count);
608 purge_job_list_from_catalog(ua, del);
610 ok = is_volume_purged(ua, mr);
621 * Get prune list for a volume
623 int get_prune_list_for_volume(UAContext *ua, MEDIA_DBR *mr, del_ctx *del)
625 POOL_MEM query(PM_MESSAGE);
628 char ed1[50], ed2[50];
630 if (mr->Enabled == 2) {
631 return 0; /* cannot prune Archived volumes */
635 * Now add to the list of JobIds for Jobs written to this Volume
637 edit_int64(mr->MediaId, ed1);
638 period = mr->VolRetention;
639 now = (utime_t)time(NULL);
640 edit_int64(now-period, ed2);
641 Mmsg(query, sel_JobMedia, ed1, ed2);
642 Dmsg3(250, "Now=%d period=%d now-period=%s\n", (int)now, (int)period,
645 Dmsg1(050, "Query=%s\n", query.c_str());
646 if (!db_sql_query(ua->db, query.c_str(), file_delete_handler, (void *)del)) {
648 ua->error_msg("%s", db_strerror(ua->db));
650 Dmsg0(050, "Count failed\n");
653 count = exclude_running_jobs_from_list(del);
660 * We have a list of jobs to prune or purge. If any of them is
661 * currently running, we set its JobId to zero which effectively
664 * Returns the number of jobs that can be prunned or purged.
667 int exclude_running_jobs_from_list(del_ctx *prune_list)
674 /* Do not prune any job currently running */
675 for (i=0; i < prune_list->num_ids; i++) {
678 if (jcr->JobId == prune_list->JobId[i]) {
679 Dmsg2(050, "skip running job JobId[%d]=%d\n", i, (int)prune_list->JobId[i]);
680 prune_list->JobId[i] = 0;
687 continue; /* don't increment count */
689 Dmsg2(050, "accept JobId[%d]=%d\n", i, (int)prune_list->JobId[i]);