2 Bacula(R) - The Network Backup Solution
4 Copyright (C) 2000-2015 Kern Sibbald
6 The original author of Bacula is Kern Sibbald, with contributions
7 from many others, a complete list can be found in the file AUTHORS.
9 You may use this file and others of this release according to the
10 license defined in the LICENSE file, which includes the Affero General
11 Public License, v3.0 ("AGPLv3") and some additional permissions and
12 terms pursuant to its AGPLv3 Section 7.
14 This notice must be preserved when any source code is
15 conveyed and/or propagated.
17 Bacula(R) is a registered trademark of Kern Sibbald.
21 * Bacula Director -- User Agent Database Purge Command
23 * Purges Files from specific JobIds
25 * Purges Jobs from Volumes
27 * Kern Sibbald, February MMII
34 /* Forward referenced functions */
35 static int purge_files_from_client(UAContext *ua, CLIENT *client);
36 static int purge_jobs_from_client(UAContext *ua, CLIENT *client);
37 int truncate_cmd(UAContext *ua, const char *cmd);
39 static const char *select_jobsfiles_from_client =
40 "SELECT JobId FROM Job "
44 static const char *select_jobs_from_client =
45 "SELECT JobId, PurgedFiles FROM Job "
49 * Purge records from database
51 * Purge Files (from) [Job|JobId|Client|Volume]
52 * Purge Jobs (from) [Client|Volume]
55 * N.B. Not all above is implemented yet.
57 int purge_cmd(UAContext *ua, const char *cmd)
63 memset(&jr, 0, sizeof(jr));
65 static const char *keywords[] = {
71 static const char *files_keywords[] = {
78 static const char *jobs_keywords[] = {
83 /* Special case for the "Action On Purge", this option is working only on
84 * Purged volume, so no jobs or files will be purged.
85 * We are skiping this message if "purge volume action=xxx"
87 if (!(find_arg(ua, "volume") >= 0 && find_arg(ua, "action") >= 0)) {
89 "\nThis command can be DANGEROUS!!!\n\n"
90 "It purges (deletes) all Files from a Job,\n"
91 "JobId, Client or Volume; or it purges (deletes)\n"
92 "all Jobs from a Client or Volume without regard\n"
93 "to retention periods. Normally you should use the\n"
94 "PRUNE command, which respects retention periods.\n"));
97 if (!open_new_client_db(ua)) {
100 switch (find_arg_keyword(ua, keywords)) {
103 switch(find_arg_keyword(ua, files_keywords)) {
106 if (get_job_dbr(ua, &jr)) {
108 edit_int64(jr.JobId, jobid);
109 purge_files_from_jobs(ua, jobid);
113 client = get_client_resource(ua);
115 purge_files_from_client(ua, client);
119 if (select_media_dbr(ua, &mr)) {
120 purge_files_from_volume(ua, &mr);
126 switch(find_arg_keyword(ua, jobs_keywords)) {
128 client = get_client_resource(ua);
130 purge_jobs_from_client(ua, client);
134 if (select_media_dbr(ua, &mr)) {
135 purge_jobs_from_volume(ua, &mr, /*force*/true);
141 /* Perform ActionOnPurge (action=truncate) */
142 if (find_arg(ua, "action") >= 0) {
143 return truncate_cmd(ua, ua->cmd);
146 while ((i=find_arg(ua, NT_("volume"))) >= 0) {
147 if (select_media_dbr(ua, &mr)) {
148 purge_jobs_from_volume(ua, &mr, /*force*/true);
150 *ua->argk[i] = 0; /* zap keyword already seen */
157 switch (do_keyword_prompt(ua, _("Choose item to purge"), keywords)) {
159 client = get_client_resource(ua);
161 purge_files_from_client(ua, client);
165 client = get_client_resource(ua);
167 purge_jobs_from_client(ua, client);
171 if (select_media_dbr(ua, &mr)) {
172 purge_jobs_from_volume(ua, &mr, /*force*/true);
180 * Purge File records from the database. For any Job which
181 * is older than the retention period, we unconditionally delete
182 * all File records for that Job. This is simple enough that no
183 * temporary tables are needed. We simply make an in memory list of
184 * the JobIds meeting the prune conditions, then delete all File records
185 * pointing to each of those JobIds.
187 static int purge_files_from_client(UAContext *ua, CLIENT *client)
190 POOL_MEM query(PM_MESSAGE);
194 memset(&cr, 0, sizeof(cr));
195 bstrncpy(cr.Name, client->name(), sizeof(cr.Name));
196 if (!db_create_client_record(ua->jcr, ua->db, &cr)) {
200 memset(&del, 0, sizeof(del));
202 del.JobId = (JobId_t *)malloc(sizeof(JobId_t) * del.max_ids);
204 ua->info_msg(_("Begin purging files for Client \"%s\"\n"), cr.Name);
206 Mmsg(query, select_jobsfiles_from_client, edit_int64(cr.ClientId, ed1));
207 Dmsg1(050, "select sql=%s\n", query.c_str());
208 db_sql_query(ua->db, query.c_str(), file_delete_handler, (void *)&del);
210 purge_files_from_job_list(ua, del);
212 if (del.num_del == 0) {
213 ua->warning_msg(_("No Files found for client %s to purge from %s catalog.\n"),
214 client->name(), client->catalog->name());
216 ua->info_msg(_("Files for %d Jobs for client \"%s\" purged from %s catalog.\n"), del.num_del,
217 client->name(), client->catalog->name());
229 * Purge Job records from the database. For any Job which
230 * is older than the retention period, we unconditionally delete
231 * it and all File records for that Job. This is simple enough that no
232 * temporary tables are needed. We simply make an in memory list of
233 * the JobIds then delete the Job, Files, and JobMedia records in that list.
235 static int purge_jobs_from_client(UAContext *ua, CLIENT *client)
238 POOL_MEM query(PM_MESSAGE);
242 memset(&cr, 0, sizeof(cr));
244 bstrncpy(cr.Name, client->name(), sizeof(cr.Name));
245 if (!db_create_client_record(ua->jcr, ua->db, &cr)) {
249 memset(&del, 0, sizeof(del));
251 del.JobId = (JobId_t *)malloc(sizeof(JobId_t) * del.max_ids);
252 del.PurgedFiles = (char *)malloc(del.max_ids);
254 ua->info_msg(_("Begin purging jobs from Client \"%s\"\n"), cr.Name);
256 Mmsg(query, select_jobs_from_client, edit_int64(cr.ClientId, ed1));
257 Dmsg1(150, "select sql=%s\n", query.c_str());
258 db_sql_query(ua->db, query.c_str(), job_delete_handler, (void *)&del);
260 purge_job_list_from_catalog(ua, del);
262 if (del.num_del == 0) {
263 ua->warning_msg(_("No Jobs found for client %s to purge from %s catalog.\n"),
264 client->name(), client->catalog->name());
266 ua->info_msg(_("%d Jobs for client %s purged from %s catalog.\n"), del.num_del,
267 client->name(), client->catalog->name());
273 if (del.PurgedFiles) {
274 free(del.PurgedFiles);
281 * Remove File records from a list of JobIds
283 void purge_files_from_jobs(UAContext *ua, char *jobs)
285 POOL_MEM query(PM_MESSAGE);
287 Mmsg(query, "DELETE FROM File WHERE JobId IN (%s)", jobs);
288 db_sql_query(ua->db, query.c_str(), NULL, (void *)NULL);
289 Dmsg1(050, "Delete File sql=%s\n", query.c_str());
291 Mmsg(query, "DELETE FROM BaseFiles WHERE JobId IN (%s)", jobs);
292 db_sql_query(ua->db, query.c_str(), NULL, (void *)NULL);
293 Dmsg1(050, "Delete BaseFiles sql=%s\n", query.c_str());
295 Mmsg(query, "DELETE FROM PathVisibility WHERE JobId IN (%s)", jobs);
296 db_sql_query(ua->db, query.c_str(), NULL, (void *)NULL);
297 Dmsg1(050, "Delete PathVisibility sql=%s\n", query.c_str());
300 * Now mark Job as having files purged. This is necessary to
301 * avoid having too many Jobs to process in future prunings. If
302 * we don't do this, the number of JobId's in our in memory list
303 * could grow very large.
305 Mmsg(query, "UPDATE Job SET PurgedFiles=1 WHERE JobId IN (%s)", jobs);
306 db_sql_query(ua->db, query.c_str(), NULL, (void *)NULL);
307 Dmsg1(050, "Mark purged sql=%s\n", query.c_str());
311 * Delete jobs (all records) from the catalog in groups of 1000
314 void purge_job_list_from_catalog(UAContext *ua, del_ctx &del)
316 POOL_MEM jobids(PM_MESSAGE);
319 for (int i=0; del.num_ids; ) {
320 Dmsg1(150, "num_ids=%d\n", del.num_ids);
321 pm_strcat(jobids, "");
322 for (int j=0; j<1000 && del.num_ids>0; j++) {
324 if (del.JobId[i] == 0 || ua->jcr->JobId == del.JobId[i]) {
325 Dmsg2(150, "skip JobId[%d]=%d\n", i, (int)del.JobId[i]);
329 if (*jobids.c_str() != 0) {
330 pm_strcat(jobids, ",");
332 pm_strcat(jobids, edit_int64(del.JobId[i++], ed1));
333 Dmsg1(150, "Add id=%s\n", ed1);
336 Dmsg1(150, "num_ids=%d\n", del.num_ids);
337 purge_jobs_from_catalog(ua, jobids.c_str());
342 * Delete files from a list of jobs in groups of 1000
345 void purge_files_from_job_list(UAContext *ua, del_ctx &del)
347 POOL_MEM jobids(PM_MESSAGE);
350 * OK, now we have the list of JobId's to be pruned, send them
351 * off to be deleted batched 1000 at a time.
353 for (int i=0; del.num_ids; ) {
354 pm_strcat(jobids, "");
355 for (int j=0; j<1000 && del.num_ids>0; j++) {
357 if (del.JobId[i] == 0 || ua->jcr->JobId == del.JobId[i]) {
358 Dmsg2(150, "skip JobId[%d]=%d\n", i, (int)del.JobId[i]);
362 if (*jobids.c_str() != 0) {
363 pm_strcat(jobids, ",");
365 pm_strcat(jobids, edit_int64(del.JobId[i++], ed1));
366 Dmsg1(150, "Add id=%s\n", ed1);
369 purge_files_from_jobs(ua, jobids.c_str());
374 * Change the type of the next copy job to backup.
375 * We need to upgrade the next copy of a normal job,
376 * and also upgrade the next copy when the normal job
377 * already have been purged.
379 * JobId: 1 PriorJobId: 0 (original)
380 * JobId: 2 PriorJobId: 1 (first copy)
381 * JobId: 3 PriorJobId: 1 (second copy)
383 * JobId: 2 PriorJobId: 1 (first copy, now regular backup)
384 * JobId: 3 PriorJobId: 1 (second copy)
386 * => Search through PriorJobId in jobid and
387 * PriorJobId in PriorJobId (jobid)
389 void upgrade_copies(UAContext *ua, char *jobs)
391 POOL_MEM query(PM_MESSAGE);
392 int dbtype = ua->db->bdb_get_type_index();
396 Mmsg(query, uap_upgrade_copies_oldest_job[dbtype], JT_JOB_COPY, jobs, jobs);
397 db_sql_query(ua->db, query.c_str(), NULL, (void *)NULL);
398 Dmsg1(050, "Upgrade copies Log sql=%s\n", query.c_str());
400 /* Now upgrade first copy to Backup */
401 Mmsg(query, "UPDATE Job SET Type='B' " /* JT_JOB_COPY => JT_BACKUP */
402 "WHERE JobId IN ( SELECT JobId FROM cpy_tmp )");
404 db_sql_query(ua->db, query.c_str(), NULL, (void *)NULL);
406 Mmsg(query, "DROP TABLE cpy_tmp");
407 db_sql_query(ua->db, query.c_str(), NULL, (void *)NULL);
413 * Remove all records from catalog for a list of JobIds
415 void purge_jobs_from_catalog(UAContext *ua, char *jobs)
417 POOL_MEM query(PM_MESSAGE);
419 /* Delete (or purge) records associated with the job */
420 purge_files_from_jobs(ua, jobs);
422 Mmsg(query, "DELETE FROM JobMedia WHERE JobId IN (%s)", jobs);
423 db_sql_query(ua->db, query.c_str(), NULL, (void *)NULL);
424 Dmsg1(050, "Delete JobMedia sql=%s\n", query.c_str());
426 Mmsg(query, "DELETE FROM Log WHERE JobId IN (%s)", jobs);
427 db_sql_query(ua->db, query.c_str(), NULL, (void *)NULL);
428 Dmsg1(050, "Delete Log sql=%s\n", query.c_str());
430 Mmsg(query, "DELETE FROM RestoreObject WHERE JobId IN (%s)", jobs);
431 db_sql_query(ua->db, query.c_str(), NULL, (void *)NULL);
432 Dmsg1(050, "Delete RestoreObject sql=%s\n", query.c_str());
434 /* The JobId of the Snapshot record is no longer usable
435 * TODO: Migth want to use a copy for the jobid?
437 Mmsg(query, "UPDATE Snapshot SET JobId=0 WHERE JobId IN (%s)", jobs);
438 db_sql_query(ua->db, query.c_str(), NULL, (void *)NULL);
440 upgrade_copies(ua, jobs);
442 /* Now remove the Job record itself */
443 Mmsg(query, "DELETE FROM Job WHERE JobId IN (%s)", jobs);
444 db_sql_query(ua->db, query.c_str(), NULL, (void *)NULL);
446 Dmsg1(050, "Delete Job sql=%s\n", query.c_str());
449 void purge_files_from_volume(UAContext *ua, MEDIA_DBR *mr )
450 {} /* ***FIXME*** implement */
453 * Returns: 1 if Volume purged
454 * 0 if Volume not purged
456 bool purge_jobs_from_volume(UAContext *ua, MEDIA_DBR *mr, bool force)
458 POOL_MEM query(PM_MESSAGE);
465 stat = strcmp(mr->VolStatus, "Append") == 0 ||
466 strcmp(mr->VolStatus, "Full") == 0 ||
467 strcmp(mr->VolStatus, "Used") == 0 ||
468 strcmp(mr->VolStatus, "Error") == 0;
470 ua->error_msg(_("\nVolume \"%s\" has VolStatus \"%s\" and cannot be purged.\n"
471 "The VolStatus must be: Append, Full, Used, or Error to be purged.\n"),
472 mr->VolumeName, mr->VolStatus);
477 * Check if he wants to purge a single jobid
479 i = find_arg_with_value(ua, "jobid");
480 if (i >= 0 && is_a_number_list(ua->argv[i])) {
481 jobids = ua->argv[i];
486 if (!db_get_volume_jobids(ua->jcr, ua->db, mr, &lst)) {
487 ua->error_msg("%s", db_strerror(ua->db));
488 Dmsg0(050, "Count failed\n");
495 purge_jobs_from_catalog(ua, jobids);
498 ua->info_msg(_("%d File%s on Volume \"%s\" purged from catalog.\n"),
499 lst.count, lst.count<=1?"":"s", mr->VolumeName);
501 purged = is_volume_purged(ua, mr, force);
508 * This routine will check the JobMedia records to see if the
509 * Volume has been purged. If so, it marks it as such and
511 * Returns: true if volume purged
514 * Note, we normally will not purge a volume that has Firstor LastWritten
515 * zero, because it means the volume is most likely being written
516 * however, if the user manually purges using the purge command in
517 * the console, he has been warned, and we go ahead and purge
518 * the volume anyway, if possible).
520 bool is_volume_purged(UAContext *ua, MEDIA_DBR *mr, bool force)
522 POOL_MEM query(PM_MESSAGE);
523 struct s_count_ctx cnt;
527 if (!force && (mr->FirstWritten == 0 || mr->LastWritten == 0)) {
528 goto bail_out; /* not written cannot purge */
531 if (strcmp(mr->VolStatus, "Purged") == 0) {
532 Dmsg1(100, "Volume=%s already purged.\n", mr->VolumeName);
537 /* If purged, mark it so */
539 Mmsg(query, "SELECT 1 FROM JobMedia WHERE MediaId=%s LIMIT 1",
540 edit_int64(mr->MediaId, ed1));
541 if (!db_sql_query(ua->db, query.c_str(), del_count_handler, (void *)&cnt)) {
542 ua->error_msg("%s", db_strerror(ua->db));
543 Dmsg0(050, "Count failed\n");
547 if (cnt.count == 0) {
548 ua->warning_msg(_("There are no more Jobs associated with Volume \"%s\". Marking it purged.\n"),
550 Dmsg1(100, "There are no more Jobs associated with Volume \"%s\". Marking it purged.\n",
552 if (!(purged = mark_media_purged(ua, mr))) {
553 ua->error_msg("%s", db_strerror(ua->db));
561 * Called here to send the appropriate commands to the SD
562 * to do truncate on purge.
564 static void do_truncate_on_purge(UAContext *ua, MEDIA_DBR *mr,
565 char *pool, char *storage,
566 int drive, BSOCK *sd)
569 uint64_t VolBytes = 0;
570 uint64_t VolABytes = 0;
571 uint32_t VolType = 0;
573 /* TODO: Return if not mr->Recyle ? */
578 /* Do it only if action on purge = truncate is set */
579 if (!(mr->ActionOnPurge & ON_PURGE_TRUNCATE)) {
580 ua->error_msg(_("\nThe option \"Action On Purge = Truncate\" was not defined in the Pool resource.\n"
581 "Unable to truncate volume \"%s\"\n"), mr->VolumeName);
585 * Send the command to truncate the volume after purge. If this feature
586 * is disabled for the specific device, this will be a no-op.
589 /* Protect us from spaces */
590 bash_spaces(mr->VolumeName);
591 bash_spaces(mr->MediaType);
593 bash_spaces(storage);
595 /* Do it by relabeling the Volume, which truncates it */
596 sd->fsend("relabel %s OldName=%s NewName=%s PoolName=%s "
597 "MediaType=%s Slot=%d drive=%d\n",
599 mr->VolumeName, mr->VolumeName,
600 pool, mr->MediaType, mr->Slot, drive);
602 unbash_spaces(mr->VolumeName);
603 unbash_spaces(mr->MediaType);
605 unbash_spaces(storage);
607 /* Send relabel command, and check for valid response */
608 while (sd->recv() >= 0) {
609 ua->send_msg("%s", sd->msg);
610 if (sscanf(sd->msg, "3000 OK label. VolBytes=%llu VolABytes=%lld VolType=%d ",
611 &VolBytes, &VolABytes, &VolType) == 3) {
617 mr->VolBytes = VolBytes;
618 mr->VolABytes = VolABytes;
619 mr->VolType = VolType;
621 set_storageid_in_mr(NULL, mr);
622 if (!db_update_media_record(ua->jcr, ua->db, mr)) {
623 ua->error_msg(_("Can't update volume size in the catalog\n"));
625 ua->send_msg(_("The volume \"%s\" has been truncated\n"), mr->VolumeName);
627 ua->warning_msg(_("Unable to truncate volume \"%s\"\n"), mr->VolumeName);
632 * Implement Bacula bconsole command purge action
633 * purge action=truncate pool= volume= storage= mediatype=
635 * truncate pool= volume= storage= mediatype=
637 * Note, later we might want to rename this action_on_purge_cmd() as
638 * was the original, but only if we add additional actions such as
639 * erase, ... For the moment, we only do a truncate.
642 int truncate_cmd(UAContext *ua, const char *cmd)
644 bool allpools = false;
647 uint32_t *results = NULL;
648 const char *action = "truncate";
655 memset(&pr, 0, sizeof(pr));
657 /* Look at arguments */
658 for (int i=1; i<ua->argc; i++) {
659 if (strcasecmp(ua->argk[i], NT_("allpools")) == 0) {
662 } else if (strcasecmp(ua->argk[i], NT_("volume")) == 0
663 && is_name_valid(ua->argv[i], NULL)) {
664 bstrncpy(mr.VolumeName, ua->argv[i], sizeof(mr.VolumeName));
666 } else if (strcasecmp(ua->argk[i], NT_("mediatype")) == 0
668 bstrncpy(mr.MediaType, ua->argv[i], sizeof(mr.MediaType));
670 } else if (strcasecmp(ua->argk[i], NT_("drive")) == 0 && ua->argv[i]) {
671 drive = atoi(ua->argv[i]);
673 } else if (strcasecmp(ua->argk[i], NT_("action")) == 0
674 && is_name_valid(ua->argv[i], NULL)) {
675 action = ua->argv[i];
680 ua->jcr->wstore = store = get_storage_resource(ua, false);
686 Dmsg0(100, "Can't open db\n");
691 /* force pool selection */
692 pool = get_pool_resource(ua);
694 Dmsg0(100, "Can't get pool resource\n");
697 bstrncpy(pr.Name, pool->name(), sizeof(pr.Name));
698 if (!db_get_pool_record(ua->jcr, ua->db, &pr)) {
699 Dmsg0(100, "Can't get pool record\n");
702 mr.PoolId = pr.PoolId;
706 * Look for all Purged volumes that can be recycled, are enabled and
707 * have more the 10,000 bytes.
712 set_storageid_in_mr(store, &mr);
713 bstrncpy(mr.VolStatus, "Purged", sizeof(mr.VolStatus));
714 if (!db_get_media_ids(ua->jcr, ua->db, &mr, &nb, &results)) {
715 Dmsg0(100, "No results from db_get_media_ids\n");
720 ua->send_msg(_("No Volumes found to perform \"truncate\" command.\n"));
724 if ((sd=open_sd_bsock(ua)) == NULL) {
725 Dmsg0(100, "Can't open connection to sd\n");
730 * Loop over the candidate Volumes and actually truncate them
732 for (int i=0; i < nb; i++) {
734 mr.MediaId = results[i];
735 if (db_get_media_record(ua->jcr, ua->db, &mr)) {
736 /* TODO: ask for drive and change Pool */
737 if (strcasecmp("truncate", action) == 0) {
738 do_truncate_on_purge(ua, &mr, pr.Name, store->dev_name(), drive, sd);
741 Dmsg1(0, "Can't find MediaId=%lld\n", (uint64_t)mr.MediaId);
748 ua->jcr->wstore = NULL;
757 * IF volume status is Append, Full, Used, or Error, mark it Purged
758 * Purged volumes can then be recycled (if enabled).
760 bool mark_media_purged(UAContext *ua, MEDIA_DBR *mr)
763 if (strcmp(mr->VolStatus, "Append") == 0 ||
764 strcmp(mr->VolStatus, "Full") == 0 ||
765 strcmp(mr->VolStatus, "Used") == 0 ||
766 strcmp(mr->VolStatus, "Error") == 0) {
767 bstrncpy(mr->VolStatus, "Purged", sizeof(mr->VolStatus));
768 set_storageid_in_mr(NULL, mr);
769 if (!db_update_media_record(jcr, ua->db, mr)) {
772 pm_strcpy(jcr->VolumeName, mr->VolumeName);
773 generate_plugin_event(jcr, bDirEventVolumePurged);
775 * If the RecyclePool is defined, move the volume there
777 if (mr->RecyclePoolId && mr->RecyclePoolId != mr->PoolId) {
778 POOL_DBR oldpr, newpr;
779 memset(&oldpr, 0, sizeof(POOL_DBR));
780 memset(&newpr, 0, sizeof(POOL_DBR));
781 newpr.PoolId = mr->RecyclePoolId;
782 oldpr.PoolId = mr->PoolId;
783 if ( db_get_pool_numvols(jcr, ua->db, &oldpr)
784 && db_get_pool_numvols(jcr, ua->db, &newpr)) {
785 /* check if destination pool size is ok */
786 if (newpr.MaxVols > 0 && newpr.NumVols >= newpr.MaxVols) {
787 ua->error_msg(_("Unable move recycled Volume in full "
788 "Pool \"%s\" MaxVols=%d\n"),
789 newpr.Name, newpr.MaxVols);
791 } else { /* move media */
792 update_vol_pool(ua, newpr.Name, mr, &oldpr);
795 ua->error_msg("%s", db_strerror(ua->db));
799 /* Send message to Job report, if it is a *real* job */
800 if (jcr && jcr->JobId > 0) {
801 Jmsg(jcr, M_INFO, 0, _("All records pruned from Volume \"%s\"; marking it \"Purged\"\n"),
806 ua->error_msg(_("Cannot purge Volume with VolStatus=%s\n"), mr->VolStatus);
808 return strcmp(mr->VolStatus, "Purged") == 0;