2 Bacula(R) - The Network Backup Solution
4 Copyright (C) 2000-2015 Kern Sibbald
5 Copyright (C) 2002-2014 Free Software Foundation Europe e.V.
7 The original author of Bacula is Kern Sibbald, with contributions
8 from many others, a complete list can be found in the file AUTHORS.
10 You may use this file and others of this release according to the
11 license defined in the LICENSE file, which includes the Affero General
12 Public License, v3.0 ("AGPLv3") and some additional permissions and
13 terms pursuant to its AGPLv3 Section 7.
15 This notice must be preserved when any source code is
16 conveyed and/or propagated.
18 Bacula(R) is a registered trademark of Kern Sibbald.
22 * Bacula Director -- User Agent Database Purge Command
24 * Purges Files from specific JobIds
26 * Purges Jobs from Volumes
28 * Kern Sibbald, February MMII
35 /* Forward referenced functions */
36 static int purge_files_from_client(UAContext *ua, CLIENT *client);
37 static int purge_jobs_from_client(UAContext *ua, CLIENT *client);
38 int truncate_cmd(UAContext *ua, const char *cmd);
40 static const char *select_jobsfiles_from_client =
41 "SELECT JobId FROM Job "
45 static const char *select_jobs_from_client =
46 "SELECT JobId, PurgedFiles FROM Job "
50 * Purge records from database
52 * Purge Files (from) [Job|JobId|Client|Volume]
53 * Purge Jobs (from) [Client|Volume]
56 * N.B. Not all above is implemented yet.
58 int purge_cmd(UAContext *ua, const char *cmd)
64 static const char *keywords[] = {
70 static const char *files_keywords[] = {
77 static const char *jobs_keywords[] = {
82 /* Special case for the "Action On Purge", this option is working only on
83 * Purged volume, so no jobs or files will be purged.
84 * We are skiping this message if "purge volume action=xxx"
86 if (!(find_arg(ua, "volume") >= 0 && find_arg(ua, "action") >= 0)) {
88 "\nThis command can be DANGEROUS!!!\n\n"
89 "It purges (deletes) all Files from a Job,\n"
90 "JobId, Client or Volume; or it purges (deletes)\n"
91 "all Jobs from a Client or Volume without regard\n"
92 "to retention periods. Normally you should use the\n"
93 "PRUNE command, which respects retention periods.\n"));
96 if (!open_new_client_db(ua)) {
99 switch (find_arg_keyword(ua, keywords)) {
102 switch(find_arg_keyword(ua, files_keywords)) {
105 if (get_job_dbr(ua, &jr)) {
107 edit_int64(jr.JobId, jobid);
108 purge_files_from_jobs(ua, jobid);
112 client = get_client_resource(ua);
114 purge_files_from_client(ua, client);
118 if (select_media_dbr(ua, &mr)) {
119 purge_files_from_volume(ua, &mr);
125 switch(find_arg_keyword(ua, jobs_keywords)) {
127 client = get_client_resource(ua);
129 purge_jobs_from_client(ua, client);
133 if (select_media_dbr(ua, &mr)) {
134 purge_jobs_from_volume(ua, &mr, /*force*/true);
140 /* Perform ActionOnPurge (action=truncate) */
141 if (find_arg(ua, "action") >= 0) {
142 return truncate_cmd(ua, ua->cmd);
145 while ((i=find_arg(ua, NT_("volume"))) >= 0) {
146 if (select_media_dbr(ua, &mr)) {
147 purge_jobs_from_volume(ua, &mr, /*force*/true);
149 *ua->argk[i] = 0; /* zap keyword already seen */
156 switch (do_keyword_prompt(ua, _("Choose item to purge"), keywords)) {
158 client = get_client_resource(ua);
160 purge_files_from_client(ua, client);
164 client = get_client_resource(ua);
166 purge_jobs_from_client(ua, client);
170 if (select_media_dbr(ua, &mr)) {
171 purge_jobs_from_volume(ua, &mr, /*force*/true);
179 * Purge File records from the database. For any Job which
180 * is older than the retention period, we unconditionally delete
181 * all File records for that Job. This is simple enough that no
182 * temporary tables are needed. We simply make an in memory list of
183 * the JobIds meeting the prune conditions, then delete all File records
184 * pointing to each of those JobIds.
186 static int purge_files_from_client(UAContext *ua, CLIENT *client)
189 POOL_MEM query(PM_MESSAGE);
193 memset(&cr, 0, sizeof(cr));
194 bstrncpy(cr.Name, client->name(), sizeof(cr.Name));
195 if (!db_create_client_record(ua->jcr, ua->db, &cr)) {
199 memset(&del, 0, sizeof(del));
201 del.JobId = (JobId_t *)malloc(sizeof(JobId_t) * del.max_ids);
203 ua->info_msg(_("Begin purging files for Client \"%s\"\n"), cr.Name);
205 Mmsg(query, select_jobsfiles_from_client, edit_int64(cr.ClientId, ed1));
206 Dmsg1(050, "select sql=%s\n", query.c_str());
207 db_sql_query(ua->db, query.c_str(), file_delete_handler, (void *)&del);
209 purge_files_from_job_list(ua, del);
211 if (del.num_ids == 0) {
212 ua->warning_msg(_("No Files found for client %s to purge from %s catalog.\n"),
213 client->name(), client->catalog->name());
215 ua->info_msg(_("Files for %d Jobs for client \"%s\" purged from %s catalog.\n"), del.num_ids,
216 client->name(), client->catalog->name());
228 * Purge Job records from the database. For any Job which
229 * is older than the retention period, we unconditionally delete
230 * it and 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 then delete the Job, Files, and JobMedia records in that list.
234 static int purge_jobs_from_client(UAContext *ua, CLIENT *client)
237 POOL_MEM query(PM_MESSAGE);
241 memset(&cr, 0, sizeof(cr));
243 bstrncpy(cr.Name, client->name(), sizeof(cr.Name));
244 if (!db_create_client_record(ua->jcr, ua->db, &cr)) {
248 memset(&del, 0, sizeof(del));
250 del.JobId = (JobId_t *)malloc(sizeof(JobId_t) * del.max_ids);
251 del.PurgedFiles = (char *)malloc(del.max_ids);
253 ua->info_msg(_("Begin purging jobs from Client \"%s\"\n"), cr.Name);
255 Mmsg(query, select_jobs_from_client, edit_int64(cr.ClientId, ed1));
256 Dmsg1(150, "select sql=%s\n", query.c_str());
257 db_sql_query(ua->db, query.c_str(), job_delete_handler, (void *)&del);
259 purge_job_list_from_catalog(ua, del);
261 if (del.num_ids == 0) {
262 ua->warning_msg(_("No Files found for client %s to purge from %s catalog.\n"),
263 client->name(), client->catalog->name());
265 ua->info_msg(_("%d Jobs for client %s purged from %s catalog.\n"), del.num_ids,
266 client->name(), client->catalog->name());
272 if (del.PurgedFiles) {
273 free(del.PurgedFiles);
280 * Remove File records from a list of JobIds
282 void purge_files_from_jobs(UAContext *ua, char *jobs)
284 POOL_MEM query(PM_MESSAGE);
286 Mmsg(query, "DELETE FROM File WHERE JobId IN (%s)", jobs);
287 db_sql_query(ua->db, query.c_str(), NULL, (void *)NULL);
288 Dmsg1(050, "Delete File sql=%s\n", query.c_str());
290 Mmsg(query, "DELETE FROM BaseFiles WHERE JobId IN (%s)", jobs);
291 db_sql_query(ua->db, query.c_str(), NULL, (void *)NULL);
292 Dmsg1(050, "Delete BaseFiles sql=%s\n", query.c_str());
294 Mmsg(query, "DELETE FROM PathVisibility WHERE JobId IN (%s)", jobs);
295 db_sql_query(ua->db, query.c_str(), NULL, (void *)NULL);
296 Dmsg1(050, "Delete PathVisibility sql=%s\n", query.c_str());
299 * Now mark Job as having files purged. This is necessary to
300 * avoid having too many Jobs to process in future prunings. If
301 * we don't do this, the number of JobId's in our in memory list
302 * could grow very large.
304 Mmsg(query, "UPDATE Job SET PurgedFiles=1 WHERE JobId IN (%s)", jobs);
305 db_sql_query(ua->db, query.c_str(), NULL, (void *)NULL);
306 Dmsg1(050, "Mark purged sql=%s\n", query.c_str());
310 * Delete jobs (all records) from the catalog in groups of 1000
313 void purge_job_list_from_catalog(UAContext *ua, del_ctx &del)
315 POOL_MEM jobids(PM_MESSAGE);
318 for (int i=0; del.num_ids; ) {
319 Dmsg1(150, "num_ids=%d\n", del.num_ids);
320 pm_strcat(jobids, "");
321 for (int j=0; j<1000 && del.num_ids>0; j++) {
323 if (del.JobId[i] == 0 || ua->jcr->JobId == del.JobId[i]) {
324 Dmsg2(150, "skip JobId[%d]=%d\n", i, (int)del.JobId[i]);
328 if (*jobids.c_str() != 0) {
329 pm_strcat(jobids, ",");
331 pm_strcat(jobids, edit_int64(del.JobId[i++], ed1));
332 Dmsg1(150, "Add id=%s\n", ed1);
335 Dmsg1(150, "num_ids=%d\n", del.num_ids);
336 purge_jobs_from_catalog(ua, jobids.c_str());
341 * Delete files from a list of jobs in groups of 1000
344 void purge_files_from_job_list(UAContext *ua, del_ctx &del)
346 POOL_MEM jobids(PM_MESSAGE);
349 * OK, now we have the list of JobId's to be pruned, send them
350 * off to be deleted batched 1000 at a time.
352 for (int i=0; del.num_ids; ) {
353 pm_strcat(jobids, "");
354 for (int j=0; j<1000 && del.num_ids>0; j++) {
356 if (del.JobId[i] == 0 || ua->jcr->JobId == del.JobId[i]) {
357 Dmsg2(150, "skip JobId[%d]=%d\n", i, (int)del.JobId[i]);
361 if (*jobids.c_str() != 0) {
362 pm_strcat(jobids, ",");
364 pm_strcat(jobids, edit_int64(del.JobId[i++], ed1));
365 Dmsg1(150, "Add id=%s\n", ed1);
368 purge_files_from_jobs(ua, jobids.c_str());
373 * Change the type of the next copy job to backup.
374 * We need to upgrade the next copy of a normal job,
375 * and also upgrade the next copy when the normal job
376 * already have been purged.
378 * JobId: 1 PriorJobId: 0 (original)
379 * JobId: 2 PriorJobId: 1 (first copy)
380 * JobId: 3 PriorJobId: 1 (second copy)
382 * JobId: 2 PriorJobId: 1 (first copy, now regular backup)
383 * JobId: 3 PriorJobId: 1 (second copy)
385 * => Search through PriorJobId in jobid and
386 * PriorJobId in PriorJobId (jobid)
388 void upgrade_copies(UAContext *ua, char *jobs)
390 POOL_MEM query(PM_MESSAGE);
391 int dbtype = ua->db->bdb_get_type_index();
395 Mmsg(query, uap_upgrade_copies_oldest_job[dbtype], JT_JOB_COPY, jobs, jobs);
396 db_sql_query(ua->db, query.c_str(), NULL, (void *)NULL);
397 Dmsg1(050, "Upgrade copies Log sql=%s\n", query.c_str());
399 /* Now upgrade first copy to Backup */
400 Mmsg(query, "UPDATE Job SET Type='B' " /* JT_JOB_COPY => JT_BACKUP */
401 "WHERE JobId IN ( SELECT JobId FROM cpy_tmp )");
403 db_sql_query(ua->db, query.c_str(), NULL, (void *)NULL);
405 Mmsg(query, "DROP TABLE cpy_tmp");
406 db_sql_query(ua->db, query.c_str(), NULL, (void *)NULL);
412 * Remove all records from catalog for a list of JobIds
414 void purge_jobs_from_catalog(UAContext *ua, char *jobs)
416 POOL_MEM query(PM_MESSAGE);
418 /* Delete (or purge) records associated with the job */
419 purge_files_from_jobs(ua, jobs);
421 Mmsg(query, "DELETE FROM JobMedia WHERE JobId IN (%s)", jobs);
422 db_sql_query(ua->db, query.c_str(), NULL, (void *)NULL);
423 Dmsg1(050, "Delete JobMedia sql=%s\n", query.c_str());
425 Mmsg(query, "DELETE FROM Log WHERE JobId IN (%s)", jobs);
426 db_sql_query(ua->db, query.c_str(), NULL, (void *)NULL);
427 Dmsg1(050, "Delete Log sql=%s\n", query.c_str());
429 Mmsg(query, "DELETE FROM RestoreObject WHERE JobId IN (%s)", jobs);
430 db_sql_query(ua->db, query.c_str(), NULL, (void *)NULL);
431 Dmsg1(050, "Delete RestoreObject sql=%s\n", query.c_str());
433 /* The JobId of the Snapshot record is no longer usable
434 * TODO: Migth want to use a copy for the jobid?
436 Mmsg(query, "UPDATE Snapshot SET JobId=0 WHERE JobId IN (%s)", jobs);
437 db_sql_query(ua->db, query.c_str(), NULL, (void *)NULL);
439 upgrade_copies(ua, jobs);
441 /* Now remove the Job record itself */
442 Mmsg(query, "DELETE FROM Job WHERE JobId IN (%s)", jobs);
443 db_sql_query(ua->db, query.c_str(), NULL, (void *)NULL);
445 Dmsg1(050, "Delete Job sql=%s\n", query.c_str());
448 void purge_files_from_volume(UAContext *ua, MEDIA_DBR *mr )
449 {} /* ***FIXME*** implement */
452 * Returns: 1 if Volume purged
453 * 0 if Volume not purged
455 bool purge_jobs_from_volume(UAContext *ua, MEDIA_DBR *mr, bool force)
457 POOL_MEM query(PM_MESSAGE);
464 stat = strcmp(mr->VolStatus, "Append") == 0 ||
465 strcmp(mr->VolStatus, "Full") == 0 ||
466 strcmp(mr->VolStatus, "Used") == 0 ||
467 strcmp(mr->VolStatus, "Error") == 0;
469 ua->error_msg(_("\nVolume \"%s\" has VolStatus \"%s\" and cannot be purged.\n"
470 "The VolStatus must be: Append, Full, Used, or Error to be purged.\n"),
471 mr->VolumeName, mr->VolStatus);
476 * Check if he wants to purge a single jobid
478 i = find_arg_with_value(ua, "jobid");
479 if (i >= 0 && is_a_number_list(ua->argv[i])) {
480 jobids = ua->argv[i];
485 if (!db_get_volume_jobids(ua->jcr, ua->db, mr, &lst)) {
486 ua->error_msg("%s", db_strerror(ua->db));
487 Dmsg0(050, "Count failed\n");
494 purge_jobs_from_catalog(ua, jobids);
497 ua->info_msg(_("%d File%s on Volume \"%s\" purged from catalog.\n"),
498 lst.count, lst.count<=1?"":"s", mr->VolumeName);
500 purged = is_volume_purged(ua, mr, force);
507 * This routine will check the JobMedia records to see if the
508 * Volume has been purged. If so, it marks it as such and
510 * Returns: true if volume purged
513 * Note, we normally will not purge a volume that has Firstor LastWritten
514 * zero, because it means the volume is most likely being written
515 * however, if the user manually purges using the purge command in
516 * the console, he has been warned, and we go ahead and purge
517 * the volume anyway, if possible).
519 bool is_volume_purged(UAContext *ua, MEDIA_DBR *mr, bool force)
521 POOL_MEM query(PM_MESSAGE);
522 struct s_count_ctx cnt;
526 if (!force && (mr->FirstWritten == 0 || mr->LastWritten == 0)) {
527 goto bail_out; /* not written cannot purge */
530 if (strcmp(mr->VolStatus, "Purged") == 0) {
531 Dmsg1(100, "Volume=%s already purged.\n", mr->VolumeName);
536 /* If purged, mark it so */
538 Mmsg(query, "SELECT 1 FROM JobMedia WHERE MediaId=%s LIMIT 1",
539 edit_int64(mr->MediaId, ed1));
540 if (!db_sql_query(ua->db, query.c_str(), del_count_handler, (void *)&cnt)) {
541 ua->error_msg("%s", db_strerror(ua->db));
542 Dmsg0(050, "Count failed\n");
546 if (cnt.count == 0) {
547 ua->warning_msg(_("There are no more Jobs associated with Volume \"%s\". Marking it purged.\n"),
549 Dmsg1(100, "There are no more Jobs associated with Volume \"%s\". Marking it purged.\n",
551 if (!(purged = mark_media_purged(ua, mr))) {
552 ua->error_msg("%s", db_strerror(ua->db));
560 * Called here to send the appropriate commands to the SD
561 * to do truncate on purge.
563 static void do_truncate_on_purge(UAContext *ua, MEDIA_DBR *mr,
564 char *pool, char *storage,
565 int drive, BSOCK *sd)
568 uint64_t VolBytes = 0;
569 uint64_t VolABytes = 0;
570 uint32_t VolType = 0;
572 /* TODO: Return if not mr->Recyle ? */
577 /* Do it only if action on purge = truncate is set */
578 if (!(mr->ActionOnPurge & ON_PURGE_TRUNCATE)) {
582 * Send the command to truncate the volume after purge. If this feature
583 * is disabled for the specific device, this will be a no-op.
586 /* Protect us from spaces */
587 bash_spaces(mr->VolumeName);
588 bash_spaces(mr->MediaType);
590 bash_spaces(storage);
592 /* Do it by relabeling the Volume, which truncates it */
593 sd->fsend("relabel %s OldName=%s NewName=%s PoolName=%s "
594 "MediaType=%s Slot=%d drive=%d\n",
596 mr->VolumeName, mr->VolumeName,
597 pool, mr->MediaType, mr->Slot, drive);
599 unbash_spaces(mr->VolumeName);
600 unbash_spaces(mr->MediaType);
602 unbash_spaces(storage);
604 /* Send relabel command, and check for valid response */
605 while (sd->recv() >= 0) {
606 ua->send_msg("%s", sd->msg);
607 if (sscanf(sd->msg, "3000 OK label. VolBytes=%llu VolABytes=%lld VolType=%d ",
608 &VolBytes, &VolABytes, &VolType) == 3) {
614 mr->VolBytes = VolBytes;
615 mr->VolABytes = VolABytes;
616 mr->VolType = VolType;
618 set_storageid_in_mr(NULL, mr);
619 if (!db_update_media_record(ua->jcr, ua->db, mr)) {
620 ua->error_msg(_("Can't update volume size in the catalog\n"));
622 ua->send_msg(_("The volume \"%s\" has been truncated\n"), mr->VolumeName);
624 ua->warning_msg(_("Unable to truncate volume \"%s\"\n"), mr->VolumeName);
629 * Implement Bacula bconsole command purge action
630 * purge action=truncate pool= volume= storage= mediatype=
632 * truncate pool= volume= storage= mediatype=
634 * Note, later we might want to rename this action_on_purge_cmd() as
635 * was the original, but only if we add additional actions such as
636 * erase, ... For the moment, we only do a truncate.
639 int truncate_cmd(UAContext *ua, const char *cmd)
641 bool allpools = false;
644 uint32_t *results = NULL;
645 const char *action = "truncate";
652 memset(&pr, 0, sizeof(pr));
654 /* Look at arguments */
655 for (int i=1; i<ua->argc; i++) {
656 if (strcasecmp(ua->argk[i], NT_("allpools")) == 0) {
659 } else if (strcasecmp(ua->argk[i], NT_("volume")) == 0
660 && is_name_valid(ua->argv[i], NULL)) {
661 bstrncpy(mr.VolumeName, ua->argv[i], sizeof(mr.VolumeName));
663 } else if (strcasecmp(ua->argk[i], NT_("mediatype")) == 0
665 bstrncpy(mr.MediaType, ua->argv[i], sizeof(mr.MediaType));
667 } else if (strcasecmp(ua->argk[i], NT_("drive")) == 0 && ua->argv[i]) {
668 drive = atoi(ua->argv[i]);
670 } else if (strcasecmp(ua->argk[i], NT_("action")) == 0
671 && is_name_valid(ua->argv[i], NULL)) {
672 action = ua->argv[i];
677 ua->jcr->wstore = store = get_storage_resource(ua, false);
683 Dmsg0(100, "Can't open db\n");
688 /* force pool selection */
689 pool = get_pool_resource(ua);
691 Dmsg0(100, "Can't get pool resource\n");
694 bstrncpy(pr.Name, pool->name(), sizeof(pr.Name));
695 if (!db_get_pool_record(ua->jcr, ua->db, &pr)) {
696 Dmsg0(100, "Can't get pool record\n");
699 mr.PoolId = pr.PoolId;
703 * Look for all Purged volumes that can be recycled, are enabled and
704 * have more the 10,000 bytes.
709 set_storageid_in_mr(store, &mr);
710 bstrncpy(mr.VolStatus, "Purged", sizeof(mr.VolStatus));
711 if (!db_get_media_ids(ua->jcr, ua->db, &mr, &nb, &results)) {
712 Dmsg0(100, "No results from db_get_media_ids\n");
717 ua->send_msg(_("No Volumes found to perform \"truncate\" command.\n"));
721 if ((sd=open_sd_bsock(ua)) == NULL) {
722 Dmsg0(100, "Can't open connection to sd\n");
727 * Loop over the candidate Volumes and actually truncate them
729 for (int i=0; i < nb; i++) {
731 mr.MediaId = results[i];
732 if (db_get_media_record(ua->jcr, ua->db, &mr)) {
733 /* TODO: ask for drive and change Pool */
734 if (strcasecmp("truncate", action) == 0) {
735 do_truncate_on_purge(ua, &mr, pr.Name, store->dev_name(), drive, sd);
738 Dmsg1(0, "Can't find MediaId=%lld\n", (uint64_t)mr.MediaId);
745 ua->jcr->wstore = NULL;
754 * IF volume status is Append, Full, Used, or Error, mark it Purged
755 * Purged volumes can then be recycled (if enabled).
757 bool mark_media_purged(UAContext *ua, MEDIA_DBR *mr)
760 if (strcmp(mr->VolStatus, "Append") == 0 ||
761 strcmp(mr->VolStatus, "Full") == 0 ||
762 strcmp(mr->VolStatus, "Used") == 0 ||
763 strcmp(mr->VolStatus, "Error") == 0) {
764 bstrncpy(mr->VolStatus, "Purged", sizeof(mr->VolStatus));
765 set_storageid_in_mr(NULL, mr);
766 if (!db_update_media_record(jcr, ua->db, mr)) {
769 pm_strcpy(jcr->VolumeName, mr->VolumeName);
770 generate_plugin_event(jcr, bDirEventVolumePurged);
772 * If the RecyclePool is defined, move the volume there
774 if (mr->RecyclePoolId && mr->RecyclePoolId != mr->PoolId) {
775 POOL_DBR oldpr, newpr;
776 memset(&oldpr, 0, sizeof(POOL_DBR));
777 memset(&newpr, 0, sizeof(POOL_DBR));
778 newpr.PoolId = mr->RecyclePoolId;
779 oldpr.PoolId = mr->PoolId;
780 if ( db_get_pool_numvols(jcr, ua->db, &oldpr)
781 && db_get_pool_numvols(jcr, ua->db, &newpr)) {
782 /* check if destination pool size is ok */
783 if (newpr.MaxVols > 0 && newpr.NumVols >= newpr.MaxVols) {
784 ua->error_msg(_("Unable move recycled Volume in full "
785 "Pool \"%s\" MaxVols=%d\n"),
786 newpr.Name, newpr.MaxVols);
788 } else { /* move media */
789 update_vol_pool(ua, newpr.Name, mr, &oldpr);
792 ua->error_msg("%s", db_strerror(ua->db));
796 /* Send message to Job report, if it is a *real* job */
797 if (jcr && jcr->JobId > 0) {
798 Jmsg(jcr, M_INFO, 0, _("All records pruned from Volume \"%s\"; marking it \"Purged\"\n"),
803 ua->error_msg(_("Cannot purge Volume with VolStatus=%s\n"), mr->VolStatus);
805 return strcmp(mr->VolStatus, "Purged") == 0;