]> git.sur5r.net Git - bacula/bacula/blob - bacula/src/dird/ua_purge.c
Fix #1467 about ActionOnPurge with Devices having space
[bacula/bacula] / bacula / src / dird / ua_purge.c
1 /*
2    Bacula® - The Network Backup Solution
3
4    Copyright (C) 2002-2008 Free Software Foundation Europe e.V.
5
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
11    in the file LICENSE.
12
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.
17
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
21    02110-1301, USA.
22
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.
27 */
28 /*
29  *
30  *   Bacula Director -- User Agent Database Purge Command
31  *
32  *      Purges Files from specific JobIds
33  * or
34  *      Purges Jobs from Volumes
35  *
36  *     Kern Sibbald, February MMII
37  *
38  *   Version $Id$
39  */
40
41 #include "bacula.h"
42 #include "dird.h"
43
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
48 static const char *select_jobsfiles_from_client =
49    "SELECT JobId FROM Job "
50    "WHERE ClientId=%s "
51    "AND PurgedFiles=0";
52
53 static const char *select_jobs_from_client =
54    "SELECT JobId, PurgedFiles FROM Job "
55    "WHERE ClientId=%s";
56
57 /*
58  *   Purge records from database
59  *
60  *     Purge Files (from) [Job|JobId|Client|Volume]
61  *     Purge Jobs  (from) [Client|Volume]
62  *
63  *  N.B. Not all above is implemented yet.
64  */
65 int purgecmd(UAContext *ua, const char *cmd)
66 {
67    int i;
68    CLIENT *client;
69    MEDIA_DBR mr;
70    JOB_DBR  jr;
71    static const char *keywords[] = {
72       NT_("files"),
73       NT_("jobs"),
74       NT_("volume"),
75       NULL};
76
77    static const char *files_keywords[] = {
78       NT_("Job"),
79       NT_("JobId"),
80       NT_("Client"),
81       NT_("Volume"),
82       NULL};
83
84    static const char *jobs_keywords[] = {
85       NT_("Client"),
86       NT_("Volume"),
87       NULL};
88
89    ua->warning_msg(_(
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"));
96
97    if (!open_db(ua)) {
98       return 1;
99    }
100    switch (find_arg_keyword(ua, keywords)) {
101    /* Files */
102    case 0:
103       switch(find_arg_keyword(ua, files_keywords)) {
104       case 0:                         /* Job */
105       case 1:                         /* JobId */
106          if (get_job_dbr(ua, &jr)) {
107             char jobid[50];
108             edit_int64(jr.JobId, jobid);
109             purge_files_from_jobs(ua, jobid);
110          }
111          return 1;
112       case 2:                         /* client */
113          client = get_client_resource(ua);
114          if (client) {
115             purge_files_from_client(ua, client);
116          }
117          return 1;
118       case 3:                         /* Volume */
119          if (select_media_dbr(ua, &mr)) {
120             purge_files_from_volume(ua, &mr);
121          }
122          return 1;
123       }
124    /* Jobs */
125    case 1:
126       switch(find_arg_keyword(ua, jobs_keywords)) {
127       case 0:                         /* client */
128          client = get_client_resource(ua);
129          if (client) {
130             purge_jobs_from_client(ua, client);
131          }
132          return 1;
133       case 1:                         /* Volume */
134          if (select_media_dbr(ua, &mr)) {
135             purge_jobs_from_volume(ua, &mr, /*force*/true);
136          }
137          return 1;
138       }
139    /* Volume */
140    case 2:
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);
144          }
145          *ua->argk[i] = 0;            /* zap keyword already seen */
146          ua->send_msg("\n");
147       }
148       return 1;
149    default:
150       break;
151    }
152    switch (do_keyword_prompt(ua, _("Choose item to purge"), keywords)) {
153    case 0:                            /* files */
154       client = get_client_resource(ua);
155       if (client) {
156          purge_files_from_client(ua, client);
157       }
158       break;
159    case 1:                            /* jobs */
160       client = get_client_resource(ua);
161       if (client) {
162          purge_jobs_from_client(ua, client);
163       }
164       break;
165    case 2:                            /* Volume */
166       if (select_media_dbr(ua, &mr)) {
167          purge_jobs_from_volume(ua, &mr, /*force*/true);
168       }
169       break;
170    }
171    return 1;
172 }
173
174 /*
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.
181  */
182 static int purge_files_from_client(UAContext *ua, CLIENT *client)
183 {
184    struct del_ctx del;
185    POOL_MEM query(PM_MESSAGE);
186    CLIENT_DBR cr;
187    char ed1[50];
188
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)) {
192       return 0;
193    }
194
195    memset(&del, 0, sizeof(del));
196    del.max_ids = 1000;
197    del.JobId = (JobId_t *)malloc(sizeof(JobId_t) * del.max_ids);
198
199    ua->info_msg(_("Begin purging files for Client \"%s\"\n"), cr.Name);
200
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);
204
205    purge_files_from_job_list(ua, del);
206
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());
210    } else {
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());
213    }
214
215    if (del.JobId) {
216       free(del.JobId);
217    }
218    return 1;
219 }
220
221
222
223 /*
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.
229  */
230 static int purge_jobs_from_client(UAContext *ua, CLIENT *client)
231 {
232    struct del_ctx del;
233    POOL_MEM query(PM_MESSAGE);
234    CLIENT_DBR cr;
235    char ed1[50];
236
237    memset(&cr, 0, sizeof(cr));
238
239    bstrncpy(cr.Name, client->name(), sizeof(cr.Name));
240    if (!db_create_client_record(ua->jcr, ua->db, &cr)) {
241       return 0;
242    }
243
244    memset(&del, 0, sizeof(del));
245    del.max_ids = 1000;
246    del.JobId = (JobId_t *)malloc(sizeof(JobId_t) * del.max_ids);
247    del.PurgedFiles = (char *)malloc(del.max_ids);
248    
249    ua->info_msg(_("Begin purging jobs from Client \"%s\"\n"), cr.Name);
250
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);
254
255    purge_job_list_from_catalog(ua, del);
256
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());
260    } else {
261       ua->info_msg(_("%d Jobs for client %s purged from %s catalog.\n"), del.num_ids,
262          client->name(), client->catalog->name());
263    }
264
265    if (del.JobId) {
266       free(del.JobId);
267    }
268    if (del.PurgedFiles) {
269       free(del.PurgedFiles);
270    }
271    return 1;
272 }
273
274
275 /*
276  * Remove File records from a list of JobIds
277  */
278 void purge_files_from_jobs(UAContext *ua, char *jobs)
279 {
280    POOL_MEM query(PM_MESSAGE);
281
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());
285
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());
289
290    /*
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.
295     */
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());
299 }
300
301 /*
302  * Delete jobs (all records) from the catalog in groups of 1000
303  *  at a time.
304  */
305 void purge_job_list_from_catalog(UAContext *ua, del_ctx &del)
306 {
307    POOL_MEM jobids(PM_MESSAGE);
308    char ed1[50];
309
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++) {
314          del.num_ids--;
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]);
317             i++;
318             continue;
319          }
320          if (*jobids.c_str() != 0) {
321             pm_strcat(jobids, ",");
322          }
323          pm_strcat(jobids, edit_int64(del.JobId[i++], ed1));
324          Dmsg1(150, "Add id=%s\n", ed1);
325          del.num_del++;
326       }
327       Dmsg1(150, "num_ids=%d\n", del.num_ids);
328       purge_jobs_from_catalog(ua, jobids.c_str());
329    }
330 }
331
332 /*
333  * Delete files from a list of jobs in groups of 1000
334  *  at a time.
335  */
336 void purge_files_from_job_list(UAContext *ua, del_ctx &del)
337 {
338    POOL_MEM jobids(PM_MESSAGE);
339    char ed1[50];
340    /*
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.
343     */
344    for (int i=0; del.num_ids; ) {
345       pm_strcat(jobids, "");
346       for (int j=0; j<1000 && del.num_ids>0; j++) {
347          del.num_ids--;
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]);
350             i++;
351             continue;
352          }
353          if (*jobids.c_str() != 0) {
354             pm_strcat(jobids, ",");
355          }
356          pm_strcat(jobids, edit_int64(del.JobId[i++], ed1));
357          Dmsg1(150, "Add id=%s\n", ed1);
358          del.num_del++;
359       }
360       purge_files_from_jobs(ua, jobids.c_str());
361    }
362 }
363
364 /*
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.
369  *
370  *   JobId: 1   PriorJobId: 0    (original)
371  *   JobId: 2   PriorJobId: 1    (first copy)
372  *   JobId: 3   PriorJobId: 1    (second copy)
373  *
374  *   JobId: 2   PriorJobId: 1    (first copy, now regular backup)
375  *   JobId: 3   PriorJobId: 1    (second copy)
376  *
377  *  => Search through PriorJobId in jobid and
378  *                    PriorJobId in PriorJobId (jobid)
379  */
380 void upgrade_copies(UAContext *ua, char *jobs)
381 {
382    POOL_MEM query(PM_MESSAGE);
383    
384    db_lock(ua->db);
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 */
388                    "WHERE Type='%c' "
389                      "AND ( PriorJobId IN (%s) "
390                          "OR "
391                           " PriorJobId IN ( "
392                              "SELECT PriorJobId "
393                                "FROM Job "
394                               "WHERE JobId IN (%s) "
395                                " AND Type='B' "
396                             ") "
397                          ") "
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());
402
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 )");
406
407    db_sql_query(ua->db, query.c_str(), NULL, (void *)NULL);
408
409    Mmsg(query, "DROP TABLE cpy_tmp");
410    db_sql_query(ua->db, query.c_str(), NULL, (void *)NULL);
411
412    db_unlock(ua->db);
413 }
414
415 /*
416  * Remove all records from catalog for a list of JobIds
417  */
418 void purge_jobs_from_catalog(UAContext *ua, char *jobs)
419 {
420    POOL_MEM query(PM_MESSAGE);
421
422    /* Delete (or purge) records associated with the job */
423    purge_files_from_jobs(ua, jobs);
424
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());
428
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());
432
433    upgrade_copies(ua, jobs);
434
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);
438
439    Dmsg1(050, "Delete Job sql=%s\n", query.c_str());
440 }
441
442 void purge_files_from_volume(UAContext *ua, MEDIA_DBR *mr )
443 {} /* ***FIXME*** implement */
444
445 /*
446  * Returns: 1 if Volume purged
447  *          0 if Volume not purged
448  */
449 bool purge_jobs_from_volume(UAContext *ua, MEDIA_DBR *mr, bool force)
450 {
451    POOL_MEM query(PM_MESSAGE);
452    struct del_ctx del;
453    int i;
454    bool purged = false;
455    bool stat;
456    JOB_DBR jr;
457    char ed1[50];
458
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;
463    if (!stat) {
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);
467       return 0;
468    }
469
470    memset(&jr, 0, sizeof(jr));
471    memset(&del, 0, sizeof(del));
472    del.max_ids = 1000;
473    del.JobId = (JobId_t *)malloc(sizeof(JobId_t) * del.max_ids);
474
475    /*
476     * Check if he wants to purge a single jobid
477     */
478    i = find_arg_with_value(ua, "jobid");
479    if (i >= 0) {
480       del.num_ids = 1;
481       del.JobId[0] = str_to_int64(ua->argv[i]);
482    } else {
483       /*
484        * Purge ALL JobIds
485        */
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");
491          goto bail_out;
492       }
493    }
494
495    purge_job_list_from_catalog(ua, del);
496
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);
499
500    purged = is_volume_purged(ua, mr, force); 
501
502 bail_out:
503    if (del.JobId) {
504       free(del.JobId);
505    }
506    return purged;
507 }
508
509 /*
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
512  *
513  * Returns: true if volume purged
514  *          false if not
515  *
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).
521  */
522 bool is_volume_purged(UAContext *ua, MEDIA_DBR *mr, bool force)
523 {
524    POOL_MEM query(PM_MESSAGE);
525    struct s_count_ctx cnt;
526    bool purged = false;
527    char ed1[50];
528
529    if (!force && (mr->FirstWritten == 0 || mr->LastWritten == 0)) {
530       goto bail_out;               /* not written cannot purge */
531    }
532
533    if (strcmp(mr->VolStatus, "Purged") == 0) {
534       purged = true;
535       goto bail_out;
536    }
537
538    /* If purged, mark it so */
539    cnt.count = 0;
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");
545       goto bail_out;
546    }
547
548    if (cnt.count == 0) {
549       ua->warning_msg(_("There are no more Jobs associated with Volume \"%s\". Marking it purged.\n"),
550          mr->VolumeName);
551       if (!(purged = mark_media_purged(ua, mr))) {
552          ua->error_msg("%s", db_strerror(ua->db));
553       }
554    }
555 bail_out:
556    return purged;
557 }
558
559 static BSOCK *open_sd_bsock(UAContext *ua)
560 {
561    STORE *store = ua->jcr->wstore;
562
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"));
568          return NULL;
569       }
570    }
571    return ua->jcr->store_bsock;
572 }
573
574 /*
575  * IF volume status is Append, Full, Used, or Error, mark it Purged
576  *   Purged volumes can then be recycled (if enabled).
577  */
578 bool mark_media_purged(UAContext *ua, MEDIA_DBR *mr)
579 {
580    char dev_name[MAX_NAME_LENGTH];
581    JCR *jcr = ua->jcr;
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)) {
588          return false;
589       }
590
591       if (mr->ActionOnPurge > 0) {
592          /* Send the command to truncate the volume after purge. If this feature
593           * is disabled for the specific device, this will be a no-op.
594           */
595          BSOCK *sd;
596          if ((sd=open_sd_bsock(ua)) != NULL) {
597             bstrncpy(dev_name, ua->jcr->wstore->dev_name(), sizeof(dev_name));
598             bash_spaces(dev_name);
599             bash_spaces(mr->VolumeName);
600             sd->fsend("action_on_purge %s vol=%s action=%d",
601                       ua->jcr->wstore->dev_name(),
602                       mr->VolumeName,
603                       mr->ActionOnPurge);
604             unbash_spaces(mr->VolumeName);
605             while (sd->recv() >= 0) {
606                ua->send_msg("%s", sd->msg);
607             }
608
609             sd->signal(BNET_TERMINATE);
610             sd->close();
611             ua->jcr->store_bsock = NULL;
612          } else {
613             ua->error_msg(_("Could not connect to storage daemon"));
614             return false;
615          }
616       }
617
618       pm_strcpy(jcr->VolumeName, mr->VolumeName);
619       generate_job_event(jcr, "VolumePurged");
620       generate_plugin_event(jcr, bEventVolumePurged);
621       /*
622        * If the RecyclePool is defined, move the volume there
623        */
624       if (mr->RecyclePoolId && mr->RecyclePoolId != mr->PoolId) {
625          POOL_DBR oldpr, newpr;
626          memset(&oldpr, 0, sizeof(POOL_DBR));
627          memset(&newpr, 0, sizeof(POOL_DBR));
628          newpr.PoolId = mr->RecyclePoolId;
629          oldpr.PoolId = mr->PoolId;
630          if (   db_get_pool_record(jcr, ua->db, &oldpr) 
631              && db_get_pool_record(jcr, ua->db, &newpr)) 
632          {
633             /* check if destination pool size is ok */
634             if (newpr.MaxVols > 0 && newpr.NumVols >= newpr.MaxVols) {
635                ua->error_msg(_("Unable move recycled Volume in full " 
636                               "Pool \"%s\" MaxVols=%d\n"),
637                         newpr.Name, newpr.MaxVols);
638
639             } else {            /* move media */
640                update_vol_pool(ua, newpr.Name, mr, &oldpr);
641             }
642          } else {
643             ua->error_msg("%s", db_strerror(ua->db));
644          }
645       }
646       /* Send message to Job report, if it is a *real* job */           
647       if (jcr && jcr->JobId > 0) {
648          Jmsg(jcr, M_INFO, 0, _("All records pruned from Volume \"%s\"; marking it \"Purged\"\n"),
649             mr->VolumeName); 
650       }
651       return true;
652    } else {
653       ua->error_msg(_("Cannot purge Volume with VolStatus=%s\n"), mr->VolStatus);
654    }
655    return strcmp(mr->VolStatus, "Purged") == 0;
656 }