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);
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 while ((i=find_arg(ua, NT_("volume"))) >= 0) {
142 if (select_media_dbr(ua, &mr)) {
143 purge_jobs_from_volume(ua, &mr, /*force*/true);
145 *ua->argk[i] = 0; /* zap keyword already seen */
152 switch (do_keyword_prompt(ua, _("Choose item to purge"), keywords)) {
154 client = get_client_resource(ua);
156 purge_files_from_client(ua, client);
160 client = get_client_resource(ua);
162 purge_jobs_from_client(ua, client);
166 if (select_media_dbr(ua, &mr)) {
167 purge_jobs_from_volume(ua, &mr, /*force*/true);
175 * Purge File records from the database. For any Job which
176 * is older than the retention period, we unconditionally delete
177 * all File records for that Job. This is simple enough that no
178 * temporary tables are needed. We simply make an in memory list of
179 * the JobIds meeting the prune conditions, then delete all File records
180 * pointing to each of those JobIds.
182 static int purge_files_from_client(UAContext *ua, CLIENT *client)
185 POOL_MEM query(PM_MESSAGE);
189 memset(&cr, 0, sizeof(cr));
190 bstrncpy(cr.Name, client->name(), sizeof(cr.Name));
191 if (!db_create_client_record(ua->jcr, ua->db, &cr)) {
195 memset(&del, 0, sizeof(del));
197 del.JobId = (JobId_t *)malloc(sizeof(JobId_t) * del.max_ids);
199 ua->info_msg(_("Begin purging files for Client \"%s\"\n"), cr.Name);
201 Mmsg(query, select_jobsfiles_from_client, edit_int64(cr.ClientId, ed1));
202 Dmsg1(050, "select sql=%s\n", query.c_str());
203 db_sql_query(ua->db, query.c_str(), file_delete_handler, (void *)&del);
205 purge_files_from_job_list(ua, del);
207 if (del.num_ids == 0) {
208 ua->warning_msg(_("No Files found for client %s to purge from %s catalog.\n"),
209 client->name(), client->catalog->name());
211 ua->info_msg(_("Files for %d Jobs for client \"%s\" purged from %s catalog.\n"), del.num_ids,
212 client->name(), client->catalog->name());
224 * Purge Job records from the database. For any Job which
225 * is older than the retention period, we unconditionally delete
226 * it and all File records for that Job. This is simple enough that no
227 * temporary tables are needed. We simply make an in memory list of
228 * the JobIds then delete the Job, Files, and JobMedia records in that list.
230 static int purge_jobs_from_client(UAContext *ua, CLIENT *client)
233 POOL_MEM query(PM_MESSAGE);
237 memset(&cr, 0, sizeof(cr));
239 bstrncpy(cr.Name, client->name(), sizeof(cr.Name));
240 if (!db_create_client_record(ua->jcr, ua->db, &cr)) {
244 memset(&del, 0, sizeof(del));
246 del.JobId = (JobId_t *)malloc(sizeof(JobId_t) * del.max_ids);
247 del.PurgedFiles = (char *)malloc(del.max_ids);
249 ua->info_msg(_("Begin purging jobs from Client \"%s\"\n"), cr.Name);
251 Mmsg(query, select_jobs_from_client, edit_int64(cr.ClientId, ed1));
252 Dmsg1(150, "select sql=%s\n", query.c_str());
253 db_sql_query(ua->db, query.c_str(), job_delete_handler, (void *)&del);
255 purge_job_list_from_catalog(ua, del);
257 if (del.num_ids == 0) {
258 ua->warning_msg(_("No Files found for client %s to purge from %s catalog.\n"),
259 client->name(), client->catalog->name());
261 ua->info_msg(_("%d Jobs for client %s purged from %s catalog.\n"), del.num_ids,
262 client->name(), client->catalog->name());
268 if (del.PurgedFiles) {
269 free(del.PurgedFiles);
276 * Remove File records from a list of JobIds
278 void purge_files_from_jobs(UAContext *ua, char *jobs)
280 POOL_MEM query(PM_MESSAGE);
282 Mmsg(query, "DELETE FROM File WHERE JobId IN (%s)", jobs);
283 db_sql_query(ua->db, query.c_str(), NULL, (void *)NULL);
284 Dmsg1(050, "Delete File sql=%s\n", query.c_str());
286 Mmsg(query, "DELETE FROM BaseFiles WHERE JobId IN (%s)", jobs);
287 db_sql_query(ua->db, query.c_str(), NULL, (void *)NULL);
288 Dmsg1(050, "Delete BaseFiles sql=%s\n", query.c_str());
291 * Now mark Job as having files purged. This is necessary to
292 * avoid having too many Jobs to process in future prunings. If
293 * we don't do this, the number of JobId's in our in memory list
294 * could grow very large.
296 Mmsg(query, "UPDATE Job SET PurgedFiles=1 WHERE JobId IN (%s)", jobs);
297 db_sql_query(ua->db, query.c_str(), NULL, (void *)NULL);
298 Dmsg1(050, "Mark purged sql=%s\n", query.c_str());
302 * Delete jobs (all records) from the catalog in groups of 1000
305 void purge_job_list_from_catalog(UAContext *ua, del_ctx &del)
307 POOL_MEM jobids(PM_MESSAGE);
310 for (int i=0; del.num_ids; ) {
311 Dmsg1(150, "num_ids=%d\n", del.num_ids);
312 pm_strcat(jobids, "");
313 for (int j=0; j<1000 && del.num_ids>0; j++) {
315 if (del.JobId[i] == 0 || ua->jcr->JobId == del.JobId[i]) {
316 Dmsg2(150, "skip JobId[%d]=%d\n", i, (int)del.JobId[i]);
320 if (*jobids.c_str() != 0) {
321 pm_strcat(jobids, ",");
323 pm_strcat(jobids, edit_int64(del.JobId[i++], ed1));
324 Dmsg1(150, "Add id=%s\n", ed1);
327 Dmsg1(150, "num_ids=%d\n", del.num_ids);
328 purge_jobs_from_catalog(ua, jobids.c_str());
333 * Delete files from a list of jobs in groups of 1000
336 void purge_files_from_job_list(UAContext *ua, del_ctx &del)
338 POOL_MEM jobids(PM_MESSAGE);
341 * OK, now we have the list of JobId's to be pruned, send them
342 * off to be deleted batched 1000 at a time.
344 for (int i=0; del.num_ids; ) {
345 pm_strcat(jobids, "");
346 for (int j=0; j<1000 && del.num_ids>0; j++) {
348 if (del.JobId[i] == 0 || ua->jcr->JobId == del.JobId[i]) {
349 Dmsg2(150, "skip JobId[%d]=%d\n", i, (int)del.JobId[i]);
353 if (*jobids.c_str() != 0) {
354 pm_strcat(jobids, ",");
356 pm_strcat(jobids, edit_int64(del.JobId[i++], ed1));
357 Dmsg1(150, "Add id=%s\n", ed1);
360 purge_files_from_jobs(ua, jobids.c_str());
365 * Change the type of the next copy job to backup.
366 * We need to upgrade the next copy of a normal job,
367 * and also upgrade the next copy when the normal job
368 * already have been purged.
370 * JobId: 1 PriorJobId: 0 (original)
371 * JobId: 2 PriorJobId: 1 (first copy)
372 * JobId: 3 PriorJobId: 1 (second copy)
374 * JobId: 2 PriorJobId: 1 (first copy, now regular backup)
375 * JobId: 3 PriorJobId: 1 (second copy)
377 * => Search through PriorJobId in jobid and
378 * PriorJobId in PriorJobId (jobid)
380 void upgrade_copies(UAContext *ua, char *jobs)
382 POOL_MEM query(PM_MESSAGE);
385 /* Do it in two times for mysql */
386 Mmsg(query, "CREATE TEMPORARY TABLE cpy_tmp AS "
387 "SELECT MIN(JobId) AS JobId FROM Job " /* Choose the oldest job */
389 "AND ( PriorJobId IN (%s) "
394 "WHERE JobId IN (%s) "
398 "GROUP BY PriorJobId ", /* one result per copy */
399 JT_JOB_COPY, jobs, jobs);
400 db_sql_query(ua->db, query.c_str(), NULL, (void *)NULL);
401 Dmsg1(050, "Upgrade copies Log sql=%s\n", query.c_str());
403 /* Now upgrade first copy to Backup */
404 Mmsg(query, "UPDATE Job SET Type='B' " /* JT_JOB_COPY => JT_BACKUP */
405 "WHERE JobId IN ( SELECT JobId FROM cpy_tmp )");
407 db_sql_query(ua->db, query.c_str(), NULL, (void *)NULL);
409 Mmsg(query, "DROP TABLE cpy_tmp");
410 db_sql_query(ua->db, query.c_str(), NULL, (void *)NULL);
416 * Remove all records from catalog for a list of JobIds
418 void purge_jobs_from_catalog(UAContext *ua, char *jobs)
420 POOL_MEM query(PM_MESSAGE);
422 /* Delete (or purge) records associated with the job */
423 purge_files_from_jobs(ua, jobs);
425 Mmsg(query, "DELETE FROM JobMedia WHERE JobId IN (%s)", jobs);
426 db_sql_query(ua->db, query.c_str(), NULL, (void *)NULL);
427 Dmsg1(050, "Delete JobMedia sql=%s\n", query.c_str());
429 Mmsg(query, "DELETE FROM Log WHERE JobId IN (%s)", jobs);
430 db_sql_query(ua->db, query.c_str(), NULL, (void *)NULL);
431 Dmsg1(050, "Delete Log sql=%s\n", query.c_str());
433 upgrade_copies(ua, jobs);
435 /* Now remove the Job record itself */
436 Mmsg(query, "DELETE FROM Job WHERE JobId IN (%s)", jobs);
437 db_sql_query(ua->db, query.c_str(), NULL, (void *)NULL);
439 Dmsg1(050, "Delete Job sql=%s\n", query.c_str());
442 void purge_files_from_volume(UAContext *ua, MEDIA_DBR *mr )
443 {} /* ***FIXME*** implement */
446 * Returns: 1 if Volume purged
447 * 0 if Volume not purged
449 bool purge_jobs_from_volume(UAContext *ua, MEDIA_DBR *mr, bool force)
451 POOL_MEM query(PM_MESSAGE);
459 stat = strcmp(mr->VolStatus, "Append") == 0 ||
460 strcmp(mr->VolStatus, "Full") == 0 ||
461 strcmp(mr->VolStatus, "Used") == 0 ||
462 strcmp(mr->VolStatus, "Error") == 0;
464 ua->error_msg(_("\nVolume \"%s\" has VolStatus \"%s\" and cannot be purged.\n"
465 "The VolStatus must be: Append, Full, Used, or Error to be purged.\n"),
466 mr->VolumeName, mr->VolStatus);
470 memset(&jr, 0, sizeof(jr));
471 memset(&del, 0, sizeof(del));
473 del.JobId = (JobId_t *)malloc(sizeof(JobId_t) * del.max_ids);
476 * Check if he wants to purge a single jobid
478 i = find_arg_with_value(ua, "jobid");
481 del.JobId[0] = str_to_int64(ua->argv[i]);
486 Mmsg(query, "SELECT DISTINCT JobId FROM JobMedia WHERE MediaId=%s",
487 edit_int64(mr->MediaId, ed1));
488 if (!db_sql_query(ua->db, query.c_str(), file_delete_handler, (void *)&del)) {
489 ua->error_msg("%s", db_strerror(ua->db));
490 Dmsg0(050, "Count failed\n");
495 purge_job_list_from_catalog(ua, del);
497 ua->info_msg(_("%d File%s on Volume \"%s\" purged from catalog.\n"), del.num_del,
498 del.num_del==1?"":"s", mr->VolumeName);
500 purged = is_volume_purged(ua, mr, force);
510 * This routine will check the JobMedia records to see if the
511 * Volume has been purged. If so, it marks it as such and
513 * Returns: true if volume purged
516 * Note, we normally will not purge a volume that has Firstor LastWritten
517 * zero, because it means the volume is most likely being written
518 * however, if the user manually purges using the purge command in
519 * the console, he has been warned, and we go ahead and purge
520 * the volume anyway, if possible).
522 bool is_volume_purged(UAContext *ua, MEDIA_DBR *mr, bool force)
524 POOL_MEM query(PM_MESSAGE);
525 struct s_count_ctx cnt;
529 if (!force && (mr->FirstWritten == 0 || mr->LastWritten == 0)) {
530 goto bail_out; /* not written cannot purge */
533 if (strcmp(mr->VolStatus, "Purged") == 0) {
538 /* If purged, mark it so */
540 Mmsg(query, "SELECT count(*) FROM JobMedia WHERE MediaId=%s",
541 edit_int64(mr->MediaId, ed1));
542 if (!db_sql_query(ua->db, query.c_str(), del_count_handler, (void *)&cnt)) {
543 ua->error_msg("%s", db_strerror(ua->db));
544 Dmsg0(050, "Count failed\n");
548 if (cnt.count == 0) {
549 ua->warning_msg(_("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));
559 static BSOCK *open_sd_bsock(UAContext *ua)
561 STORE *store = ua->jcr->wstore;
563 if (!ua->jcr->store_bsock) {
564 ua->send_msg(_("Connecting to Storage daemon %s at %s:%d ...\n"),
565 store->name(), store->address, store->SDport);
566 if (!connect_to_storage_daemon(ua->jcr, 10, SDConnectTimeout, 1)) {
567 ua->error_msg(_("Failed to connect to Storage daemon.\n"));
571 return ua->jcr->store_bsock;
575 * IF volume status is Append, Full, Used, or Error, mark it Purged
576 * Purged volumes can then be recycled (if enabled).
578 bool mark_media_purged(UAContext *ua, MEDIA_DBR *mr)
580 char dev_name[MAX_NAME_LENGTH];
582 if (strcmp(mr->VolStatus, "Append") == 0 ||
583 strcmp(mr->VolStatus, "Full") == 0 ||
584 strcmp(mr->VolStatus, "Used") == 0 ||
585 strcmp(mr->VolStatus, "Error") == 0) {
586 bstrncpy(mr->VolStatus, "Purged", sizeof(mr->VolStatus));
587 if (!db_update_media_record(jcr, ua->db, mr)) {
591 /* Code currently disabled */
593 if (mr->ActionOnPurge > 0) {
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 if ((sd=open_sd_bsock(ua)) != NULL) {
599 bstrncpy(dev_name, ua->jcr->wstore->dev_name(), sizeof(dev_name));
600 bash_spaces(dev_name);
601 bash_spaces(mr->VolumeName);
602 sd->fsend("action_on_purge %s vol=%s action=%d",
606 unbash_spaces(mr->VolumeName);
607 while (sd->recv() >= 0) {
608 ua->send_msg("%s", sd->msg);
611 sd->signal(BNET_TERMINATE);
613 ua->jcr->store_bsock = NULL;
615 ua->error_msg(_("Could not connect to storage daemon"));
621 pm_strcpy(jcr->VolumeName, mr->VolumeName);
622 generate_job_event(jcr, "VolumePurged");
623 generate_plugin_event(jcr, bEventVolumePurged);
625 * If the RecyclePool is defined, move the volume there
627 if (mr->RecyclePoolId && mr->RecyclePoolId != mr->PoolId) {
628 POOL_DBR oldpr, newpr;
629 memset(&oldpr, 0, sizeof(POOL_DBR));
630 memset(&newpr, 0, sizeof(POOL_DBR));
631 newpr.PoolId = mr->RecyclePoolId;
632 oldpr.PoolId = mr->PoolId;
633 if ( db_get_pool_record(jcr, ua->db, &oldpr)
634 && db_get_pool_record(jcr, ua->db, &newpr))
636 /* check if destination pool size is ok */
637 if (newpr.MaxVols > 0 && newpr.NumVols >= newpr.MaxVols) {
638 ua->error_msg(_("Unable move recycled Volume in full "
639 "Pool \"%s\" MaxVols=%d\n"),
640 newpr.Name, newpr.MaxVols);
642 } else { /* move media */
643 update_vol_pool(ua, newpr.Name, mr, &oldpr);
646 ua->error_msg("%s", db_strerror(ua->db));
649 /* Send message to Job report, if it is a *real* job */
650 if (jcr && jcr->JobId > 0) {
651 Jmsg(jcr, M_INFO, 0, _("All records pruned from Volume \"%s\"; marking it \"Purged\"\n"),
656 ua->error_msg(_("Cannot purge Volume with VolStatus=%s\n"), mr->VolStatus);
658 return strcmp(mr->VolStatus, "Purged") == 0;