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 */
45 * Called here to count entries to be deleted
47 int del_count_handler(void *ctx, int num_fields, char **row)
49 struct s_count_ctx *cnt = (struct s_count_ctx *)ctx;
52 cnt->count = str_to_int64(row[0]);
61 * Called here to make in memory list of JobIds to be
62 * deleted and the associated PurgedFiles flag.
63 * The in memory list will then be transversed
64 * to issue the SQL DELETE commands. Note, the list
65 * is allowed to get to MAX_DEL_LIST_LEN to limit the
66 * maximum malloc'ed memory.
68 int job_delete_handler(void *ctx, int num_fields, char **row)
70 struct del_ctx *del = (struct del_ctx *)ctx;
72 if (del->num_ids == MAX_DEL_LIST_LEN) {
75 if (del->num_ids == del->max_ids) {
76 del->max_ids = (del->max_ids * 3) / 2;
77 del->JobId = (JobId_t *)brealloc(del->JobId, sizeof(JobId_t) * del->max_ids);
78 del->PurgedFiles = (char *)brealloc(del->PurgedFiles, del->max_ids);
80 del->JobId[del->num_ids] = (JobId_t)str_to_int64(row[0]);
81 // Dmsg2(60, "row=%d val=%d\n", del->num_ids, del->JobId[del->num_ids]);
82 del->PurgedFiles[del->num_ids++] = (char)str_to_int64(row[1]);
86 int file_delete_handler(void *ctx, int num_fields, char **row)
88 struct del_ctx *del = (struct del_ctx *)ctx;
90 if (del->num_ids == MAX_DEL_LIST_LEN) {
93 if (del->num_ids == del->max_ids) {
94 del->max_ids = (del->max_ids * 3) / 2;
95 del->JobId = (JobId_t *)brealloc(del->JobId, sizeof(JobId_t) *
98 del->JobId[del->num_ids++] = (JobId_t)str_to_int64(row[0]);
99 // Dmsg2(150, "row=%d val=%d\n", del->num_ids-1, del->JobId[del->num_ids-1]);
104 * Prune records from database
106 * prune files (from) client=xxx [pool=yyy]
107 * prune jobs (from) client=xxx [pool=yyy]
111 int prunecmd(UAContext *ua, const char *cmd)
121 static const char *keywords[] = {
128 if (!open_client_db(ua)) {
132 /* First search args */
133 kw = find_arg_keyword(ua, keywords);
134 if (kw < 0 || kw > 3) {
135 /* no args, so ask user */
136 kw = do_keyword_prompt(ua, _("Choose item to prune"), keywords);
140 case 0: /* prune files */
141 client = get_client_resource(ua);
142 if (find_arg_with_value(ua, "pool") >= 0) {
143 pool = get_pool_resource(ua);
147 /* Pool File Retention takes precedence over client File Retention */
148 if (pool && pool->FileRetention > 0) {
149 if (!confirm_retention(ua, &pool->FileRetention, "File")) {
152 } else if (!client || !confirm_retention(ua, &client->FileRetention, "File")) {
155 prune_files(ua, client, pool);
157 case 1: /* prune jobs */
158 client = get_client_resource(ua);
159 if (find_arg_with_value(ua, "pool") >= 0) {
160 pool = get_pool_resource(ua);
164 /* Pool Job Retention takes precedence over client Job Retention */
165 if (pool && pool->JobRetention > 0) {
166 if (!confirm_retention(ua, &pool->JobRetention, "Job")) {
169 } else if (!client || !confirm_retention(ua, &client->JobRetention, "Job")) {
172 /* ****FIXME**** allow user to select JobType */
173 prune_jobs(ua, client, pool, JT_BACKUP);
175 case 2: /* prune volume */
176 if (!select_pool_and_media_dbr(ua, &pr, &mr)) {
179 if (mr.Enabled == 2) {
180 ua->error_msg(_("Cannot prune Volume \"%s\" because it is archived.\n"),
184 if (!confirm_retention(ua, &mr.VolRetention, "Volume")) {
187 prune_volume(ua, &mr);
189 case 3: /* prune stats */
190 dir = (DIRRES *)GetNextRes(R_DIRECTOR, NULL);
191 if (!dir->stats_retention) {
194 retention = dir->stats_retention;
195 if (!confirm_retention(ua, &retention, "Statistics")) {
198 prune_stats(ua, retention);
207 /* Prune Job stat records from the database.
210 int prune_stats(UAContext *ua, utime_t retention)
213 POOL_MEM query(PM_MESSAGE);
214 utime_t now = (utime_t)time(NULL);
217 Mmsg(query, "DELETE FROM JobHisto WHERE JobTDate < %s",
218 edit_int64(now - retention, ed1));
219 db_sql_query(ua->db, query.c_str(), NULL, NULL);
222 ua->info_msg(_("Pruned Jobs from JobHisto catalog.\n"));
228 * Prune File records from the database. For any Job which
229 * is older than the retention period, we unconditionally delete
230 * all File records for that Job. This is simple enough that no
231 * temporary tables are needed. We simply make an in memory list of
232 * the JobIds meeting the prune conditions, then delete all File records
233 * pointing to each of those JobIds.
235 * This routine assumes you want the pruning to be done. All checking
236 * must be done before calling this routine.
238 * Note: pool can possibly be NULL.
240 int prune_files(UAContext *ua, CLIENT *client, POOL *pool)
243 struct s_count_ctx cnt;
244 POOL_MEM query(PM_MESSAGE);
247 char ed1[50], ed2[50];
250 memset(&cr, 0, sizeof(cr));
251 memset(&del, 0, sizeof(del));
252 bstrncpy(cr.Name, client->name(), sizeof(cr.Name));
253 if (!db_create_client_record(ua->jcr, ua->db, &cr)) {
258 if (pool && pool->FileRetention > 0) {
259 period = pool->FileRetention;
261 period = client->FileRetention;
263 now = (utime_t)time(NULL);
265 // edit_utime(now-period, ed1, sizeof(ed1));
266 // Jmsg(ua->jcr, M_INFO, 0, _("Begin pruning Jobs older than %s secs.\n"), ed1);
267 Jmsg(ua->jcr, M_INFO, 0, _("Begin pruning Jobs.\n"));
268 /* Select Jobs -- for counting */
269 edit_int64(now - period, ed1);
270 Mmsg(query, count_select_job, ed1, edit_int64(cr.ClientId, ed2));
271 Dmsg3(050, "select now=%u period=%u sql=%s\n", (uint32_t)now,
272 (uint32_t)period, query.c_str());
274 if (!db_sql_query(ua->db, query.c_str(), del_count_handler, (void *)&cnt)) {
275 ua->error_msg("%s", db_strerror(ua->db));
276 Dmsg0(050, "Count failed\n");
280 if (cnt.count == 0) {
282 ua->warning_msg(_("No Files found to prune.\n"));
287 if (cnt.count < MAX_DEL_LIST_LEN) {
288 del.max_ids = cnt.count + 1;
290 del.max_ids = MAX_DEL_LIST_LEN;
294 del.JobId = (JobId_t *)malloc(sizeof(JobId_t) * del.max_ids);
296 /* Now process same set but making a delete list */
297 Mmsg(query, select_job, edit_int64(now - period, ed1),
298 edit_int64(cr.ClientId, ed2));
299 db_sql_query(ua->db, query.c_str(), file_delete_handler, (void *)&del);
301 purge_files_from_job_list(ua, del);
303 edit_uint64_with_commas(del.num_del, ed1);
304 ua->info_msg(_("Pruned Files from %s Jobs for client %s from catalog.\n"),
305 ed1, client->name());
316 static void drop_temp_tables(UAContext *ua)
319 for (i=0; drop_deltabs[i]; i++) {
320 db_sql_query(ua->db, drop_deltabs[i], NULL, (void *)NULL);
324 static bool create_temp_tables(UAContext *ua)
326 /* Create temp tables and indicies */
327 if (!db_sql_query(ua->db, create_deltabs[db_type], NULL, (void *)NULL)) {
328 ua->error_msg("%s", db_strerror(ua->db));
329 Dmsg0(050, "create DelTables table failed\n");
332 if (!db_sql_query(ua->db, create_delindex, NULL, (void *)NULL)) {
333 ua->error_msg("%s", db_strerror(ua->db));
334 Dmsg0(050, "create DelInx1 index failed\n");
343 * Pruning Jobs is a bit more complicated than purging Files
344 * because we delete Job records only if there is a more current
345 * backup of the FileSet. Otherwise, we keep the Job record.
346 * In other words, we never delete the only Job record that
347 * contains a current backup of a FileSet. This prevents the
348 * Volume from being recycled and destroying a current backup.
350 * For Verify Jobs, we do not delete the last InitCatalog.
352 * For Restore Jobs there are no restrictions.
354 int prune_jobs(UAContext *ua, CLIENT *client, POOL *pool, int JobType)
357 POOL_MEM query(PM_MESSAGE);
360 char ed1[50], ed2[50];
363 memset(&cr, 0, sizeof(cr));
365 bstrncpy(cr.Name, client->name(), sizeof(cr.Name));
366 if (!db_create_client_record(ua->jcr, ua->db, &cr)) {
371 if (pool && pool->JobRetention > 0) {
372 period = pool->JobRetention;
374 period = client->JobRetention;
376 now = (utime_t)time(NULL);
378 /* Drop any previous temporary tables still there */
379 drop_temp_tables(ua);
381 /* Create temp tables and indicies */
382 if (!create_temp_tables(ua)) {
386 edit_utime(period, ed1, sizeof(ed1));
387 Jmsg(ua->jcr, M_INFO, 0, _("Begin pruning Jobs older than %s.\n"), ed1);
389 edit_int64(now - period, ed1); /* Jobs older than ed1 are good candidates */
390 edit_int64(cr.ClientId, ed2);
392 memset(&del, 0, sizeof(del));
394 del.JobId = (JobId_t *)malloc(sizeof(JobId_t) * del.max_ids);
395 del.PurgedFiles = (char *)malloc(del.max_ids);
397 /* Prune garbage jobs (JobStatus not successful) */
399 "SELECT JobId, PurgedFiles FROM Job "
400 "WHERE ( JobFiles=0 "
401 "OR JobStatus NOT IN ('T', 'W') "
404 "AND ClientId = %s ",
407 Dmsg1(150, "Query=%s\n", query.c_str());
408 if (!db_sql_query(ua->db, query.c_str(), job_delete_handler, (void *)&del)) {
409 ua->error_msg("%s", db_strerror(ua->db));
412 /* Prune Admin, Restore, Copy and Migration jobs */
414 "SELECT JobId, PurgedFiles FROM Job "
415 "WHERE JobType IN ('D', 'R', 'c', 'm') "
417 "AND ClientId = %s ",
420 Dmsg1(150, "Query=%s\n", query.c_str());
421 if (!db_sql_query(ua->db, query.c_str(), job_delete_handler, (void *)&del)) {
422 ua->error_msg("%s", db_strerror(ua->db));
425 /* Select all backups that can be used */
426 /* need to check by fileset name */
427 /* need to check unused base jobs */
428 /* how we do with old fileset.... */
429 /* did we check only for defined jobs ? */
431 "SELECT JobId, FileSet, PurgedFiles FROM Job "
432 "WHERE JobType IN = 'B' "
434 "AND ClientId = %s ",
437 Dmsg1(150, "Query=%s\n", query.c_str());
438 if (!db_sql_query(ua->db, query.c_str(), job_delete_handler, (void *)&del)) {
439 ua->error_msg("%s", db_strerror(ua->db));
443 * Select all files that are older than the JobRetention period
444 * and stuff them into the "DeletionCandidates" table.
446 Mmsg(query, insert_delcand, (char)JobType, ed1,
447 edit_int64(cr.ClientId, ed2));
448 if (!db_sql_query(ua->db, query.c_str(), NULL, (void *)NULL)) {
450 ua->error_msg("%s", db_strerror(ua->db));
452 Dmsg0(050, "insert delcand failed\n");
459 Mmsg(query, select_backup_del, ed1, ed2);
462 Mmsg(query, select_restore_del, ed1, ed2);
465 Mmsg(query, select_verify_del, ed1, ed2);
468 Mmsg(query, select_admin_del, ed1, ed2);
471 Mmsg(query, select_copy_del, ed1, ed2);
474 Mmsg(query, select_migrate_del, ed1, ed2);
478 Dmsg1(150, "Query=%s\n", query.c_str());
479 if (!db_sql_query(ua->db, query.c_str(), job_delete_handler, (void *)&del)) {
480 ua->error_msg("%s", db_strerror(ua->db));
483 purge_job_list_from_catalog(ua, del);
485 if (del.num_del > 0) {
486 ua->info_msg(_("Pruned %d %s for client %s from catalog.\n"), del.num_del,
487 del.num_del==1?_("Job"):_("Jobs"), client->name());
488 } else if (ua->verbose) {
489 ua->info_msg(_("No Jobs found to prune.\n"));
493 drop_temp_tables(ua);
498 if (del.PurgedFiles) {
499 free(del.PurgedFiles);
505 * Prune a given Volume
507 bool prune_volume(UAContext *ua, MEDIA_DBR *mr)
509 POOL_MEM query(PM_MESSAGE);
514 if (mr->Enabled == 2) {
515 return false; /* Cannot prune archived volumes */
518 memset(&del, 0, sizeof(del));
520 del.JobId = (JobId_t *)malloc(sizeof(JobId_t) * del.max_ids);
524 /* Prune only Volumes with status "Full", or "Used" */
525 if (strcmp(mr->VolStatus, "Full") == 0 ||
526 strcmp(mr->VolStatus, "Used") == 0) {
527 Dmsg2(050, "get prune list MediaId=%d Volume %s\n", (int)mr->MediaId, mr->VolumeName);
528 count = get_prune_list_for_volume(ua, mr, &del);
529 Dmsg1(050, "Num pruned = %d\n", count);
531 purge_job_list_from_catalog(ua, del);
533 ok = is_volume_purged(ua, mr);
544 * Get prune list for a volume
546 int get_prune_list_for_volume(UAContext *ua, MEDIA_DBR *mr, del_ctx *del)
548 POOL_MEM query(PM_MESSAGE);
551 char ed1[50], ed2[50];
553 if (mr->Enabled == 2) {
554 return 0; /* cannot prune Archived volumes */
558 * Now add to the list of JobIds for Jobs written to this Volume
560 edit_int64(mr->MediaId, ed1);
561 period = mr->VolRetention;
562 now = (utime_t)time(NULL);
563 edit_int64(now-period, ed2);
564 Mmsg(query, sel_JobMedia, ed1, ed2);
565 Dmsg3(250, "Now=%d period=%d now-period=%s\n", (int)now, (int)period,
568 Dmsg1(050, "Query=%s\n", query.c_str());
569 if (!db_sql_query(ua->db, query.c_str(), file_delete_handler, (void *)del)) {
571 ua->error_msg("%s", db_strerror(ua->db));
573 Dmsg0(050, "Count failed\n");
576 count = exclude_running_jobs_from_list(del);
583 * We have a list of jobs to prune or purge. If any of them is
584 * currently running, we set its JobId to zero which effectively
587 * Returns the number of jobs that can be prunned or purged.
590 int exclude_running_jobs_from_list(del_ctx *prune_list)
597 /* Do not prune any job currently running */
598 for (i=0; i < prune_list->num_ids; i++) {
601 if (jcr->JobId == prune_list->JobId[i]) {
602 Dmsg2(050, "skip running job JobId[%d]=%d\n", i, (int)prune_list->JobId[i]);
603 prune_list->JobId[i] = 0;
610 continue; /* don't increment count */
612 Dmsg2(050, "accept JobId[%d]=%d\n", i, (int)prune_list->JobId[i]);