2 Bacula® - The Network Backup Solution
4 Copyright (C) 2002-2008 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 Purge Command
32 * Purges Files from specific JobIds
34 * Purges Jobs from Volumes
36 * Kern Sibbald, February MMII
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);
47 static int aop_cmd(UAContext *ua, const char *cmd);
49 static const char *select_jobsfiles_from_client =
50 "SELECT JobId FROM Job "
54 static const char *select_jobs_from_client =
55 "SELECT JobId, PurgedFiles FROM Job "
59 * Purge records from database
61 * Purge Files (from) [Job|JobId|Client|Volume]
62 * Purge Jobs (from) [Client|Volume]
64 * N.B. Not all above is implemented yet.
66 int purgecmd(UAContext *ua, const char *cmd)
72 static const char *keywords[] = {
78 static const char *files_keywords[] = {
85 static const char *jobs_keywords[] = {
91 "\nThis command can be DANGEROUS!!!\n\n"
92 "It purges (deletes) all Files from a Job,\n"
93 "JobId, Client or Volume; or it purges (deletes)\n"
94 "all Jobs from a Client or Volume without regard\n"
95 "to retention periods. Normally you should use the\n"
96 "PRUNE command, which respects retention periods.\n"));
101 switch (find_arg_keyword(ua, keywords)) {
104 switch(find_arg_keyword(ua, files_keywords)) {
107 if (get_job_dbr(ua, &jr)) {
109 edit_int64(jr.JobId, jobid);
110 purge_files_from_jobs(ua, jobid);
114 client = get_client_resource(ua);
116 purge_files_from_client(ua, client);
120 if (select_media_dbr(ua, &mr)) {
121 purge_files_from_volume(ua, &mr);
127 switch(find_arg_keyword(ua, jobs_keywords)) {
129 client = get_client_resource(ua);
131 purge_jobs_from_client(ua, client);
135 if (select_media_dbr(ua, &mr)) {
136 purge_jobs_from_volume(ua, &mr, /*force*/true);
142 /* Perform ActionOnPurge (action=truncate) */
143 if (find_arg(ua, "action") >= 0) {
144 return aop_cmd(ua, ua->cmd);
147 while ((i=find_arg(ua, NT_("volume"))) >= 0) {
148 if (select_media_dbr(ua, &mr)) {
149 purge_jobs_from_volume(ua, &mr, /*force*/true);
151 *ua->argk[i] = 0; /* zap keyword already seen */
158 switch (do_keyword_prompt(ua, _("Choose item to purge"), keywords)) {
160 client = get_client_resource(ua);
162 purge_files_from_client(ua, client);
166 client = get_client_resource(ua);
168 purge_jobs_from_client(ua, client);
172 if (select_media_dbr(ua, &mr)) {
173 purge_jobs_from_volume(ua, &mr, /*force*/true);
181 * Purge File records from the database. For any Job which
182 * is older than the retention period, we unconditionally delete
183 * all File records for that Job. This is simple enough that no
184 * temporary tables are needed. We simply make an in memory list of
185 * the JobIds meeting the prune conditions, then delete all File records
186 * pointing to each of those JobIds.
188 static int purge_files_from_client(UAContext *ua, CLIENT *client)
191 POOL_MEM query(PM_MESSAGE);
195 memset(&cr, 0, sizeof(cr));
196 bstrncpy(cr.Name, client->name(), sizeof(cr.Name));
197 if (!db_create_client_record(ua->jcr, ua->db, &cr)) {
201 memset(&del, 0, sizeof(del));
203 del.JobId = (JobId_t *)malloc(sizeof(JobId_t) * del.max_ids);
205 ua->info_msg(_("Begin purging files for Client \"%s\"\n"), cr.Name);
207 Mmsg(query, select_jobsfiles_from_client, edit_int64(cr.ClientId, ed1));
208 Dmsg1(050, "select sql=%s\n", query.c_str());
209 db_sql_query(ua->db, query.c_str(), file_delete_handler, (void *)&del);
211 purge_files_from_job_list(ua, del);
213 if (del.num_ids == 0) {
214 ua->warning_msg(_("No Files found for client %s to purge from %s catalog.\n"),
215 client->name(), client->catalog->name());
217 ua->info_msg(_("Files for %d Jobs for client \"%s\" purged from %s catalog.\n"), del.num_ids,
218 client->name(), client->catalog->name());
230 * Purge Job records from the database. For any Job which
231 * is older than the retention period, we unconditionally delete
232 * it and all File records for that Job. This is simple enough that no
233 * temporary tables are needed. We simply make an in memory list of
234 * the JobIds then delete the Job, Files, and JobMedia records in that list.
236 static int purge_jobs_from_client(UAContext *ua, CLIENT *client)
239 POOL_MEM query(PM_MESSAGE);
243 memset(&cr, 0, sizeof(cr));
245 bstrncpy(cr.Name, client->name(), sizeof(cr.Name));
246 if (!db_create_client_record(ua->jcr, ua->db, &cr)) {
250 memset(&del, 0, sizeof(del));
252 del.JobId = (JobId_t *)malloc(sizeof(JobId_t) * del.max_ids);
253 del.PurgedFiles = (char *)malloc(del.max_ids);
255 ua->info_msg(_("Begin purging jobs from Client \"%s\"\n"), cr.Name);
257 Mmsg(query, select_jobs_from_client, edit_int64(cr.ClientId, ed1));
258 Dmsg1(150, "select sql=%s\n", query.c_str());
259 db_sql_query(ua->db, query.c_str(), job_delete_handler, (void *)&del);
261 purge_job_list_from_catalog(ua, del);
263 if (del.num_ids == 0) {
264 ua->warning_msg(_("No Files found for client %s to purge from %s catalog.\n"),
265 client->name(), client->catalog->name());
267 ua->info_msg(_("%d Jobs for client %s purged from %s catalog.\n"), del.num_ids,
268 client->name(), client->catalog->name());
274 if (del.PurgedFiles) {
275 free(del.PurgedFiles);
282 * Remove File records from a list of JobIds
284 void purge_files_from_jobs(UAContext *ua, char *jobs)
286 POOL_MEM query(PM_MESSAGE);
288 Mmsg(query, "DELETE FROM File WHERE JobId IN (%s)", jobs);
289 db_sql_query(ua->db, query.c_str(), NULL, (void *)NULL);
290 Dmsg1(050, "Delete File sql=%s\n", query.c_str());
292 Mmsg(query, "DELETE FROM BaseFiles WHERE JobId IN (%s)", jobs);
293 db_sql_query(ua->db, query.c_str(), NULL, (void *)NULL);
294 Dmsg1(050, "Delete BaseFiles sql=%s\n", query.c_str());
297 * Now mark Job as having files purged. This is necessary to
298 * avoid having too many Jobs to process in future prunings. If
299 * we don't do this, the number of JobId's in our in memory list
300 * could grow very large.
302 Mmsg(query, "UPDATE Job SET PurgedFiles=1 WHERE JobId IN (%s)", jobs);
303 db_sql_query(ua->db, query.c_str(), NULL, (void *)NULL);
304 Dmsg1(050, "Mark purged sql=%s\n", query.c_str());
308 * Delete jobs (all records) from the catalog in groups of 1000
311 void purge_job_list_from_catalog(UAContext *ua, del_ctx &del)
313 POOL_MEM jobids(PM_MESSAGE);
316 for (int i=0; del.num_ids; ) {
317 Dmsg1(150, "num_ids=%d\n", del.num_ids);
318 pm_strcat(jobids, "");
319 for (int j=0; j<1000 && del.num_ids>0; j++) {
321 if (del.JobId[i] == 0 || ua->jcr->JobId == del.JobId[i]) {
322 Dmsg2(150, "skip JobId[%d]=%d\n", i, (int)del.JobId[i]);
326 if (*jobids.c_str() != 0) {
327 pm_strcat(jobids, ",");
329 pm_strcat(jobids, edit_int64(del.JobId[i++], ed1));
330 Dmsg1(150, "Add id=%s\n", ed1);
333 Dmsg1(150, "num_ids=%d\n", del.num_ids);
334 purge_jobs_from_catalog(ua, jobids.c_str());
339 * Delete files from a list of jobs in groups of 1000
342 void purge_files_from_job_list(UAContext *ua, del_ctx &del)
344 POOL_MEM jobids(PM_MESSAGE);
347 * OK, now we have the list of JobId's to be pruned, send them
348 * off to be deleted batched 1000 at a time.
350 for (int i=0; del.num_ids; ) {
351 pm_strcat(jobids, "");
352 for (int j=0; j<1000 && del.num_ids>0; j++) {
354 if (del.JobId[i] == 0 || ua->jcr->JobId == del.JobId[i]) {
355 Dmsg2(150, "skip JobId[%d]=%d\n", i, (int)del.JobId[i]);
359 if (*jobids.c_str() != 0) {
360 pm_strcat(jobids, ",");
362 pm_strcat(jobids, edit_int64(del.JobId[i++], ed1));
363 Dmsg1(150, "Add id=%s\n", ed1);
366 purge_files_from_jobs(ua, jobids.c_str());
371 * Change the type of the next copy job to backup.
372 * We need to upgrade the next copy of a normal job,
373 * and also upgrade the next copy when the normal job
374 * already have been purged.
376 * JobId: 1 PriorJobId: 0 (original)
377 * JobId: 2 PriorJobId: 1 (first copy)
378 * JobId: 3 PriorJobId: 1 (second copy)
380 * JobId: 2 PriorJobId: 1 (first copy, now regular backup)
381 * JobId: 3 PriorJobId: 1 (second copy)
383 * => Search through PriorJobId in jobid and
384 * PriorJobId in PriorJobId (jobid)
386 void upgrade_copies(UAContext *ua, char *jobs)
388 POOL_MEM query(PM_MESSAGE);
391 /* Do it in two times for mysql */
392 Mmsg(query, "CREATE TEMPORARY TABLE cpy_tmp AS "
393 "SELECT MIN(JobId) AS JobId FROM Job " /* Choose the oldest job */
395 "AND ( PriorJobId IN (%s) "
400 "WHERE JobId IN (%s) "
404 "GROUP BY PriorJobId ", /* one result per copy */
405 JT_JOB_COPY, jobs, jobs);
406 db_sql_query(ua->db, query.c_str(), NULL, (void *)NULL);
407 Dmsg1(050, "Upgrade copies Log sql=%s\n", query.c_str());
409 /* Now upgrade first copy to Backup */
410 Mmsg(query, "UPDATE Job SET Type='B' " /* JT_JOB_COPY => JT_BACKUP */
411 "WHERE JobId IN ( SELECT JobId FROM cpy_tmp )");
413 db_sql_query(ua->db, query.c_str(), NULL, (void *)NULL);
415 Mmsg(query, "DROP TABLE cpy_tmp");
416 db_sql_query(ua->db, query.c_str(), NULL, (void *)NULL);
422 * Remove all records from catalog for a list of JobIds
424 void purge_jobs_from_catalog(UAContext *ua, char *jobs)
426 POOL_MEM query(PM_MESSAGE);
428 /* Delete (or purge) records associated with the job */
429 purge_files_from_jobs(ua, jobs);
431 Mmsg(query, "DELETE FROM JobMedia WHERE JobId IN (%s)", jobs);
432 db_sql_query(ua->db, query.c_str(), NULL, (void *)NULL);
433 Dmsg1(050, "Delete JobMedia sql=%s\n", query.c_str());
435 Mmsg(query, "DELETE FROM Log WHERE JobId IN (%s)", jobs);
436 db_sql_query(ua->db, query.c_str(), NULL, (void *)NULL);
437 Dmsg1(050, "Delete Log sql=%s\n", query.c_str());
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);
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);
476 memset(&jr, 0, sizeof(jr));
477 memset(&del, 0, sizeof(del));
479 del.JobId = (JobId_t *)malloc(sizeof(JobId_t) * del.max_ids);
482 * Check if he wants to purge a single jobid
484 i = find_arg_with_value(ua, "jobid");
487 del.JobId[0] = str_to_int64(ua->argv[i]);
492 Mmsg(query, "SELECT DISTINCT JobId FROM JobMedia WHERE MediaId=%s",
493 edit_int64(mr->MediaId, ed1));
494 if (!db_sql_query(ua->db, query.c_str(), file_delete_handler, (void *)&del)) {
495 ua->error_msg("%s", db_strerror(ua->db));
496 Dmsg0(050, "Count failed\n");
501 purge_job_list_from_catalog(ua, del);
503 ua->info_msg(_("%d File%s on Volume \"%s\" purged from catalog.\n"), del.num_del,
504 del.num_del==1?"":"s", mr->VolumeName);
506 purged = is_volume_purged(ua, mr, force);
516 * This routine will check the JobMedia records to see if the
517 * Volume has been purged. If so, it marks it as such and
519 * Returns: true if volume purged
522 * Note, we normally will not purge a volume that has Firstor LastWritten
523 * zero, because it means the volume is most likely being written
524 * however, if the user manually purges using the purge command in
525 * the console, he has been warned, and we go ahead and purge
526 * the volume anyway, if possible).
528 bool is_volume_purged(UAContext *ua, MEDIA_DBR *mr, bool force)
530 POOL_MEM query(PM_MESSAGE);
531 struct s_count_ctx cnt;
535 if (!force && (mr->FirstWritten == 0 || mr->LastWritten == 0)) {
536 goto bail_out; /* not written cannot purge */
539 if (strcmp(mr->VolStatus, "Purged") == 0) {
544 /* If purged, mark it so */
546 Mmsg(query, "SELECT count(*) FROM JobMedia WHERE MediaId=%s",
547 edit_int64(mr->MediaId, ed1));
548 if (!db_sql_query(ua->db, query.c_str(), del_count_handler, (void *)&cnt)) {
549 ua->error_msg("%s", db_strerror(ua->db));
550 Dmsg0(050, "Count failed\n");
554 if (cnt.count == 0) {
555 ua->warning_msg(_("There are no more Jobs associated with Volume \"%s\". Marking it purged.\n"),
557 if (!(purged = mark_media_purged(ua, mr))) {
558 ua->error_msg("%s", db_strerror(ua->db));
565 static BSOCK *open_sd_bsock(UAContext *ua)
567 STORE *store = ua->jcr->wstore;
569 if (!ua->jcr->store_bsock) {
570 ua->send_msg(_("Connecting to Storage daemon %s at %s:%d ...\n"),
571 store->name(), store->address, store->SDport);
572 if (!connect_to_storage_daemon(ua->jcr, 10, SDConnectTimeout, 1)) {
573 ua->error_msg(_("Failed to connect to Storage daemon.\n"));
577 return ua->jcr->store_bsock;
580 static void do_truncate_on_purge(UAContext *ua, MEDIA_DBR *mr,
581 char *pool, char *storage,
582 int drive, BSOCK *sd)
586 uint64_t VolBytes = 0;
588 /* TODO: Return if not mr->Recyle ? */
593 if (mr->ActionOnPurge & AOP_TRUNCATE) {
594 /* Send the command to truncate the volume after purge. If this feature
595 * is disabled for the specific device, this will be a no-op.
598 /* Protect us from spaces */
599 bash_spaces(mr->VolumeName);
600 bash_spaces(mr->MediaType);
602 bash_spaces(storage);
604 sd->fsend("relabel %s OldName=%s NewName=%s PoolName=%s "
605 "MediaType=%s Slot=%d drive=%d\n",
607 mr->VolumeName, mr->VolumeName,
608 pool, mr->MediaType, mr->Slot, drive);
610 unbash_spaces(mr->VolumeName);
611 unbash_spaces(mr->MediaType);
613 unbash_spaces(storage);
615 while (sd->recv() >= 0) {
616 ua->send_msg("%s", sd->msg);
617 if (sscanf(sd->msg, "3000 OK label. VolBytes=%llu DVD=%d ",
618 &VolBytes, &dvd) == 2)
625 mr->VolBytes = VolBytes;
627 if (!db_update_media_record(ua->jcr, ua->db, mr)) {
628 ua->error_msg(_("Can't update volume size in the catalog\n"));
630 ua->send_msg(_("The volume \"%s\" has been truncated\n"), mr->VolumeName);
632 ua->warning_msg(_("Unable to truncate volume \"%s\"\n"), mr->VolumeName);
637 /* purge action= pool= volume= storage= devicetype= */
638 static int aop_cmd(UAContext *ua, const char *cmd)
644 uint32_t *results=NULL;
645 const char *action="all";
653 memset(&pr, 0, sizeof(pr));
654 memset(&mr, 0, sizeof(mr));
657 for (int i=1; i<ua->argc; i++) {
658 if (strcasecmp(ua->argk[i], NT_("allpools")) == 0) {
661 } else if (strcasecmp(ua->argk[i], NT_("volume")) == 0 && ua->argv[i]) {
662 bstrncpy(mr.VolumeName, ua->argv[i], sizeof(mr.VolumeName));
664 } else if (strcasecmp(ua->argk[i], NT_("devicetype")) == 0 && ua->argv[i]) {
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 && ua->argv[i]) {
676 ua->jcr->wstore = store = get_storage_resource(ua, false);
680 mr.StorageId = store->StorageId;
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;
705 bstrncpy(mr.VolStatus, "Purged", sizeof(mr.VolStatus));
707 if (!db_get_media_ids(ua->jcr, ua->db, &mr, &nb, &results)) {
708 Dmsg0(100, "No results from db_get_media_ids\n");
713 ua->send_msg(_("No volume founds to perform %s action(s)\n"), action);
717 if ((sd=open_sd_bsock(ua)) == NULL) {
718 Dmsg0(100, "Can't open connection to sd\n");
722 for (int i=0; i < nb; i++) {
723 memset(&mr, 0, sizeof(mr));
724 mr.MediaId = results[i];
725 if (db_get_media_record(ua->jcr, ua->db, &mr)) {
726 /* TODO: ask for drive and change Pool */
727 if (!strcasecmp("truncate", action) || !strcasecmp("all", action)) {
728 do_truncate_on_purge(ua, &mr, pr.Name, store->dev_name(), drive, sd);
731 Dmsg1(0, "Can't find MediaId=%lld\n", (uint64_t) mr.MediaId);
738 sd->signal(BNET_TERMINATE);
740 ua->jcr->store_bsock = NULL;
742 ua->jcr->wstore = NULL;
751 * IF volume status is Append, Full, Used, or Error, mark it Purged
752 * Purged volumes can then be recycled (if enabled).
754 bool mark_media_purged(UAContext *ua, MEDIA_DBR *mr)
757 if (strcmp(mr->VolStatus, "Append") == 0 ||
758 strcmp(mr->VolStatus, "Full") == 0 ||
759 strcmp(mr->VolStatus, "Used") == 0 ||
760 strcmp(mr->VolStatus, "Error") == 0) {
761 bstrncpy(mr->VolStatus, "Purged", sizeof(mr->VolStatus));
762 if (!db_update_media_record(jcr, ua->db, mr)) {
765 pm_strcpy(jcr->VolumeName, mr->VolumeName);
766 generate_job_event(jcr, "VolumePurged");
767 generate_plugin_event(jcr, bEventVolumePurged);
769 * If the RecyclePool is defined, move the volume there
771 if (mr->RecyclePoolId && mr->RecyclePoolId != mr->PoolId) {
772 POOL_DBR oldpr, newpr;
773 memset(&oldpr, 0, sizeof(POOL_DBR));
774 memset(&newpr, 0, sizeof(POOL_DBR));
775 newpr.PoolId = mr->RecyclePoolId;
776 oldpr.PoolId = mr->PoolId;
777 if ( db_get_pool_record(jcr, ua->db, &oldpr)
778 && db_get_pool_record(jcr, ua->db, &newpr))
780 /* check if destination pool size is ok */
781 if (newpr.MaxVols > 0 && newpr.NumVols >= newpr.MaxVols) {
782 ua->error_msg(_("Unable move recycled Volume in full "
783 "Pool \"%s\" MaxVols=%d\n"),
784 newpr.Name, newpr.MaxVols);
786 } else { /* move media */
787 update_vol_pool(ua, newpr.Name, mr, &oldpr);
790 ua->error_msg("%s", db_strerror(ua->db));
794 /* Send message to Job report, if it is a *real* job */
795 if (jcr && jcr->JobId > 0) {
796 Jmsg(jcr, M_INFO, 0, _("All records pruned from Volume \"%s\"; marking it \"Purged\"\n"),
801 ua->error_msg(_("Cannot purge Volume with VolStatus=%s\n"), mr->VolStatus);
803 return strcmp(mr->VolStatus, "Purged") == 0;