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 filesets that are no longer
484 * in the configuration file. Interesting ClientId/FileSetId will be
485 * added to jobids_check
487 if (!db_sql_query(ua->db, query.c_str(), job_select_handler, jobids_check)) {
488 ua->error_msg("%s", db_strerror(ua->db));
491 /* For this selection, we exclude current jobs used for restore or
492 * accurate. This will prevent to prune the last full backup used for
493 * current backup & restore
495 memset(&jr, 0, sizeof(jr));
496 /* To find useful jobs, we do like an incremental */
497 jr.JobLevel = L_INCREMENTAL;
498 foreach_alist(elt, jobids_check) {
499 jr.ClientId = elt->ClientId;
500 jr.FileSetId = elt->FileSetId;
501 db_accurate_get_jobids(ua->jcr, ua->db, &jr, &jobids);
504 /* Discard latest Verify level=InitCatalog job */
506 "SELECT JobId, JobTDate "
508 "WHERE JobTDate<%s AND ClientId=%s "
509 "AND Type='V' AND Level='V' "
510 "ORDER BY JobTDate DESC LIMIT 1",
513 if (!db_sql_query(ua->db, query.c_str(), db_list_handler, &jobids)) {
514 ua->error_msg("%s", db_strerror(ua->db));
517 /* If we found jobs to exclude from the DelCandidates list, we should
518 * also remove BaseJobs that can be linked with them
520 if (jobids.count > 0) {
521 Dmsg1(60, "jobids to exclude before basejobs = %s\n", jobids.list);
522 /* We also need to exclude all basejobs used */
523 db_get_used_base_jobids(ua->jcr, ua->db, jobids.list, &jobids);
525 /* Removing exceptions from the DelCandidates list */
526 Mmsg(query, "DELETE FROM DelCandidates WHERE JobId IN (%s)", jobids.list);
527 if (!db_sql_query(ua->db, query.c_str(), NULL, NULL)) {
528 ua->error_msg("%s", db_strerror(ua->db));
529 goto bail_out; /* Don't continue if the list isn't clean */
531 Dmsg1(60, "jobids to exclude = %s\n", jobids.list);
534 /* Prune garbage jobs (JobStatus not successful) */
536 "INSERT INTO DelCandidates "
537 "SELECT JobId,PurgedFiles,FileSetId,JobFiles,JobStatus "
539 "WHERE ( JobFiles=0 "
540 "OR JobStatus NOT IN ('T', 'W') "
543 "AND ClientId = %s ",
546 Dmsg1(150, "Query=%s\n", query.c_str());
547 if (!db_sql_query(ua->db, query.c_str(), NULL, NULL)) {
548 ua->error_msg("%s", db_strerror(ua->db));
551 /* We use DISTINCT because we can have two times the same job */
553 "SELECT DISTINCT DelCandidates.JobId,DelCandidates.PurgedFiles "
554 "FROM DelCandidates");
555 if (!db_sql_query(ua->db, query.c_str(), job_delete_handler, (void *)&del)) {
556 ua->error_msg("%s", db_strerror(ua->db));
559 purge_job_list_from_catalog(ua, del);
561 if (del.num_del > 0) {
562 ua->info_msg(_("Pruned %d %s for client %s from catalog.\n"), del.num_del,
563 del.num_del==1?_("Job"):_("Jobs"), client->name());
564 } else if (ua->verbose) {
565 ua->info_msg(_("No Jobs found to prune.\n"));
569 drop_temp_tables(ua);
574 if (del.PurgedFiles) {
575 free(del.PurgedFiles);
584 * Prune a given Volume
586 bool prune_volume(UAContext *ua, MEDIA_DBR *mr)
588 POOL_MEM query(PM_MESSAGE);
593 if (mr->Enabled == 2) {
594 return false; /* Cannot prune archived volumes */
597 memset(&del, 0, sizeof(del));
599 del.JobId = (JobId_t *)malloc(sizeof(JobId_t) * del.max_ids);
603 /* Prune only Volumes with status "Full", or "Used" */
604 if (strcmp(mr->VolStatus, "Full") == 0 ||
605 strcmp(mr->VolStatus, "Used") == 0) {
606 Dmsg2(050, "get prune list MediaId=%d Volume %s\n", (int)mr->MediaId, mr->VolumeName);
607 count = get_prune_list_for_volume(ua, mr, &del);
608 Dmsg1(050, "Num pruned = %d\n", count);
610 purge_job_list_from_catalog(ua, del);
612 ok = is_volume_purged(ua, mr);
623 * Get prune list for a volume
625 int get_prune_list_for_volume(UAContext *ua, MEDIA_DBR *mr, del_ctx *del)
627 POOL_MEM query(PM_MESSAGE);
630 char ed1[50], ed2[50];
632 if (mr->Enabled == 2) {
633 return 0; /* cannot prune Archived volumes */
637 * Now add to the list of JobIds for Jobs written to this Volume
639 edit_int64(mr->MediaId, ed1);
640 period = mr->VolRetention;
641 now = (utime_t)time(NULL);
642 edit_int64(now-period, ed2);
643 Mmsg(query, sel_JobMedia, ed1, ed2);
644 Dmsg3(250, "Now=%d period=%d now-period=%s\n", (int)now, (int)period,
647 Dmsg1(050, "Query=%s\n", query.c_str());
648 if (!db_sql_query(ua->db, query.c_str(), file_delete_handler, (void *)del)) {
650 ua->error_msg("%s", db_strerror(ua->db));
652 Dmsg0(050, "Count failed\n");
655 count = exclude_running_jobs_from_list(del);
662 * We have a list of jobs to prune or purge. If any of them is
663 * currently running, we set its JobId to zero which effectively
666 * Returns the number of jobs that can be prunned or purged.
669 int exclude_running_jobs_from_list(del_ctx *prune_list)
676 /* Do not prune any job currently running */
677 for (i=0; i < prune_list->num_ids; i++) {
680 if (jcr->JobId == prune_list->JobId[i]) {
681 Dmsg2(050, "skip running job JobId[%d]=%d\n", i, (int)prune_list->JobId[i]);
682 prune_list->JobId[i] = 0;
689 continue; /* don't increment count */
691 Dmsg2(050, "accept JobId[%d]=%d\n", i, (int)prune_list->JobId[i]);