2 Bacula® - The Network Backup Solution
4 Copyright (C) 2002-2007 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 plus additions
11 that are listed in the file LICENSE.
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 John Walker.
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);
141 while ((i=find_arg(ua, NT_("volume"))) >= 0) {
142 if (select_media_dbr(ua, &mr)) {
143 purge_jobs_from_volume(ua, &mr);
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);
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 bsendmsg(ua, _("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 bsendmsg(ua, _("No Files found for client %s to purge from %s catalog.\n"),
209 client->name(), client->catalog->name());
211 bsendmsg(ua, _("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 bsendmsg(ua, _("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 bsendmsg(ua, _("No Files found for client %s to purge from %s catalog.\n"),
259 client->name(), client->catalog->name());
261 bsendmsg(ua, _("%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());
287 * Now mark Job as having files purged. This is necessary to
288 * avoid having too many Jobs to process in future prunings. If
289 * we don't do this, the number of JobId's in our in memory list
290 * could grow very large.
292 Mmsg(query, "UPDATE Job SET PurgedFiles=1 WHERE JobId IN (%s)", jobs);
293 db_sql_query(ua->db, query.c_str(), NULL, (void *)NULL);
294 Dmsg1(050, "Mark purged sql=%s\n", query.c_str());
298 * Delete jobs (all records) from the catalog in groups of 1000
301 void purge_job_list_from_catalog(UAContext *ua, del_ctx &del)
303 POOL_MEM jobids(PM_MESSAGE);
306 for (int i=0; del.num_ids; ) {
307 Dmsg1(150, "num_ids=%d\n", del.num_ids);
308 pm_strcat(jobids, "");
309 for (int j=0; j<1000 && del.num_ids>0; j++) {
311 if (del.JobId[i] == 0 || ua->jcr->JobId == del.JobId[i]) {
312 Dmsg2(150, "skip JobId[%d]=%d\n", i, (int)del.JobId[i]);
316 if (*jobids.c_str() != 0) {
317 pm_strcat(jobids, ",");
319 pm_strcat(jobids, edit_int64(del.JobId[i++], ed1));
320 Dmsg1(150, "Add id=%s\n", ed1);
323 Dmsg1(150, "num_ids=%d\n", del.num_ids);
324 purge_jobs_from_catalog(ua, jobids.c_str());
329 * Delete files from a list of jobs in groups of 1000
332 void purge_files_from_job_list(UAContext *ua, del_ctx &del)
334 POOL_MEM jobids(PM_MESSAGE);
337 * OK, now we have the list of JobId's to be pruned, send them
338 * off to be deleted batched 1000 at a time.
340 for (int i=0; del.num_ids; ) {
341 pm_strcat(jobids, "");
342 for (int j=0; j<1000 && del.num_ids>0; j++) {
344 if (del.JobId[i] == 0 || ua->jcr->JobId == del.JobId[i]) {
345 Dmsg2(150, "skip JobId[%d]=%d\n", i, (int)del.JobId[i]);
349 if (*jobids.c_str() != 0) {
350 pm_strcat(jobids, ",");
352 pm_strcat(jobids, edit_int64(del.JobId[i++], ed1));
353 Dmsg1(150, "Add id=%s\n", ed1);
356 purge_files_from_jobs(ua, jobids.c_str());
361 * Remove all records from catalog for a list of JobIds
363 void purge_jobs_from_catalog(UAContext *ua, char *jobs)
365 POOL_MEM query(PM_MESSAGE);
367 /* Delete (or purge) records associated with the job */
368 purge_files_from_jobs(ua, jobs);
370 Mmsg(query, "DELETE FROM JobMedia WHERE JobId IN (%s)", jobs);
371 db_sql_query(ua->db, query.c_str(), NULL, (void *)NULL);
372 Dmsg1(050, "Delete JobMedia sql=%s\n", query.c_str());
374 Mmsg(query, "DELETE FROM Log WHERE JobId IN (%s)", jobs);
375 db_sql_query(ua->db, query.c_str(), NULL, (void *)NULL);
376 Dmsg1(050, "Delete Log sql=%s\n", query.c_str());
378 /* Now remove the Job record itself */
379 Mmsg(query, "DELETE FROM Job WHERE JobId IN (%s)", jobs);
380 db_sql_query(ua->db, query.c_str(), NULL, (void *)NULL);
381 Dmsg1(050, "Delete Job sql=%s\n", query.c_str());
385 void purge_files_from_volume(UAContext *ua, MEDIA_DBR *mr )
386 {} /* ***FIXME*** implement */
389 * Returns: 1 if Volume purged
390 * 0 if Volume not purged
392 bool purge_jobs_from_volume(UAContext *ua, MEDIA_DBR *mr)
394 POOL_MEM query(PM_MESSAGE);
402 stat = strcmp(mr->VolStatus, "Append") == 0 ||
403 strcmp(mr->VolStatus, "Full") == 0 ||
404 strcmp(mr->VolStatus, "Used") == 0 ||
405 strcmp(mr->VolStatus, "Error") == 0;
408 bsendmsg(ua, _("Volume \"%s\" has VolStatus \"%s\" and cannot be purged.\n"
409 "The VolStatus must be: Append, Full, Used, or Error to be purged.\n"),
410 mr->VolumeName, mr->VolStatus);
414 memset(&jr, 0, sizeof(jr));
415 memset(&del, 0, sizeof(del));
417 del.JobId = (JobId_t *)malloc(sizeof(JobId_t) * del.max_ids);
420 * Check if he wants to purge a single jobid
422 i = find_arg_with_value(ua, "jobid");
425 del.JobId[0] = str_to_int64(ua->argv[i]);
430 Mmsg(query, "SELECT DISTINCT JobId FROM JobMedia WHERE MediaId=%s",
431 edit_int64(mr->MediaId, ed1));
432 if (!db_sql_query(ua->db, query.c_str(), file_delete_handler, (void *)&del)) {
433 bsendmsg(ua, "%s", db_strerror(ua->db));
434 Dmsg0(050, "Count failed\n");
439 purge_job_list_from_catalog(ua, del);
441 bsendmsg(ua, _("%d File%s on Volume \"%s\" purged from catalog.\n"), del.num_del,
442 del.num_del==1?"":"s", mr->VolumeName);
444 purged = is_volume_purged(ua, mr);
454 * This routine will check the JobMedia records to see if the
455 * Volume has been purged. If so, it marks it as such and
457 * Returns: true if volume purged
460 bool is_volume_purged(UAContext *ua, MEDIA_DBR *mr)
462 POOL_MEM query(PM_MESSAGE);
463 struct s_count_ctx cnt;
467 if (strcmp(mr->VolStatus, "Purged") == 0) {
471 /* If purged, mark it so */
473 Mmsg(query, "SELECT count(*) FROM JobMedia WHERE MediaId=%s",
474 edit_int64(mr->MediaId, ed1));
475 if (!db_sql_query(ua->db, query.c_str(), del_count_handler, (void *)&cnt)) {
476 bsendmsg(ua, "%s", db_strerror(ua->db));
477 Dmsg0(050, "Count failed\n");
481 if (cnt.count == 0) {
482 bsendmsg(ua, _("There are no more Jobs associated with Volume \"%s\". Marking it purged.\n"),
484 if (!(purged = mark_media_purged(ua, mr))) {
485 bsendmsg(ua, "%s", db_strerror(ua->db));
493 * IF volume status is Append, Full, Used, or Error, mark it Purged
494 * Purged volumes can then be recycled (if enabled).
496 bool mark_media_purged(UAContext *ua, MEDIA_DBR *mr)
499 if (strcmp(mr->VolStatus, "Append") == 0 ||
500 strcmp(mr->VolStatus, "Full") == 0 ||
501 strcmp(mr->VolStatus, "Used") == 0 ||
502 strcmp(mr->VolStatus, "Error") == 0) {
503 bstrncpy(mr->VolStatus, "Purged", sizeof(mr->VolStatus));
504 if (!db_update_media_record(jcr, ua->db, mr)) {
507 pm_strcpy(jcr->VolumeName, mr->VolumeName);
508 generate_job_event(jcr, "VolumePurged");
510 * If the RecyclePool is defined, move the volume there
512 if (mr->RecyclePoolId && mr->RecyclePoolId != mr->PoolId) {
513 POOL_DBR oldpr, newpr;
514 memset(&oldpr, 0, sizeof(POOL_DBR));
515 memset(&newpr, 0, sizeof(POOL_DBR));
516 newpr.PoolId = mr->RecyclePoolId;
517 oldpr.PoolId = mr->PoolId;
518 if ( db_get_pool_record(jcr, ua->db, &oldpr)
519 && db_get_pool_record(jcr, ua->db, &newpr))
521 /* check if destination pool size is ok */
522 if (newpr.MaxVols > 0 && newpr.NumVols >= newpr.MaxVols) {
523 bsendmsg(ua, _("Unable move recycled Volume in full "
524 "Pool \"%s\" MaxVols=%d\n"),
525 newpr.Name, newpr.MaxVols);
527 } else { /* move media */
528 update_vol_pool(ua, newpr.Name, mr, &oldpr);
531 bsendmsg(ua, "%s", db_strerror(ua->db));
534 /* Send message to Job report, if it is a *real* job */
535 if (jcr && jcr->JobId > 0) {
536 Jmsg1(jcr, M_INFO, 0, _("All records pruned from Volume \"%s\"; marking it \"Purged\"\n"),
541 bsendmsg(ua, _("Cannot purge Volume with VolStatus=%s\n"), mr->VolStatus);
543 return strcmp(mr->VolStatus, "Purged") == 0;