]> git.sur5r.net Git - bacula/bacula/blob - bacula/src/dird/ua_purge.c
kes Implement new prunning code that prunes up to 1000 jobs at
[bacula/bacula] / bacula / src / dird / ua_purge.c
1 /*
2  *
3  *   Bacula Director -- User Agent Database Purge Command
4  *
5  *      Purges Files from specific JobIds
6  * or
7  *      Purges Jobs from Volumes
8  *
9  *     Kern Sibbald, February MMII
10  *
11  *   Version $Id$
12  */
13 /*
14    Bacula® - The Network Backup Solution
15
16    Copyright (C) 2002-2007 Free Software Foundation Europe e.V.
17
18    The main author of Bacula is Kern Sibbald, with contributions from
19    many others, a complete list can be found in the file AUTHORS.
20    This program is Free Software; you can redistribute it and/or
21    modify it under the terms of version two of the GNU General Public
22    License as published by the Free Software Foundation plus additions
23    that are listed in the file LICENSE.
24
25    This program is distributed in the hope that it will be useful, but
26    WITHOUT ANY WARRANTY; without even the implied warranty of
27    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
28    General Public License for more details.
29
30    You should have received a copy of the GNU General Public License
31    along with this program; if not, write to the Free Software
32    Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
33    02110-1301, USA.
34
35    Bacula® is a registered trademark of John Walker.
36    The licensor of Bacula is the Free Software Foundation Europe
37    (FSFE), Fiduciary Program, Sumatrastrasse 25, 8006 Zürich,
38    Switzerland, email:ftf@fsfeurope.org.
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 #define MAX_DEL_LIST_LEN 1000000
49
50 static const char *select_jobsfiles_from_client =
51    "SELECT JobId FROM Job "
52    "WHERE ClientId=%s "
53    "AND PurgedFiles=0";
54
55 static const char *select_jobs_from_client =
56    "SELECT JobId, PurgedFiles FROM Job "
57    "WHERE ClientId=%s";
58
59
60 /* In memory list of JobIds */
61 struct s_file_del_ctx {
62    JobId_t *JobId;
63    int num_ids;                       /* ids stored */
64    int max_ids;                       /* size of array */
65    int num_del;                       /* number deleted */
66    int tot_ids;                       /* total to process */
67 };
68
69 struct s_job_del_ctx {
70    JobId_t *JobId;                    /* array of JobIds */
71    char *PurgedFiles;                 /* Array of PurgedFile flags */
72    int num_ids;                       /* ids stored */
73    int max_ids;                       /* size of array */
74    int num_del;                       /* number deleted */
75    int tot_ids;                       /* total to process */
76 };
77
78 struct s_count_ctx {
79    int count;
80 };
81
82 /*
83  * Called here to count entries to be deleted
84  */
85 static int count_handler(void *ctx, int num_fields, char **row)
86 {
87    struct s_count_ctx *cnt = (struct s_count_ctx *)ctx;
88
89    if (row[0]) {
90       cnt->count = str_to_int64(row[0]);
91    } else {
92       cnt->count = 0;
93    }
94    return 0;
95 }
96
97 /*
98  * Called here to count entries to be deleted
99  */
100 static int file_count_handler(void *ctx, int num_fields, char **row)
101 {
102    struct s_file_del_ctx *del = (struct s_file_del_ctx *)ctx;
103    del->tot_ids++;
104    return 0;
105 }
106
107
108 static int job_count_handler(void *ctx, int num_fields, char **row)
109 {
110    struct s_job_del_ctx *del = (struct s_job_del_ctx *)ctx;
111    del->tot_ids++;
112    return 0;
113 }
114
115
116 /*
117  * Called here to make in memory list of JobIds to be
118  *  deleted and the associated PurgedFiles flag.
119  *  The in memory list will then be transversed
120  *  to issue the SQL DELETE commands.  Note, the list
121  *  is allowed to get to MAX_DEL_LIST_LEN to limit the
122  *  maximum malloc'ed memory.
123  */
124 static int job_delete_handler(void *ctx, int num_fields, char **row)
125 {
126    struct s_job_del_ctx *del = (struct s_job_del_ctx *)ctx;
127
128    if (del->num_ids == MAX_DEL_LIST_LEN) {
129       return 1;
130    }
131    if (del->num_ids == del->max_ids) {
132       del->max_ids = (del->max_ids * 3) / 2;
133       del->JobId = (JobId_t *)brealloc(del->JobId, sizeof(JobId_t) * del->max_ids);
134       del->PurgedFiles = (char *)brealloc(del->PurgedFiles, del->max_ids);
135    }
136    del->JobId[del->num_ids] = (JobId_t)str_to_int64(row[0]);
137    del->PurgedFiles[del->num_ids++] = (char)str_to_int64(row[1]);
138    return 0;
139 }
140
141 static int file_delete_handler(void *ctx, int num_fields, char **row)
142 {
143    struct s_file_del_ctx *del = (struct s_file_del_ctx *)ctx;
144
145    if (del->num_ids == MAX_DEL_LIST_LEN) {
146       return 1;
147    }
148    if (del->num_ids == del->max_ids) {
149       del->max_ids = (del->max_ids * 3) / 2;
150       del->JobId = (JobId_t *)brealloc(del->JobId, sizeof(JobId_t) *
151          del->max_ids);
152    }
153    del->JobId[del->num_ids++] = (JobId_t)str_to_int64(row[0]);
154    return 0;
155 }
156
157 /*
158  *   Purge records from database
159  *
160  *     Purge Files (from) [Job|JobId|Client|Volume]
161  *     Purge Jobs  (from) [Client|Volume]
162  *
163  *  N.B. Not all above is implemented yet.
164  */
165 int purgecmd(UAContext *ua, const char *cmd)
166 {
167    int i;
168    CLIENT *client;
169    MEDIA_DBR mr;
170    JOB_DBR  jr;
171    static const char *keywords[] = {
172       NT_("files"),
173       NT_("jobs"),
174       NT_("volume"),
175       NULL};
176
177    static const char *files_keywords[] = {
178       NT_("Job"),
179       NT_("JobId"),
180       NT_("Client"),
181       NT_("Volume"),
182       NULL};
183
184    static const char *jobs_keywords[] = {
185       NT_("Client"),
186       NT_("Volume"),
187       NULL};
188
189    bsendmsg(ua, _(
190       "\nThis command is can be DANGEROUS!!!\n\n"
191       "It purges (deletes) all Files from a Job,\n"
192       "JobId, Client or Volume; or it purges (deletes)\n"
193       "all Jobs from a Client or Volume without regard\n"
194       "for retention periods. Normally you should use the\n"
195       "PRUNE command, which respects retention periods.\n"));
196
197    if (!open_db(ua)) {
198       return 1;
199    }
200    switch (find_arg_keyword(ua, keywords)) {
201    /* Files */
202    case 0:
203       switch(find_arg_keyword(ua, files_keywords)) {
204       case 0:                         /* Job */
205       case 1:                         /* JobId */
206          if (get_job_dbr(ua, &jr)) {
207             purge_files_from_job(ua, jr.JobId);
208          }
209          return 1;
210       case 2:                         /* client */
211          client = get_client_resource(ua);
212          if (client) {
213             purge_files_from_client(ua, client);
214          }
215          return 1;
216       case 3:                         /* Volume */
217          if (select_media_dbr(ua, &mr)) {
218             purge_files_from_volume(ua, &mr);
219          }
220          return 1;
221       }
222    /* Jobs */
223    case 1:
224       switch(find_arg_keyword(ua, jobs_keywords)) {
225       case 0:                         /* client */
226          client = get_client_resource(ua);
227          if (client) {
228             purge_jobs_from_client(ua, client);
229          }
230          return 1;
231       case 1:                         /* Volume */
232          if (select_media_dbr(ua, &mr)) {
233             purge_jobs_from_volume(ua, &mr);
234          }
235          return 1;
236       }
237    /* Volume */
238    case 2:
239       while ((i=find_arg(ua, NT_("volume"))) >= 0) {
240          if (select_media_dbr(ua, &mr)) {
241             purge_jobs_from_volume(ua, &mr);
242          }
243          *ua->argk[i] = 0;            /* zap keyword already seen */
244          bsendmsg(ua, "\n");
245       }
246       return 1;
247    default:
248       break;
249    }
250    switch (do_keyword_prompt(ua, _("Choose item to purge"), keywords)) {
251    case 0:                            /* files */
252       client = get_client_resource(ua);
253       if (client) {
254          purge_files_from_client(ua, client);
255       }
256       break;
257    case 1:                            /* jobs */
258       client = get_client_resource(ua);
259       if (client) {
260          purge_jobs_from_client(ua, client);
261       }
262       break;
263    case 2:                            /* Volume */
264       if (select_media_dbr(ua, &mr)) {
265          purge_jobs_from_volume(ua, &mr);
266       }
267       break;
268    }
269    return 1;
270 }
271
272 /*
273  * Purge File records from the database. For any Job which
274  * is older than the retention period, we unconditionally delete
275  * all File records for that Job.  This is simple enough that no
276  * temporary tables are needed. We simply make an in memory list of
277  * the JobIds meeting the prune conditions, then delete all File records
278  * pointing to each of those JobIds.
279  */
280 static int purge_files_from_client(UAContext *ua, CLIENT *client)
281 {
282    struct s_file_del_ctx del;
283    POOLMEM *query = get_pool_memory(PM_MESSAGE);
284    int i;
285    CLIENT_DBR cr;
286    char ed1[50];
287
288    memset(&cr, 0, sizeof(cr));
289    memset(&del, 0, sizeof(del));
290
291    bstrncpy(cr.Name, client->name(), sizeof(cr.Name));
292    if (!db_create_client_record(ua->jcr, ua->db, &cr)) {
293       return 0;
294    }
295    bsendmsg(ua, _("Begin purging files for Client \"%s\"\n"), cr.Name);
296    Mmsg(query, select_jobsfiles_from_client, edit_int64(cr.ClientId, ed1));
297
298    Dmsg1(050, "select sql=%s\n", query);
299
300    if (!db_sql_query(ua->db, query, file_count_handler, (void *)&del)) {
301       bsendmsg(ua, "%s", db_strerror(ua->db));
302       Dmsg0(050, "Count failed\n");
303       goto bail_out;
304    }
305
306    if (del.tot_ids == 0) {
307       bsendmsg(ua, _("No Files found for client %s to purge from %s catalog.\n"),
308          client->name(), client->catalog->name());
309       goto bail_out;
310    }
311
312    if (del.tot_ids < MAX_DEL_LIST_LEN) {
313       del.max_ids = del.tot_ids + 1;
314    } else {
315       del.max_ids = MAX_DEL_LIST_LEN;
316    }
317    del.tot_ids = 0;
318
319    del.JobId = (JobId_t *)malloc(sizeof(JobId_t) * del.max_ids);
320
321    db_sql_query(ua->db, query, file_delete_handler, (void *)&del);
322
323    for (i=0; i < del.num_ids; i++) {
324       purge_files_from_job(ua, del.JobId[i]);
325    }
326    bsendmsg(ua, _("%d Files for client \"%s\" purged from %s catalog.\n"), del.num_ids,
327       client->name(), client->catalog->name());
328
329 bail_out:
330    if (del.JobId) {
331       free(del.JobId);
332    }
333    free_pool_memory(query);
334    return 1;
335 }
336
337
338
339 /*
340  * Purge Job records from the database. For any Job which
341  * is older than the retention period, we unconditionally delete
342  * it and all File records for that Job.  This is simple enough that no
343  * temporary tables are needed. We simply make an in memory list of
344  * the JobIds then delete the Job, Files, and JobMedia records in that list.
345  */
346 static int purge_jobs_from_client(UAContext *ua, CLIENT *client)
347 {
348    struct s_job_del_ctx del;
349    POOLMEM *query = get_pool_memory(PM_MESSAGE);
350    int i;
351    CLIENT_DBR cr;
352    char ed1[50];
353
354    memset(&cr, 0, sizeof(cr));
355    memset(&del, 0, sizeof(del));
356
357    bstrncpy(cr.Name, client->name(), sizeof(cr.Name));
358    if (!db_create_client_record(ua->jcr, ua->db, &cr)) {
359       return 0;
360    }
361
362    bsendmsg(ua, _("Begin purging jobs from Client \"%s\"\n"), cr.Name);
363    Mmsg(query, select_jobs_from_client, edit_int64(cr.ClientId, ed1));
364
365    Dmsg1(050, "select sql=%s\n", query);
366
367    if (!db_sql_query(ua->db, query, job_count_handler, (void *)&del)) {
368       bsendmsg(ua, "%s", db_strerror(ua->db));
369       Dmsg0(050, "Count failed\n");
370       goto bail_out;
371    }
372    if (del.tot_ids == 0) {
373       bsendmsg(ua, _("No Jobs found for client %s to purge from %s catalog.\n"),
374          client->name(), client->catalog->name());
375       goto bail_out;
376    }
377
378    if (del.tot_ids < MAX_DEL_LIST_LEN) {
379       del.max_ids = del.tot_ids + 1;
380    } else {
381       del.max_ids = MAX_DEL_LIST_LEN;
382    }
383
384    del.tot_ids = 0;
385
386    del.JobId = (JobId_t *)malloc(sizeof(JobId_t) * del.max_ids);
387    del.PurgedFiles = (char *)malloc(del.max_ids);
388
389    db_sql_query(ua->db, query, job_delete_handler, (void *)&del);
390
391    /*
392     * OK, now we have the list of JobId's to be purged, first check
393     * if the Files have been purged, if not, purge (delete) them.
394     * Then delete the Job entry, and finally and JobMedia records.
395     */
396    for (i=0; i < del.num_ids; i++) {
397       Dmsg1(050, "Delete Files JobId=%s\n", ed1); 
398       if (!del.PurgedFiles[i]) {
399          purge_files_from_job(ua, del.JobId[i]);
400       }
401       purge_job_from_catalog(ua, del.JobId[i]);
402    }
403    bsendmsg(ua, _("%d Jobs for client %s purged from %s catalog.\n"), del.num_ids,
404       client->name(), client->catalog->name());
405
406 bail_out:
407    if (del.JobId) {
408       free(del.JobId);
409    }
410    if (del.PurgedFiles) {
411       free(del.PurgedFiles);
412    }
413    free_pool_memory(query);
414    return 1;
415 }
416
417 void purge_job_from_catalog(UAContext *ua, JobId_t JobId)
418 {
419    POOL_MEM query(PM_MESSAGE);
420    char ed1[50];
421
422    /* Delete (or purge) records associated with the job */
423    purge_job_records_from_catalog(ua, JobId);
424
425    /* Now remove the Job record itself */
426    edit_int64(JobId, ed1);
427    Mmsg(query, "DELETE FROM Job WHERE JobId=%s", ed1);
428    db_sql_query(ua->db, query.c_str(), NULL, (void *)NULL);
429    Dmsg1(050, "Delete Job sql=%s\n", query.c_str());
430 }
431
432 /*
433  * This removes all the records associated with a Job from
434  *  the catalog (i.e. prunes it) without removing the Job
435  *  record itself.
436  */
437 void purge_job_records_from_catalog(UAContext *ua, JobId_t JobId)
438 {
439    POOL_MEM query(PM_MESSAGE);
440    char ed1[50];
441
442    purge_files_from_job(ua, JobId);
443
444    edit_int64(JobId, ed1);
445    Mmsg(query, "DELETE FROM JobMedia WHERE JobId=%s", ed1);
446    db_sql_query(ua->db, query.c_str(), NULL, (void *)NULL);
447    Dmsg1(050, "Delete JobMedia sql=%s\n", query.c_str());
448
449    Mmsg(query, "DELETE FROM Log WHERE JobId=%s", ed1);
450    db_sql_query(ua->db, query.c_str(), NULL, (void *)NULL);
451    Dmsg1(050, "Delete Log sql=%s\n", query.c_str());
452
453 }
454
455 /*
456  * Remove File records for a particular Job.
457  */
458 void purge_files_from_job(UAContext *ua, JobId_t JobId)
459 {
460    POOL_MEM query(PM_MESSAGE);
461    char ed1[50];
462
463    edit_int64(JobId, ed1);
464    Mmsg(query, del_File, ed1);
465    db_sql_query(ua->db, query.c_str(), NULL, (void *)NULL);
466
467    /*
468     * Now mark Job as having files purged. This is necessary to
469     * avoid having too many Jobs to process in future prunings. If
470     * we don't do this, the number of JobId's in our in memory list
471     * could grow very large.
472     */
473    Mmsg(query, upd_Purged, ed1);
474    db_sql_query(ua->db, query.c_str(), NULL, (void *)NULL);
475 }
476
477 void purge_jobs_from_catalog(UAContext *ua, char *jobs)
478 {
479    POOL_MEM query(PM_MESSAGE);
480
481    /* Delete (or purge) records associated with the job */
482    Mmsg(query, "DELETE FROM File WHERE JobId IN (%s)", jobs);
483    db_sql_query(ua->db, query.c_str(), NULL, (void *)NULL);
484    Dmsg1(050, "Delete File sql=%s\n", query.c_str());
485
486    Mmsg(query, "DELETE FROM JobMedia WHERE JobId IN (%s)", jobs);
487    db_sql_query(ua->db, query.c_str(), NULL, (void *)NULL);
488    Dmsg1(050, "Delete JobMedia sql=%s\n", query.c_str());
489
490    Mmsg(query, "DELETE FROM Log WHERE JobId IN (%s)", jobs);
491    db_sql_query(ua->db, query.c_str(), NULL, (void *)NULL);
492    Dmsg1(050, "Delete Log sql=%s\n", query.c_str());
493
494    /* Now remove the Job record itself */
495    Mmsg(query, "DELETE FROM Job WHERE JobId IN (%s)", jobs);
496    db_sql_query(ua->db, query.c_str(), NULL, (void *)NULL);
497    Dmsg1(050, "Delete Job sql=%s\n", query.c_str());
498
499    /*
500     * Now mark Job as having files purged. This is necessary to
501     * avoid having too many Jobs to process in future prunings. If
502     * we don't do this, the number of JobId's in our in memory list
503     * could grow very large.
504     */
505    Mmsg(query, "UPDATE Job SET PurgedFiles=1 WHERE JobId IN (%s)", jobs);
506    db_sql_query(ua->db, query.c_str(), NULL, (void *)NULL);
507 }
508
509
510 void purge_files_from_volume(UAContext *ua, MEDIA_DBR *mr )
511 {} /* ***FIXME*** implement */
512
513 /*
514  * Returns: 1 if Volume purged
515  *          0 if Volume not purged
516  */
517 int purge_jobs_from_volume(UAContext *ua, MEDIA_DBR *mr)
518 {
519    POOLMEM *query = get_pool_memory(PM_MESSAGE);
520    struct s_count_ctx cnt;
521    struct s_file_del_ctx del;
522    int i, stat = 0;
523    JOB_DBR jr;
524    char ed1[50];
525
526    stat = strcmp(mr->VolStatus, "Append") == 0 ||
527           strcmp(mr->VolStatus, "Full")   == 0 ||
528           strcmp(mr->VolStatus, "Used")   == 0 ||
529           strcmp(mr->VolStatus, "Error")  == 0;
530    if (!stat) {
531       bsendmsg(ua, "\n");
532       bsendmsg(ua, _("Volume \"%s\" has VolStatus \"%s\" and cannot be purged.\n"
533                      "The VolStatus must be: Append, Full, Used, or Error to be purged.\n"),
534                      mr->VolumeName, mr->VolStatus);
535       goto bail_out;
536    }
537
538    memset(&jr, 0, sizeof(jr));
539    memset(&del, 0, sizeof(del));
540    cnt.count = 0;
541    Mmsg(query, "SELECT count(*) FROM JobMedia WHERE MediaId=%s", 
542         edit_int64(mr->MediaId, ed1));
543    if (!db_sql_query(ua->db, query, count_handler, (void *)&cnt)) {
544       bsendmsg(ua, "%s", db_strerror(ua->db));
545       Dmsg0(050, "Count failed\n");
546       goto bail_out;
547    }
548
549    if (cnt.count == 0) {
550       bsendmsg(ua, _("There are no Jobs associated with Volume \"%s\". Marking it purged.\n"),
551          mr->VolumeName);
552       if (!mark_media_purged(ua, mr)) {
553          bsendmsg(ua, "%s", db_strerror(ua->db));
554          goto bail_out;
555       }
556       goto bail_out;
557    }
558
559    if (cnt.count < MAX_DEL_LIST_LEN) {
560       del.max_ids = cnt.count + 1;
561    } else {
562       del.max_ids = MAX_DEL_LIST_LEN;
563    }
564
565    /*
566     * Check if he wants to purge a single jobid
567     */
568    i = find_arg_with_value(ua, "jobid");
569    if (i >= 0) {
570       del.JobId = (JobId_t *)malloc(sizeof(JobId_t));
571       del.num_ids = 1;
572       del.JobId[0] = str_to_int64(ua->argv[i]);
573    } else {
574       /*
575        * Purge ALL JobIds
576        */
577       del.JobId = (JobId_t *)malloc(sizeof(JobId_t) * del.max_ids);
578
579       Mmsg(query, "SELECT JobId FROM JobMedia WHERE MediaId=%s", 
580            edit_int64(mr->MediaId, ed1));
581       if (!db_sql_query(ua->db, query, file_delete_handler, (void *)&del)) {
582          bsendmsg(ua, "%s", db_strerror(ua->db));
583          Dmsg0(050, "Count failed\n");
584          goto bail_out;
585       }
586    }
587
588    for (i=0; i < del.num_ids; i++) {
589       purge_files_from_job(ua, del.JobId[i]);
590       purge_job_from_catalog(ua, del.JobId[i]);
591       del.num_del++;
592    }
593    if (del.JobId) {
594       free(del.JobId);
595    }
596    bsendmsg(ua, _("%d File%s on Volume \"%s\" purged from catalog.\n"), del.num_del,
597       del.num_del==1?"":"s", mr->VolumeName);
598
599    /* If purged, mark it so */
600    cnt.count = 0;
601    Mmsg(query, "SELECT count(*) FROM JobMedia WHERE MediaId=%s", 
602         edit_int64(mr->MediaId, ed1));
603    if (!db_sql_query(ua->db, query, count_handler, (void *)&cnt)) {
604       bsendmsg(ua, "%s", db_strerror(ua->db));
605       Dmsg0(050, "Count failed\n");
606       goto bail_out;
607    }
608
609    if (cnt.count == 0) {
610       bsendmsg(ua, _("There are no more Jobs associated with Volume \"%s\". Marking it purged.\n"),
611          mr->VolumeName);
612       if (!(stat = mark_media_purged(ua, mr))) {
613          bsendmsg(ua, "%s", db_strerror(ua->db));
614          goto bail_out;
615       }
616    }
617
618 bail_out:
619    free_pool_memory(query);
620    return stat;
621 }
622
623 /*
624  * IF volume status is Append, Full, Used, or Error, mark it Purged
625  *   Purged volumes can then be recycled (if enabled).
626  */
627 bool mark_media_purged(UAContext *ua, MEDIA_DBR *mr)
628 {
629    JCR *jcr = ua->jcr;
630    if (strcmp(mr->VolStatus, "Append") == 0 ||
631        strcmp(mr->VolStatus, "Full")   == 0 ||
632        strcmp(mr->VolStatus, "Used")   == 0 ||
633        strcmp(mr->VolStatus, "Error")  == 0) {
634       bstrncpy(mr->VolStatus, "Purged", sizeof(mr->VolStatus));
635       if (!db_update_media_record(jcr, ua->db, mr)) {
636          return false;
637       }
638       pm_strcpy(jcr->VolumeName, mr->VolumeName);
639       generate_job_event(jcr, "VolumePurged");
640       /*
641        * If the RecyclePool is defined, move the volume there
642        */
643       if (mr->RecyclePoolId && mr->RecyclePoolId != mr->PoolId) {
644          POOL_DBR oldpr, newpr;
645          memset(&oldpr, 0, sizeof(POOL_DBR));
646          memset(&newpr, 0, sizeof(POOL_DBR));
647          newpr.PoolId = mr->RecyclePoolId;
648          oldpr.PoolId = mr->PoolId;
649          if (   db_get_pool_record(jcr, ua->db, &oldpr) 
650              && db_get_pool_record(jcr, ua->db, &newpr)) 
651          {
652             /* check if destination pool size is ok */
653             if (newpr.MaxVols > 0 && newpr.NumVols >= newpr.MaxVols) {
654                bsendmsg(ua, _("Unable move recycled Volume in full " 
655                               "Pool \"%s\" MaxVols=%d\n"),
656                         newpr.Name, newpr.MaxVols);
657
658             } else {            /* move media */
659                update_vol_pool(ua, newpr.Name, mr, &oldpr);
660             }
661          } else {
662             bsendmsg(ua, "%s", db_strerror(ua->db));
663          }
664       }
665       /* Send message to Job report, if it is a *real* job */           
666       if (jcr && jcr->JobId > 0) {
667          Jmsg1(jcr, M_INFO, 0, _("All records pruned from Volume \"%s\"; marking it \"Purged\"\n"),
668             mr->VolumeName); 
669       }
670       return true;
671    } else {
672       bsendmsg(ua, _("Cannot purge Volume with VolStatus=%s\n"), mr->VolStatus);
673    }
674    return strcmp(mr->VolStatus, "Purged") == 0;
675 }