3 * Bacula Director -- User Agent Database Purge Command
5 * Purges Files from specific JobIds
7 * Purges Jobs from Volumes
9 * Kern Sibbald, February MMII
14 Bacula® - The Network Backup Solution
16 Copyright (C) 2002-2007 Free Software Foundation Europe e.V.
18 The main author of Bacula is Kern Sibbald, with contributions from
19 many others, a complete list can be found in the file AUTHORS.
20 This program is Free Software; you can redistribute it and/or
21 modify it under the terms of version two of the GNU General Public
22 License as published by the Free Software Foundation plus additions
23 that are listed in the file LICENSE.
25 This program is distributed in the hope that it will be useful, but
26 WITHOUT ANY WARRANTY; without even the implied warranty of
27 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
28 General Public License for more details.
30 You should have received a copy of the GNU General Public License
31 along with this program; if not, write to the Free Software
32 Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
35 Bacula® is a registered trademark of John Walker.
36 The licensor of Bacula is the Free Software Foundation Europe
37 (FSFE), Fiduciary Program, Sumatrastrasse 25, 8006 Zürich,
38 Switzerland, email:ftf@fsfeurope.org.
44 /* Forward referenced functions */
45 static int purge_files_from_client(UAContext *ua, CLIENT *client);
46 static int purge_jobs_from_client(UAContext *ua, CLIENT *client);
48 #define MAX_DEL_LIST_LEN 1000000
50 static const char *select_jobsfiles_from_client =
51 "SELECT JobId FROM Job "
55 static const char *select_jobs_from_client =
56 "SELECT JobId, PurgedFiles FROM Job "
60 /* In memory list of JobIds */
61 struct s_file_del_ctx {
63 int num_ids; /* ids stored */
64 int max_ids; /* size of array */
65 int num_del; /* number deleted */
66 int tot_ids; /* total to process */
69 struct s_job_del_ctx {
70 JobId_t *JobId; /* array of JobIds */
71 char *PurgedFiles; /* Array of PurgedFile flags */
72 int num_ids; /* ids stored */
73 int max_ids; /* size of array */
74 int num_del; /* number deleted */
75 int tot_ids; /* total to process */
83 * Called here to count entries to be deleted
85 static int count_handler(void *ctx, int num_fields, char **row)
87 struct s_count_ctx *cnt = (struct s_count_ctx *)ctx;
90 cnt->count = str_to_int64(row[0]);
98 * Called here to count entries to be deleted
100 static int file_count_handler(void *ctx, int num_fields, char **row)
102 struct s_file_del_ctx *del = (struct s_file_del_ctx *)ctx;
108 static int job_count_handler(void *ctx, int num_fields, char **row)
110 struct s_job_del_ctx *del = (struct s_job_del_ctx *)ctx;
117 * Called here to make in memory list of JobIds to be
118 * deleted and the associated PurgedFiles flag.
119 * The in memory list will then be transversed
120 * to issue the SQL DELETE commands. Note, the list
121 * is allowed to get to MAX_DEL_LIST_LEN to limit the
122 * maximum malloc'ed memory.
124 static int job_delete_handler(void *ctx, int num_fields, char **row)
126 struct s_job_del_ctx *del = (struct s_job_del_ctx *)ctx;
128 if (del->num_ids == MAX_DEL_LIST_LEN) {
131 if (del->num_ids == del->max_ids) {
132 del->max_ids = (del->max_ids * 3) / 2;
133 del->JobId = (JobId_t *)brealloc(del->JobId, sizeof(JobId_t) * del->max_ids);
134 del->PurgedFiles = (char *)brealloc(del->PurgedFiles, del->max_ids);
136 del->JobId[del->num_ids] = (JobId_t)str_to_int64(row[0]);
137 del->PurgedFiles[del->num_ids++] = (char)str_to_int64(row[1]);
141 static int file_delete_handler(void *ctx, int num_fields, char **row)
143 struct s_file_del_ctx *del = (struct s_file_del_ctx *)ctx;
145 if (del->num_ids == MAX_DEL_LIST_LEN) {
148 if (del->num_ids == del->max_ids) {
149 del->max_ids = (del->max_ids * 3) / 2;
150 del->JobId = (JobId_t *)brealloc(del->JobId, sizeof(JobId_t) *
153 del->JobId[del->num_ids++] = (JobId_t)str_to_int64(row[0]);
158 * Purge records from database
160 * Purge Files (from) [Job|JobId|Client|Volume]
161 * Purge Jobs (from) [Client|Volume]
163 * N.B. Not all above is implemented yet.
165 int purgecmd(UAContext *ua, const char *cmd)
171 static const char *keywords[] = {
177 static const char *files_keywords[] = {
184 static const char *jobs_keywords[] = {
190 "\nThis command is can be DANGEROUS!!!\n\n"
191 "It purges (deletes) all Files from a Job,\n"
192 "JobId, Client or Volume; or it purges (deletes)\n"
193 "all Jobs from a Client or Volume without regard\n"
194 "for retention periods. Normally you should use the\n"
195 "PRUNE command, which respects retention periods.\n"));
200 switch (find_arg_keyword(ua, keywords)) {
203 switch(find_arg_keyword(ua, files_keywords)) {
206 if (get_job_dbr(ua, &jr)) {
207 purge_files_from_job(ua, jr.JobId);
211 client = get_client_resource(ua);
213 purge_files_from_client(ua, client);
217 if (select_media_dbr(ua, &mr)) {
218 purge_files_from_volume(ua, &mr);
224 switch(find_arg_keyword(ua, jobs_keywords)) {
226 client = get_client_resource(ua);
228 purge_jobs_from_client(ua, client);
232 if (select_media_dbr(ua, &mr)) {
233 purge_jobs_from_volume(ua, &mr);
239 while ((i=find_arg(ua, NT_("volume"))) >= 0) {
240 if (select_media_dbr(ua, &mr)) {
241 purge_jobs_from_volume(ua, &mr);
243 *ua->argk[i] = 0; /* zap keyword already seen */
250 switch (do_keyword_prompt(ua, _("Choose item to purge"), keywords)) {
252 client = get_client_resource(ua);
254 purge_files_from_client(ua, client);
258 client = get_client_resource(ua);
260 purge_jobs_from_client(ua, client);
264 if (select_media_dbr(ua, &mr)) {
265 purge_jobs_from_volume(ua, &mr);
273 * Purge File records from the database. For any Job which
274 * is older than the retention period, we unconditionally delete
275 * all File records for that Job. This is simple enough that no
276 * temporary tables are needed. We simply make an in memory list of
277 * the JobIds meeting the prune conditions, then delete all File records
278 * pointing to each of those JobIds.
280 static int purge_files_from_client(UAContext *ua, CLIENT *client)
282 struct s_file_del_ctx del;
283 POOLMEM *query = get_pool_memory(PM_MESSAGE);
288 memset(&cr, 0, sizeof(cr));
289 memset(&del, 0, sizeof(del));
291 bstrncpy(cr.Name, client->name(), sizeof(cr.Name));
292 if (!db_create_client_record(ua->jcr, ua->db, &cr)) {
295 bsendmsg(ua, _("Begin purging files for Client \"%s\"\n"), cr.Name);
296 Mmsg(query, select_jobsfiles_from_client, edit_int64(cr.ClientId, ed1));
298 Dmsg1(050, "select sql=%s\n", query);
300 if (!db_sql_query(ua->db, query, file_count_handler, (void *)&del)) {
301 bsendmsg(ua, "%s", db_strerror(ua->db));
302 Dmsg0(050, "Count failed\n");
306 if (del.tot_ids == 0) {
307 bsendmsg(ua, _("No Files found for client %s to purge from %s catalog.\n"),
308 client->name(), client->catalog->name());
312 if (del.tot_ids < MAX_DEL_LIST_LEN) {
313 del.max_ids = del.tot_ids + 1;
315 del.max_ids = MAX_DEL_LIST_LEN;
319 del.JobId = (JobId_t *)malloc(sizeof(JobId_t) * del.max_ids);
321 db_sql_query(ua->db, query, file_delete_handler, (void *)&del);
323 for (i=0; i < del.num_ids; i++) {
324 purge_files_from_job(ua, del.JobId[i]);
326 bsendmsg(ua, _("%d Files for client \"%s\" purged from %s catalog.\n"), del.num_ids,
327 client->name(), client->catalog->name());
333 free_pool_memory(query);
340 * Purge Job records from the database. For any Job which
341 * is older than the retention period, we unconditionally delete
342 * it and all File records for that Job. This is simple enough that no
343 * temporary tables are needed. We simply make an in memory list of
344 * the JobIds then delete the Job, Files, and JobMedia records in that list.
346 static int purge_jobs_from_client(UAContext *ua, CLIENT *client)
348 struct s_job_del_ctx del;
349 POOLMEM *query = get_pool_memory(PM_MESSAGE);
354 memset(&cr, 0, sizeof(cr));
355 memset(&del, 0, sizeof(del));
357 bstrncpy(cr.Name, client->name(), sizeof(cr.Name));
358 if (!db_create_client_record(ua->jcr, ua->db, &cr)) {
362 bsendmsg(ua, _("Begin purging jobs from Client \"%s\"\n"), cr.Name);
363 Mmsg(query, select_jobs_from_client, edit_int64(cr.ClientId, ed1));
365 Dmsg1(050, "select sql=%s\n", query);
367 if (!db_sql_query(ua->db, query, job_count_handler, (void *)&del)) {
368 bsendmsg(ua, "%s", db_strerror(ua->db));
369 Dmsg0(050, "Count failed\n");
372 if (del.tot_ids == 0) {
373 bsendmsg(ua, _("No Jobs found for client %s to purge from %s catalog.\n"),
374 client->name(), client->catalog->name());
378 if (del.tot_ids < MAX_DEL_LIST_LEN) {
379 del.max_ids = del.tot_ids + 1;
381 del.max_ids = MAX_DEL_LIST_LEN;
386 del.JobId = (JobId_t *)malloc(sizeof(JobId_t) * del.max_ids);
387 del.PurgedFiles = (char *)malloc(del.max_ids);
389 db_sql_query(ua->db, query, job_delete_handler, (void *)&del);
392 * OK, now we have the list of JobId's to be purged, first check
393 * if the Files have been purged, if not, purge (delete) them.
394 * Then delete the Job entry, and finally and JobMedia records.
396 for (i=0; i < del.num_ids; i++) {
397 Dmsg1(050, "Delete Files JobId=%s\n", ed1);
398 if (!del.PurgedFiles[i]) {
399 purge_files_from_job(ua, del.JobId[i]);
401 purge_job_from_catalog(ua, del.JobId[i]);
403 bsendmsg(ua, _("%d Jobs for client %s purged from %s catalog.\n"), del.num_ids,
404 client->name(), client->catalog->name());
410 if (del.PurgedFiles) {
411 free(del.PurgedFiles);
413 free_pool_memory(query);
417 void purge_job_from_catalog(UAContext *ua, JobId_t JobId)
419 POOL_MEM query(PM_MESSAGE);
422 /* Delete (or purge) records associated with the job */
423 purge_job_records_from_catalog(ua, JobId);
425 /* Now remove the Job record itself */
426 edit_int64(JobId, ed1);
427 Mmsg(query, "DELETE FROM Job WHERE JobId=%s", ed1);
428 db_sql_query(ua->db, query.c_str(), NULL, (void *)NULL);
429 Dmsg1(050, "Delete Job sql=%s\n", query.c_str());
433 * This removes all the records associated with a Job from
434 * the catalog (i.e. prunes it) without removing the Job
437 void purge_job_records_from_catalog(UAContext *ua, JobId_t JobId)
439 POOL_MEM query(PM_MESSAGE);
442 purge_files_from_job(ua, JobId);
444 edit_int64(JobId, ed1);
445 Mmsg(query, "DELETE FROM JobMedia WHERE JobId=%s", ed1);
446 db_sql_query(ua->db, query.c_str(), NULL, (void *)NULL);
447 Dmsg1(050, "Delete JobMedia sql=%s\n", query.c_str());
449 Mmsg(query, "DELETE FROM Log WHERE JobId=%s", ed1);
450 db_sql_query(ua->db, query.c_str(), NULL, (void *)NULL);
451 Dmsg1(050, "Delete Log sql=%s\n", query.c_str());
457 * Remove File records for a particular Job.
459 void purge_files_from_job(UAContext *ua, JobId_t JobId)
461 POOL_MEM query(PM_MESSAGE);
464 edit_int64(JobId, ed1);
465 Mmsg(query, del_File, ed1);
466 db_sql_query(ua->db, query.c_str(), NULL, (void *)NULL);
469 * Now mark Job as having files purged. This is necessary to
470 * avoid having too many Jobs to process in future prunings. If
471 * we don't do this, the number of JobId's in our in memory list
472 * could grow very large.
474 Mmsg(query, upd_Purged, ed1);
475 db_sql_query(ua->db, query.c_str(), NULL, (void *)NULL);
478 void purge_files_from_volume(UAContext *ua, MEDIA_DBR *mr )
479 {} /* ***FIXME*** implement */
482 * Returns: 1 if Volume purged
483 * 0 if Volume not purged
485 int purge_jobs_from_volume(UAContext *ua, MEDIA_DBR *mr)
487 POOLMEM *query = get_pool_memory(PM_MESSAGE);
488 struct s_count_ctx cnt;
489 struct s_file_del_ctx del;
494 stat = strcmp(mr->VolStatus, "Append") == 0 ||
495 strcmp(mr->VolStatus, "Full") == 0 ||
496 strcmp(mr->VolStatus, "Used") == 0 ||
497 strcmp(mr->VolStatus, "Error") == 0;
500 bsendmsg(ua, _("Volume \"%s\" has VolStatus \"%s\" and cannot be purged.\n"
501 "The VolStatus must be: Append, Full, Used, or Error to be purged.\n"),
502 mr->VolumeName, mr->VolStatus);
506 memset(&jr, 0, sizeof(jr));
507 memset(&del, 0, sizeof(del));
509 Mmsg(query, "SELECT count(*) FROM JobMedia WHERE MediaId=%s",
510 edit_int64(mr->MediaId, ed1));
511 if (!db_sql_query(ua->db, query, count_handler, (void *)&cnt)) {
512 bsendmsg(ua, "%s", db_strerror(ua->db));
513 Dmsg0(050, "Count failed\n");
517 if (cnt.count == 0) {
518 bsendmsg(ua, _("There are no Jobs associated with Volume \"%s\". Marking it purged.\n"),
520 if (!mark_media_purged(ua, mr)) {
521 bsendmsg(ua, "%s", db_strerror(ua->db));
527 if (cnt.count < MAX_DEL_LIST_LEN) {
528 del.max_ids = cnt.count + 1;
530 del.max_ids = MAX_DEL_LIST_LEN;
534 * Check if he wants to purge a single jobid
536 i = find_arg_with_value(ua, "jobid");
538 del.JobId = (JobId_t *)malloc(sizeof(JobId_t));
540 del.JobId[0] = str_to_int64(ua->argv[i]);
545 del.JobId = (JobId_t *)malloc(sizeof(JobId_t) * del.max_ids);
547 Mmsg(query, "SELECT JobId FROM JobMedia WHERE MediaId=%s",
548 edit_int64(mr->MediaId, ed1));
549 if (!db_sql_query(ua->db, query, file_delete_handler, (void *)&del)) {
550 bsendmsg(ua, "%s", db_strerror(ua->db));
551 Dmsg0(050, "Count failed\n");
556 for (i=0; i < del.num_ids; i++) {
557 purge_files_from_job(ua, del.JobId[i]);
558 purge_job_from_catalog(ua, del.JobId[i]);
564 bsendmsg(ua, _("%d File%s on Volume \"%s\" purged from catalog.\n"), del.num_del,
565 del.num_del==1?"":"s", mr->VolumeName);
567 /* If purged, mark it so */
569 Mmsg(query, "SELECT count(*) FROM JobMedia WHERE MediaId=%s",
570 edit_int64(mr->MediaId, ed1));
571 if (!db_sql_query(ua->db, query, count_handler, (void *)&cnt)) {
572 bsendmsg(ua, "%s", db_strerror(ua->db));
573 Dmsg0(050, "Count failed\n");
577 if (cnt.count == 0) {
578 bsendmsg(ua, _("There are no more Jobs associated with Volume \"%s\". Marking it purged.\n"),
580 if (!(stat = mark_media_purged(ua, mr))) {
581 bsendmsg(ua, "%s", db_strerror(ua->db));
587 free_pool_memory(query);
592 * IF volume status is Append, Full, Used, or Error, mark it Purged
593 * Purged volumes can then be recycled (if enabled).
595 bool mark_media_purged(UAContext *ua, MEDIA_DBR *mr)
598 if (strcmp(mr->VolStatus, "Append") == 0 ||
599 strcmp(mr->VolStatus, "Full") == 0 ||
600 strcmp(mr->VolStatus, "Used") == 0 ||
601 strcmp(mr->VolStatus, "Error") == 0) {
602 bstrncpy(mr->VolStatus, "Purged", sizeof(mr->VolStatus));
603 if (!db_update_media_record(jcr, ua->db, mr)) {
606 pm_strcpy(jcr->VolumeName, mr->VolumeName);
607 generate_job_event(jcr, "VolumePurged");
609 * If the RecyclePool is defined, move the volume there
611 if (mr->RecyclePoolId && mr->RecyclePoolId != mr->PoolId) {
612 POOL_DBR oldpr, newpr;
613 memset(&oldpr, 0, sizeof(POOL_DBR));
614 memset(&newpr, 0, sizeof(POOL_DBR));
615 newpr.PoolId = mr->RecyclePoolId;
616 oldpr.PoolId = mr->PoolId;
617 if ( db_get_pool_record(jcr, ua->db, &oldpr)
618 && db_get_pool_record(jcr, ua->db, &newpr))
620 /* check if destination pool size is ok */
621 if (newpr.MaxVols > 0 && newpr.NumVols >= newpr.MaxVols) {
622 bsendmsg(ua, _("Unable move recycled Volume in full "
623 "Pool \"%s\" MaxVols=%d\n"),
624 newpr.Name, newpr.MaxVols);
626 } else { /* move media */
627 update_vol_pool(ua, newpr.Name, mr, &oldpr);
630 bsendmsg(ua, "%s", db_strerror(ua->db));
633 /* Send message to Job report, if it is a *real* job */
634 if (jcr && jcr->JobId > 0) {
635 Jmsg1(jcr, M_INFO, 0, _("All records pruned from Volume \"%s\"; marking it \"Purged\"\n"),
640 bsendmsg(ua, _("Cannot purge Volume with VolStatus=%s\n"), mr->VolStatus);
642 return strcmp(mr->VolStatus, "Purged") == 0;