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 is 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 "for 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);
137 purge_jobs_from_volume(ua, &mr);
143 while ((i=find_arg(ua, NT_("volume"))) >= 0) {
144 if (select_media_dbr(ua, &mr)) {
145 purge_jobs_from_volume(ua, &mr, /*force*/true);
147 *ua->argk[i] = 0; /* zap keyword already seen */
154 switch (do_keyword_prompt(ua, _("Choose item to purge"), keywords)) {
156 client = get_client_resource(ua);
158 purge_files_from_client(ua, client);
162 client = get_client_resource(ua);
164 purge_jobs_from_client(ua, client);
168 if (select_media_dbr(ua, &mr)) {
169 purge_jobs_from_volume(ua, &mr, /*force*/true);
177 * Purge File records from the database. For any Job which
178 * is older than the retention period, we unconditionally delete
179 * all File records for that Job. This is simple enough that no
180 * temporary tables are needed. We simply make an in memory list of
181 * the JobIds meeting the prune conditions, then delete all File records
182 * pointing to each of those JobIds.
184 static int purge_files_from_client(UAContext *ua, CLIENT *client)
187 POOL_MEM query(PM_MESSAGE);
191 memset(&cr, 0, sizeof(cr));
192 bstrncpy(cr.Name, client->name(), sizeof(cr.Name));
193 if (!db_create_client_record(ua->jcr, ua->db, &cr)) {
197 memset(&del, 0, sizeof(del));
199 del.JobId = (JobId_t *)malloc(sizeof(JobId_t) * del.max_ids);
201 ua->info_msg(_("Begin purging files for Client \"%s\"\n"), cr.Name);
203 Mmsg(query, select_jobsfiles_from_client, edit_int64(cr.ClientId, ed1));
204 Dmsg1(050, "select sql=%s\n", query.c_str());
205 db_sql_query(ua->db, query.c_str(), file_delete_handler, (void *)&del);
207 purge_files_from_job_list(ua, del);
209 if (del.num_ids == 0) {
210 ua->warning_msg(_("No Files found for client %s to purge from %s catalog.\n"),
211 client->name(), client->catalog->name());
213 ua->info_msg(_("Files for %d Jobs for client \"%s\" purged from %s catalog.\n"), del.num_ids,
214 client->name(), client->catalog->name());
226 * Purge Job records from the database. For any Job which
227 * is older than the retention period, we unconditionally delete
228 * it and all File records for that Job. This is simple enough that no
229 * temporary tables are needed. We simply make an in memory list of
230 * the JobIds then delete the Job, Files, and JobMedia records in that list.
232 static int purge_jobs_from_client(UAContext *ua, CLIENT *client)
235 POOL_MEM query(PM_MESSAGE);
239 memset(&cr, 0, sizeof(cr));
241 bstrncpy(cr.Name, client->name(), sizeof(cr.Name));
242 if (!db_create_client_record(ua->jcr, ua->db, &cr)) {
246 memset(&del, 0, sizeof(del));
248 del.JobId = (JobId_t *)malloc(sizeof(JobId_t) * del.max_ids);
249 del.PurgedFiles = (char *)malloc(del.max_ids);
251 ua->info_msg(_("Begin purging jobs from Client \"%s\"\n"), cr.Name);
253 Mmsg(query, select_jobs_from_client, edit_int64(cr.ClientId, ed1));
254 Dmsg1(150, "select sql=%s\n", query.c_str());
255 db_sql_query(ua->db, query.c_str(), job_delete_handler, (void *)&del);
257 purge_job_list_from_catalog(ua, del);
259 if (del.num_ids == 0) {
260 ua->warning_msg(_("No Files found for client %s to purge from %s catalog.\n"),
261 client->name(), client->catalog->name());
263 ua->info_msg(_("%d Jobs for client %s purged from %s catalog.\n"), del.num_ids,
264 client->name(), client->catalog->name());
270 if (del.PurgedFiles) {
271 free(del.PurgedFiles);
278 * Remove File records from a list of JobIds
280 void purge_files_from_jobs(UAContext *ua, char *jobs)
282 POOL_MEM query(PM_MESSAGE);
284 Mmsg(query, "DELETE FROM File WHERE JobId IN (%s)", jobs);
285 db_sql_query(ua->db, query.c_str(), NULL, (void *)NULL);
286 Dmsg1(050, "Delete File sql=%s\n", query.c_str());
289 * Now mark Job as having files purged. This is necessary to
290 * avoid having too many Jobs to process in future prunings. If
291 * we don't do this, the number of JobId's in our in memory list
292 * could grow very large.
294 Mmsg(query, "UPDATE Job SET PurgedFiles=1 WHERE JobId IN (%s)", jobs);
295 db_sql_query(ua->db, query.c_str(), NULL, (void *)NULL);
296 Dmsg1(050, "Mark purged sql=%s\n", query.c_str());
300 * Delete jobs (all records) from the catalog in groups of 1000
303 void purge_job_list_from_catalog(UAContext *ua, del_ctx &del)
305 POOL_MEM jobids(PM_MESSAGE);
308 for (int i=0; del.num_ids; ) {
309 Dmsg1(150, "num_ids=%d\n", del.num_ids);
310 pm_strcat(jobids, "");
311 for (int j=0; j<1000 && del.num_ids>0; j++) {
313 if (del.JobId[i] == 0 || ua->jcr->JobId == del.JobId[i]) {
314 Dmsg2(150, "skip JobId[%d]=%d\n", i, (int)del.JobId[i]);
318 if (*jobids.c_str() != 0) {
319 pm_strcat(jobids, ",");
321 pm_strcat(jobids, edit_int64(del.JobId[i++], ed1));
322 Dmsg1(150, "Add id=%s\n", ed1);
325 Dmsg1(150, "num_ids=%d\n", del.num_ids);
326 purge_jobs_from_catalog(ua, jobids.c_str());
331 * Delete files from a list of jobs in groups of 1000
334 void purge_files_from_job_list(UAContext *ua, del_ctx &del)
336 POOL_MEM jobids(PM_MESSAGE);
339 * OK, now we have the list of JobId's to be pruned, send them
340 * off to be deleted batched 1000 at a time.
342 for (int i=0; del.num_ids; ) {
343 pm_strcat(jobids, "");
344 for (int j=0; j<1000 && del.num_ids>0; j++) {
346 if (del.JobId[i] == 0 || ua->jcr->JobId == del.JobId[i]) {
347 Dmsg2(150, "skip JobId[%d]=%d\n", i, (int)del.JobId[i]);
351 if (*jobids.c_str() != 0) {
352 pm_strcat(jobids, ",");
354 pm_strcat(jobids, edit_int64(del.JobId[i++], ed1));
355 Dmsg1(150, "Add id=%s\n", ed1);
358 purge_files_from_jobs(ua, jobids.c_str());
363 * Change the type of the next copy job to backup.
364 * We need to upgrade the next copy of a normal job,
365 * and also upgrade the next copy when the normal job
366 * already have been purged.
368 * JobId: 1 PriorJobId: 0 (original)
369 * JobId: 2 PriorJobId: 1 (first copy)
370 * JobId: 3 PriorJobId: 1 (second copy)
372 * JobId: 2 PriorJobId: 1 (first copy, now regular backup)
373 * JobId: 3 PriorJobId: 1 (second copy)
375 * => Search through PriorJobId in jobid and
376 * PriorJobId in PriorJobId (jobid)
378 void upgrade_copies(UAContext *ua, char *jobs)
380 POOL_MEM query(PM_MESSAGE);
383 /* Do it in two times for mysql */
384 Mmsg(query, "CREATE TEMPORARY TABLE cpy_tmp AS "
385 "SELECT MIN(JobId) AS JobId FROM Job " /* Choose the oldest job */
387 "AND ( PriorJobId IN (%s) "
392 "WHERE JobId IN (%s) "
396 "GROUP BY PriorJobId ", /* one result per copy */
397 JT_JOB_COPY, jobs, jobs);
398 db_sql_query(ua->db, query.c_str(), NULL, (void *)NULL);
399 Dmsg1(050, "Upgrade copies Log sql=%s\n", query.c_str());
401 /* Now upgrade first copy to Backup */
402 Mmsg(query, "UPDATE Job SET Type='B' " /* JT_JOB_COPY => JT_BACKUP */
403 "WHERE JobId IN ( SELECT JobId FROM cpy_tmp )");
405 db_sql_query(ua->db, query.c_str(), NULL, (void *)NULL);
407 Mmsg(query, "DROP TABLE cpy_tmp");
408 db_sql_query(ua->db, query.c_str(), NULL, (void *)NULL);
414 * Remove all records from catalog for a list of JobIds
416 void purge_jobs_from_catalog(UAContext *ua, char *jobs)
418 POOL_MEM query(PM_MESSAGE);
420 /* Delete (or purge) records associated with the job */
421 purge_files_from_jobs(ua, jobs);
423 Mmsg(query, "DELETE FROM JobMedia WHERE JobId IN (%s)", jobs);
424 db_sql_query(ua->db, query.c_str(), NULL, (void *)NULL);
425 Dmsg1(050, "Delete JobMedia sql=%s\n", query.c_str());
427 Mmsg(query, "DELETE FROM Log WHERE JobId IN (%s)", jobs);
428 db_sql_query(ua->db, query.c_str(), NULL, (void *)NULL);
429 Dmsg1(050, "Delete Log sql=%s\n", query.c_str());
431 upgrade_copies(ua, jobs);
433 /* Now remove the Job record itself */
434 Mmsg(query, "DELETE FROM Job WHERE JobId IN (%s)", jobs);
435 db_sql_query(ua->db, query.c_str(), NULL, (void *)NULL);
437 Dmsg1(050, "Delete Job sql=%s\n", query.c_str());
440 void purge_files_from_volume(UAContext *ua, MEDIA_DBR *mr )
441 {} /* ***FIXME*** implement */
444 * Returns: 1 if Volume purged
445 * 0 if Volume not purged
447 bool purge_jobs_from_volume(UAContext *ua, MEDIA_DBR *mr, bool force)
449 POOL_MEM query(PM_MESSAGE);
457 stat = strcmp(mr->VolStatus, "Append") == 0 ||
458 strcmp(mr->VolStatus, "Full") == 0 ||
459 strcmp(mr->VolStatus, "Used") == 0 ||
460 strcmp(mr->VolStatus, "Error") == 0;
462 ua->error_msg(_("\nVolume \"%s\" has VolStatus \"%s\" and cannot be purged.\n"
463 "The VolStatus must be: Append, Full, Used, or Error to be purged.\n"),
464 mr->VolumeName, mr->VolStatus);
468 memset(&jr, 0, sizeof(jr));
469 memset(&del, 0, sizeof(del));
471 del.JobId = (JobId_t *)malloc(sizeof(JobId_t) * del.max_ids);
474 * Check if he wants to purge a single jobid
476 i = find_arg_with_value(ua, "jobid");
479 del.JobId[0] = str_to_int64(ua->argv[i]);
484 Mmsg(query, "SELECT DISTINCT JobId FROM JobMedia WHERE MediaId=%s",
485 edit_int64(mr->MediaId, ed1));
486 if (!db_sql_query(ua->db, query.c_str(), file_delete_handler, (void *)&del)) {
487 ua->error_msg("%s", db_strerror(ua->db));
488 Dmsg0(050, "Count failed\n");
493 purge_job_list_from_catalog(ua, del);
495 ua->info_msg(_("%d File%s on Volume \"%s\" purged from catalog.\n"), del.num_del,
496 del.num_del==1?"":"s", mr->VolumeName);
498 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) {
536 /* If purged, mark it so */
538 Mmsg(query, "SELECT count(*) FROM JobMedia WHERE MediaId=%s",
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 if (!(purged = mark_media_purged(ua, mr))) {
550 ua->error_msg("%s", db_strerror(ua->db));
558 * IF volume status is Append, Full, Used, or Error, mark it Purged
559 * Purged volumes can then be recycled (if enabled).
561 bool mark_media_purged(UAContext *ua, MEDIA_DBR *mr)
564 if (strcmp(mr->VolStatus, "Append") == 0 ||
565 strcmp(mr->VolStatus, "Full") == 0 ||
566 strcmp(mr->VolStatus, "Used") == 0 ||
567 strcmp(mr->VolStatus, "Error") == 0) {
568 bstrncpy(mr->VolStatus, "Purged", sizeof(mr->VolStatus));
569 if (!db_update_media_record(jcr, ua->db, mr)) {
572 pm_strcpy(jcr->VolumeName, mr->VolumeName);
573 generate_job_event(jcr, "VolumePurged");
574 generate_plugin_event(jcr, bEventVolumePurged);
576 * If the RecyclePool is defined, move the volume there
578 if (mr->RecyclePoolId && mr->RecyclePoolId != mr->PoolId) {
579 POOL_DBR oldpr, newpr;
580 memset(&oldpr, 0, sizeof(POOL_DBR));
581 memset(&newpr, 0, sizeof(POOL_DBR));
582 newpr.PoolId = mr->RecyclePoolId;
583 oldpr.PoolId = mr->PoolId;
584 if ( db_get_pool_record(jcr, ua->db, &oldpr)
585 && db_get_pool_record(jcr, ua->db, &newpr))
587 /* check if destination pool size is ok */
588 if (newpr.MaxVols > 0 && newpr.NumVols >= newpr.MaxVols) {
589 ua->error_msg(_("Unable move recycled Volume in full "
590 "Pool \"%s\" MaxVols=%d\n"),
591 newpr.Name, newpr.MaxVols);
593 } else { /* move media */
594 update_vol_pool(ua, newpr.Name, mr, &oldpr);
597 ua->error_msg("%s", db_strerror(ua->db));
600 /* Send message to Job report, if it is a *real* job */
601 if (jcr && jcr->JobId > 0) {
602 Jmsg(jcr, M_INFO, 0, _("All records pruned from Volume \"%s\"; marking it \"Purged\"\n"),
607 ua->error_msg(_("Cannot purge Volume with VolStatus=%s\n"), mr->VolStatus);
609 return strcmp(mr->VolStatus, "Purged") == 0;