2 Bacula® - The Network Backup Solution
4 Copyright (C) 2002-2010 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 three of the GNU Affero 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 Affero 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 Purge Command
32 * Purges Files from specific JobIds
34 * Purges Jobs from Volumes
36 * Kern Sibbald, February MMII
43 /* Forward referenced functions */
44 static int purge_files_from_client(UAContext *ua, CLIENT *client);
45 static int purge_jobs_from_client(UAContext *ua, CLIENT *client);
46 static int action_on_purge_cmd(UAContext *ua, const char *cmd);
48 static const char *select_jobsfiles_from_client =
49 "SELECT JobId FROM Job "
53 static const char *select_jobs_from_client =
54 "SELECT JobId, PurgedFiles FROM Job "
58 * Purge records from database
60 * Purge Files (from) [Job|JobId|Client|Volume]
61 * Purge Jobs (from) [Client|Volume]
63 * N.B. Not all above is implemented yet.
65 int purgecmd(UAContext *ua, const char *cmd)
71 static const char *keywords[] = {
77 static const char *files_keywords[] = {
84 static const char *jobs_keywords[] = {
90 "\nThis command can be DANGEROUS!!!\n\n"
91 "It purges (deletes) all Files from a Job,\n"
92 "JobId, Client or Volume; or it purges (deletes)\n"
93 "all Jobs from a Client or Volume without regard\n"
94 "to retention periods. Normally you should use the\n"
95 "PRUNE command, which respects retention periods.\n"));
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 action_on_purge_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_ids == 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_ids,
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_ids == 0) {
263 ua->warning_msg(_("No Files 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_ids,
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());
296 * Now mark Job as having files purged. This is necessary to
297 * avoid having too many Jobs to process in future prunings. If
298 * we don't do this, the number of JobId's in our in memory list
299 * could grow very large.
301 Mmsg(query, "UPDATE Job SET PurgedFiles=1 WHERE JobId IN (%s)", jobs);
302 db_sql_query(ua->db, query.c_str(), NULL, (void *)NULL);
303 Dmsg1(050, "Mark purged sql=%s\n", query.c_str());
307 * Delete jobs (all records) from the catalog in groups of 1000
310 void purge_job_list_from_catalog(UAContext *ua, del_ctx &del)
312 POOL_MEM jobids(PM_MESSAGE);
315 for (int i=0; del.num_ids; ) {
316 Dmsg1(150, "num_ids=%d\n", del.num_ids);
317 pm_strcat(jobids, "");
318 for (int j=0; j<1000 && del.num_ids>0; j++) {
320 if (del.JobId[i] == 0 || ua->jcr->JobId == del.JobId[i]) {
321 Dmsg2(150, "skip JobId[%d]=%d\n", i, (int)del.JobId[i]);
325 if (*jobids.c_str() != 0) {
326 pm_strcat(jobids, ",");
328 pm_strcat(jobids, edit_int64(del.JobId[i++], ed1));
329 Dmsg1(150, "Add id=%s\n", ed1);
332 Dmsg1(150, "num_ids=%d\n", del.num_ids);
333 purge_jobs_from_catalog(ua, jobids.c_str());
338 * Delete files from a list of jobs in groups of 1000
341 void purge_files_from_job_list(UAContext *ua, del_ctx &del)
343 POOL_MEM jobids(PM_MESSAGE);
346 * OK, now we have the list of JobId's to be pruned, send them
347 * off to be deleted batched 1000 at a time.
349 for (int i=0; del.num_ids; ) {
350 pm_strcat(jobids, "");
351 for (int j=0; j<1000 && del.num_ids>0; j++) {
353 if (del.JobId[i] == 0 || ua->jcr->JobId == del.JobId[i]) {
354 Dmsg2(150, "skip JobId[%d]=%d\n", i, (int)del.JobId[i]);
358 if (*jobids.c_str() != 0) {
359 pm_strcat(jobids, ",");
361 pm_strcat(jobids, edit_int64(del.JobId[i++], ed1));
362 Dmsg1(150, "Add id=%s\n", ed1);
365 purge_files_from_jobs(ua, jobids.c_str());
370 * Change the type of the next copy job to backup.
371 * We need to upgrade the next copy of a normal job,
372 * and also upgrade the next copy when the normal job
373 * already have been purged.
375 * JobId: 1 PriorJobId: 0 (original)
376 * JobId: 2 PriorJobId: 1 (first copy)
377 * JobId: 3 PriorJobId: 1 (second copy)
379 * JobId: 2 PriorJobId: 1 (first copy, now regular backup)
380 * JobId: 3 PriorJobId: 1 (second copy)
382 * => Search through PriorJobId in jobid and
383 * PriorJobId in PriorJobId (jobid)
385 void upgrade_copies(UAContext *ua, char *jobs)
387 POOL_MEM query(PM_MESSAGE);
391 /* Do it in two times for mysql */
392 Mmsg(query, uap_upgrade_copies_oldest_job[db_type], JT_JOB_COPY, jobs, jobs);
393 db_sql_query(ua->db, query.c_str(), NULL, (void *)NULL);
394 Dmsg1(050, "Upgrade copies Log sql=%s\n", query.c_str());
396 /* Now upgrade first copy to Backup */
397 Mmsg(query, "UPDATE Job SET Type='B' " /* JT_JOB_COPY => JT_BACKUP */
398 "WHERE JobId IN ( SELECT JobId FROM cpy_tmp )");
400 db_sql_query(ua->db, query.c_str(), NULL, (void *)NULL);
402 Mmsg(query, "DROP TABLE cpy_tmp");
403 db_sql_query(ua->db, query.c_str(), NULL, (void *)NULL);
409 * Remove all records from catalog for a list of JobIds
411 void purge_jobs_from_catalog(UAContext *ua, char *jobs)
413 POOL_MEM query(PM_MESSAGE);
415 /* Delete (or purge) records associated with the job */
416 purge_files_from_jobs(ua, jobs);
418 Mmsg(query, "DELETE FROM JobMedia WHERE JobId IN (%s)", jobs);
419 db_sql_query(ua->db, query.c_str(), NULL, (void *)NULL);
420 Dmsg1(050, "Delete JobMedia sql=%s\n", query.c_str());
422 Mmsg(query, "DELETE FROM Log WHERE JobId IN (%s)", jobs);
423 db_sql_query(ua->db, query.c_str(), NULL, (void *)NULL);
424 Dmsg1(050, "Delete Log sql=%s\n", query.c_str());
426 Mmsg(query, "DELETE FROM RestoreObject WHERE JobId IN (%s)", jobs);
427 db_sql_query(ua->db, query.c_str(), NULL, (void *)NULL);
428 Dmsg1(050, "Delete RestoreObject sql=%s\n", query.c_str());
430 Mmsg(query, "DELETE FROM PathVisibility WHERE JobId IN (%s)", jobs);
431 db_sql_query(ua->db, query.c_str(), NULL, (void *)NULL);
432 Dmsg1(050, "Delete PathVisibility sql=%s\n", query.c_str());
434 upgrade_copies(ua, jobs);
436 /* Now remove the Job record itself */
437 Mmsg(query, "DELETE FROM Job WHERE JobId IN (%s)", jobs);
438 db_sql_query(ua->db, query.c_str(), NULL, (void *)NULL);
440 Dmsg1(050, "Delete Job sql=%s\n", query.c_str());
443 void purge_files_from_volume(UAContext *ua, MEDIA_DBR *mr )
444 {} /* ***FIXME*** implement */
447 * Returns: 1 if Volume purged
448 * 0 if Volume not purged
450 bool purge_jobs_from_volume(UAContext *ua, MEDIA_DBR *mr, bool force)
452 POOL_MEM query(PM_MESSAGE);
460 stat = strcmp(mr->VolStatus, "Append") == 0 ||
461 strcmp(mr->VolStatus, "Full") == 0 ||
462 strcmp(mr->VolStatus, "Used") == 0 ||
463 strcmp(mr->VolStatus, "Error") == 0;
465 ua->error_msg(_("\nVolume \"%s\" has VolStatus \"%s\" and cannot be purged.\n"
466 "The VolStatus must be: Append, Full, Used, or Error to be purged.\n"),
467 mr->VolumeName, mr->VolStatus);
471 memset(&jr, 0, sizeof(jr));
472 memset(&del, 0, sizeof(del));
474 del.JobId = (JobId_t *)malloc(sizeof(JobId_t) * del.max_ids);
477 * Check if he wants to purge a single jobid
479 i = find_arg_with_value(ua, "jobid");
482 del.JobId[0] = str_to_int64(ua->argv[i]);
487 Mmsg(query, "SELECT DISTINCT JobId FROM JobMedia WHERE MediaId=%s",
488 edit_int64(mr->MediaId, ed1));
489 if (!db_sql_query(ua->db, query.c_str(), file_delete_handler, (void *)&del)) {
490 ua->error_msg("%s", db_strerror(ua->db));
491 Dmsg0(050, "Count failed\n");
496 purge_job_list_from_catalog(ua, del);
498 ua->info_msg(_("%d File%s on Volume \"%s\" purged from catalog.\n"), del.num_del,
499 del.num_del==1?"":"s", mr->VolumeName);
501 purged = is_volume_purged(ua, mr, force);
511 * This routine will check the JobMedia records to see if the
512 * Volume has been purged. If so, it marks it as such and
514 * Returns: true if volume purged
517 * Note, we normally will not purge a volume that has Firstor LastWritten
518 * zero, because it means the volume is most likely being written
519 * however, if the user manually purges using the purge command in
520 * the console, he has been warned, and we go ahead and purge
521 * the volume anyway, if possible).
523 bool is_volume_purged(UAContext *ua, MEDIA_DBR *mr, bool force)
525 POOL_MEM query(PM_MESSAGE);
526 struct s_count_ctx cnt;
530 if (!force && (mr->FirstWritten == 0 || mr->LastWritten == 0)) {
531 goto bail_out; /* not written cannot purge */
534 if (strcmp(mr->VolStatus, "Purged") == 0) {
539 /* If purged, mark it so */
541 Mmsg(query, "SELECT count(*) FROM JobMedia WHERE MediaId=%s",
542 edit_int64(mr->MediaId, ed1));
543 if (!db_sql_query(ua->db, query.c_str(), del_count_handler, (void *)&cnt)) {
544 ua->error_msg("%s", db_strerror(ua->db));
545 Dmsg0(050, "Count failed\n");
549 if (cnt.count == 0) {
550 ua->warning_msg(_("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));
560 static BSOCK *open_sd_bsock(UAContext *ua)
562 STORE *store = ua->jcr->wstore;
564 if (!ua->jcr->store_bsock) {
565 ua->send_msg(_("Connecting to Storage daemon %s at %s:%d ...\n"),
566 store->name(), store->address, store->SDport);
567 if (!connect_to_storage_daemon(ua->jcr, 10, SDConnectTimeout, 1)) {
568 ua->error_msg(_("Failed to connect to Storage daemon.\n"));
572 return ua->jcr->store_bsock;
576 * Called here to send the appropriate commands to the SD
577 * to do truncate on purge.
579 static void do_truncate_on_purge(UAContext *ua, MEDIA_DBR *mr,
580 char *pool, char *storage,
581 int drive, BSOCK *sd)
585 uint64_t VolBytes = 0;
587 /* TODO: Return if not mr->Recyle ? */
592 /* Do it only if action on purge = truncate is set */
593 if (!(mr->ActionOnPurge & ON_PURGE_TRUNCATE)) {
597 * Send the command to truncate the volume after purge. If this feature
598 * is disabled for the specific device, this will be a no-op.
601 /* Protect us from spaces */
602 bash_spaces(mr->VolumeName);
603 bash_spaces(mr->MediaType);
605 bash_spaces(storage);
607 /* Do it by relabeling the Volume, which truncates it */
608 sd->fsend("relabel %s OldName=%s NewName=%s PoolName=%s "
609 "MediaType=%s Slot=%d drive=%d\n",
611 mr->VolumeName, mr->VolumeName,
612 pool, mr->MediaType, mr->Slot, drive);
614 unbash_spaces(mr->VolumeName);
615 unbash_spaces(mr->MediaType);
617 unbash_spaces(storage);
619 /* Send relabel command, and check for valid response */
620 while (sd->recv() >= 0) {
621 ua->send_msg("%s", sd->msg);
622 if (sscanf(sd->msg, "3000 OK label. VolBytes=%llu DVD=%d ", &VolBytes, &dvd) == 2) {
628 mr->VolBytes = VolBytes;
630 if (!db_update_media_record(ua->jcr, ua->db, mr)) {
631 ua->error_msg(_("Can't update volume size in the catalog\n"));
633 ua->send_msg(_("The volume \"%s\" has been truncated\n"), mr->VolumeName);
635 ua->warning_msg(_("Unable to truncate volume \"%s\"\n"), mr->VolumeName);
640 * Implement Bacula bconsole command purge action
641 * purge action= pool= volume= storage= devicetype=
643 static int action_on_purge_cmd(UAContext *ua, const char *cmd)
645 bool allpools = false;
648 uint32_t *results = NULL;
649 const char *action = "all";
656 memset(&pr, 0, sizeof(pr));
657 memset(&mr, 0, sizeof(mr));
659 /* Look at arguments */
660 for (int i=1; i<ua->argc; i++) {
661 if (strcasecmp(ua->argk[i], NT_("allpools")) == 0) {
664 } else if (strcasecmp(ua->argk[i], NT_("volume")) == 0 && ua->argv[i]) {
665 bstrncpy(mr.VolumeName, ua->argv[i], sizeof(mr.VolumeName));
667 } else if (strcasecmp(ua->argk[i], NT_("devicetype")) == 0 && ua->argv[i]) {
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 && ua->argv[i]) {
679 ua->jcr->wstore = store = get_storage_resource(ua, false);
683 mr.StorageId = store->StorageId;
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 bstrncpy(mr.VolStatus, "Purged", sizeof(mr.VolStatus));
713 if (!db_get_media_ids(ua->jcr, ua->db, &mr, &nb, &results)) {
714 Dmsg0(100, "No results from db_get_media_ids\n");
719 ua->send_msg(_("No Volumes found to perform %s action.\n"), action);
723 if ((sd=open_sd_bsock(ua)) == NULL) {
724 Dmsg0(100, "Can't open connection to sd\n");
729 * Loop over the candidate Volumes and actually truncate them
731 for (int i=0; i < nb; i++) {
732 memset(&mr, 0, sizeof(mr));
733 mr.MediaId = results[i];
734 if (db_get_media_record(ua->jcr, ua->db, &mr)) {
735 /* TODO: ask for drive and change Pool */
736 if (!strcasecmp("truncate", action) || !strcasecmp("all", action)) {
737 do_truncate_on_purge(ua, &mr, pr.Name, store->dev_name(), drive, sd);
740 Dmsg1(0, "Can't find MediaId=%lld\n", (uint64_t) mr.MediaId);
747 sd->signal(BNET_TERMINATE);
749 ua->jcr->store_bsock = NULL;
751 ua->jcr->wstore = NULL;
760 * IF volume status is Append, Full, Used, or Error, mark it Purged
761 * Purged volumes can then be recycled (if enabled).
763 bool mark_media_purged(UAContext *ua, MEDIA_DBR *mr)
766 if (strcmp(mr->VolStatus, "Append") == 0 ||
767 strcmp(mr->VolStatus, "Full") == 0 ||
768 strcmp(mr->VolStatus, "Used") == 0 ||
769 strcmp(mr->VolStatus, "Error") == 0) {
770 bstrncpy(mr->VolStatus, "Purged", sizeof(mr->VolStatus));
771 if (!db_update_media_record(jcr, ua->db, mr)) {
774 pm_strcpy(jcr->VolumeName, mr->VolumeName);
775 generate_job_event(jcr, "VolumePurged");
776 generate_plugin_event(jcr, bEventVolumePurged);
778 * If the RecyclePool is defined, move the volume there
780 if (mr->RecyclePoolId && mr->RecyclePoolId != mr->PoolId) {
781 POOL_DBR oldpr, newpr;
782 memset(&oldpr, 0, sizeof(POOL_DBR));
783 memset(&newpr, 0, sizeof(POOL_DBR));
784 newpr.PoolId = mr->RecyclePoolId;
785 oldpr.PoolId = mr->PoolId;
786 if ( db_get_pool_record(jcr, ua->db, &oldpr)
787 && db_get_pool_record(jcr, ua->db, &newpr))
789 /* check if destination pool size is ok */
790 if (newpr.MaxVols > 0 && newpr.NumVols >= newpr.MaxVols) {
791 ua->error_msg(_("Unable move recycled Volume in full "
792 "Pool \"%s\" MaxVols=%d\n"),
793 newpr.Name, newpr.MaxVols);
795 } else { /* move media */
796 update_vol_pool(ua, newpr.Name, mr, &oldpr);
799 ua->error_msg("%s", db_strerror(ua->db));
803 /* Send message to Job report, if it is a *real* job */
804 if (jcr && jcr->JobId > 0) {
805 Jmsg(jcr, M_INFO, 0, _("All records pruned from Volume \"%s\"; marking it \"Purged\"\n"),
810 ua->error_msg(_("Cannot purge Volume with VolStatus=%s\n"), mr->VolStatus);
812 return strcmp(mr->VolStatus, "Purged") == 0;