]> git.sur5r.net Git - bacula/bacula/blob - bacula/src/dird/ua_purge.c
Switch from GPLv2 to AGPLv3
[bacula/bacula] / bacula / src / dird / ua_purge.c
1 /*
2    Bacula® - The Network Backup Solution
3
4    Copyright (C) 2002-2010 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 three of the GNU Affero 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 Affero 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  */
39
40 #include "bacula.h"
41 #include "dird.h"
42
43 /* Forward referenced functions */
44 static int purge_files_from_client(UAContext *ua, CLIENT *client);
45 static int purge_jobs_from_client(UAContext *ua, CLIENT *client);
46 static int action_on_purge_cmd(UAContext *ua, const char *cmd);
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       /* Perform ActionOnPurge (action=truncate) */
142       if (find_arg(ua, "action") >= 0) {
143          return action_on_purge_cmd(ua, ua->cmd);
144       }
145
146       while ((i=find_arg(ua, NT_("volume"))) >= 0) {
147          if (select_media_dbr(ua, &mr)) {
148             purge_jobs_from_volume(ua, &mr, /*force*/true);
149          }
150          *ua->argk[i] = 0;            /* zap keyword already seen */
151          ua->send_msg("\n");
152       }
153       return 1;
154    default:
155       break;
156    }
157    switch (do_keyword_prompt(ua, _("Choose item to purge"), keywords)) {
158    case 0:                            /* files */
159       client = get_client_resource(ua);
160       if (client) {
161          purge_files_from_client(ua, client);
162       }
163       break;
164    case 1:                            /* jobs */
165       client = get_client_resource(ua);
166       if (client) {
167          purge_jobs_from_client(ua, client);
168       }
169       break;
170    case 2:                            /* Volume */
171       if (select_media_dbr(ua, &mr)) {
172          purge_jobs_from_volume(ua, &mr, /*force*/true);
173       }
174       break;
175    }
176    return 1;
177 }
178
179 /*
180  * Purge File records from the database. For any Job which
181  * is older than the retention period, we unconditionally delete
182  * all File records for that Job.  This is simple enough that no
183  * temporary tables are needed. We simply make an in memory list of
184  * the JobIds meeting the prune conditions, then delete all File records
185  * pointing to each of those JobIds.
186  */
187 static int purge_files_from_client(UAContext *ua, CLIENT *client)
188 {
189    struct del_ctx del;
190    POOL_MEM query(PM_MESSAGE);
191    CLIENT_DBR cr;
192    char ed1[50];
193
194    memset(&cr, 0, sizeof(cr));
195    bstrncpy(cr.Name, client->name(), sizeof(cr.Name));
196    if (!db_create_client_record(ua->jcr, ua->db, &cr)) {
197       return 0;
198    }
199
200    memset(&del, 0, sizeof(del));
201    del.max_ids = 1000;
202    del.JobId = (JobId_t *)malloc(sizeof(JobId_t) * del.max_ids);
203
204    ua->info_msg(_("Begin purging files for Client \"%s\"\n"), cr.Name);
205
206    Mmsg(query, select_jobsfiles_from_client, edit_int64(cr.ClientId, ed1));
207    Dmsg1(050, "select sql=%s\n", query.c_str());
208    db_sql_query(ua->db, query.c_str(), file_delete_handler, (void *)&del);
209
210    purge_files_from_job_list(ua, del);
211
212    if (del.num_ids == 0) {
213       ua->warning_msg(_("No Files found for client %s to purge from %s catalog.\n"),
214          client->name(), client->catalog->name());
215    } else {
216       ua->info_msg(_("Files for %d Jobs for client \"%s\" purged from %s catalog.\n"), del.num_ids,
217          client->name(), client->catalog->name());
218    }
219
220    if (del.JobId) {
221       free(del.JobId);
222    }
223    return 1;
224 }
225
226
227
228 /*
229  * Purge Job records from the database. For any Job which
230  * is older than the retention period, we unconditionally delete
231  * it and all File records for that Job.  This is simple enough that no
232  * temporary tables are needed. We simply make an in memory list of
233  * the JobIds then delete the Job, Files, and JobMedia records in that list.
234  */
235 static int purge_jobs_from_client(UAContext *ua, CLIENT *client)
236 {
237    struct del_ctx del;
238    POOL_MEM query(PM_MESSAGE);
239    CLIENT_DBR cr;
240    char ed1[50];
241
242    memset(&cr, 0, sizeof(cr));
243
244    bstrncpy(cr.Name, client->name(), sizeof(cr.Name));
245    if (!db_create_client_record(ua->jcr, ua->db, &cr)) {
246       return 0;
247    }
248
249    memset(&del, 0, sizeof(del));
250    del.max_ids = 1000;
251    del.JobId = (JobId_t *)malloc(sizeof(JobId_t) * del.max_ids);
252    del.PurgedFiles = (char *)malloc(del.max_ids);
253    
254    ua->info_msg(_("Begin purging jobs from Client \"%s\"\n"), cr.Name);
255
256    Mmsg(query, select_jobs_from_client, edit_int64(cr.ClientId, ed1));
257    Dmsg1(150, "select sql=%s\n", query.c_str());
258    db_sql_query(ua->db, query.c_str(), job_delete_handler, (void *)&del);
259
260    purge_job_list_from_catalog(ua, del);
261
262    if (del.num_ids == 0) {
263       ua->warning_msg(_("No Files found for client %s to purge from %s catalog.\n"),
264          client->name(), client->catalog->name());
265    } else {
266       ua->info_msg(_("%d Jobs for client %s purged from %s catalog.\n"), del.num_ids,
267          client->name(), client->catalog->name());
268    }
269
270    if (del.JobId) {
271       free(del.JobId);
272    }
273    if (del.PurgedFiles) {
274       free(del.PurgedFiles);
275    }
276    return 1;
277 }
278
279
280 /*
281  * Remove File records from a list of JobIds
282  */
283 void purge_files_from_jobs(UAContext *ua, char *jobs)
284 {
285    POOL_MEM query(PM_MESSAGE);
286
287    Mmsg(query, "DELETE FROM File WHERE JobId IN (%s)", jobs);
288    db_sql_query(ua->db, query.c_str(), NULL, (void *)NULL);
289    Dmsg1(050, "Delete File sql=%s\n", query.c_str());
290
291    Mmsg(query, "DELETE FROM BaseFiles WHERE JobId IN (%s)", jobs);
292    db_sql_query(ua->db, query.c_str(), NULL, (void *)NULL);
293    Dmsg1(050, "Delete BaseFiles sql=%s\n", query.c_str());
294
295    /*
296     * Now mark Job as having files purged. This is necessary to
297     * avoid having too many Jobs to process in future prunings. If
298     * we don't do this, the number of JobId's in our in memory list
299     * could grow very large.
300     */
301    Mmsg(query, "UPDATE Job SET PurgedFiles=1 WHERE JobId IN (%s)", jobs);
302    db_sql_query(ua->db, query.c_str(), NULL, (void *)NULL);
303    Dmsg1(050, "Mark purged sql=%s\n", query.c_str());
304 }
305
306 /*
307  * Delete jobs (all records) from the catalog in groups of 1000
308  *  at a time.
309  */
310 void purge_job_list_from_catalog(UAContext *ua, del_ctx &del)
311 {
312    POOL_MEM jobids(PM_MESSAGE);
313    char ed1[50];
314
315    for (int i=0; del.num_ids; ) {
316       Dmsg1(150, "num_ids=%d\n", del.num_ids);
317       pm_strcat(jobids, "");
318       for (int j=0; j<1000 && del.num_ids>0; j++) {
319          del.num_ids--;
320          if (del.JobId[i] == 0 || ua->jcr->JobId == del.JobId[i]) {
321             Dmsg2(150, "skip JobId[%d]=%d\n", i, (int)del.JobId[i]);
322             i++;
323             continue;
324          }
325          if (*jobids.c_str() != 0) {
326             pm_strcat(jobids, ",");
327          }
328          pm_strcat(jobids, edit_int64(del.JobId[i++], ed1));
329          Dmsg1(150, "Add id=%s\n", ed1);
330          del.num_del++;
331       }
332       Dmsg1(150, "num_ids=%d\n", del.num_ids);
333       purge_jobs_from_catalog(ua, jobids.c_str());
334    }
335 }
336
337 /*
338  * Delete files from a list of jobs in groups of 1000
339  *  at a time.
340  */
341 void purge_files_from_job_list(UAContext *ua, del_ctx &del)
342 {
343    POOL_MEM jobids(PM_MESSAGE);
344    char ed1[50];
345    /*
346     * OK, now we have the list of JobId's to be pruned, send them
347     *   off to be deleted batched 1000 at a time.
348     */
349    for (int i=0; del.num_ids; ) {
350       pm_strcat(jobids, "");
351       for (int j=0; j<1000 && del.num_ids>0; j++) {
352          del.num_ids--;
353          if (del.JobId[i] == 0 || ua->jcr->JobId == del.JobId[i]) {
354             Dmsg2(150, "skip JobId[%d]=%d\n", i, (int)del.JobId[i]);
355             i++;
356             continue;
357          }
358          if (*jobids.c_str() != 0) {
359             pm_strcat(jobids, ",");
360          }
361          pm_strcat(jobids, edit_int64(del.JobId[i++], ed1));
362          Dmsg1(150, "Add id=%s\n", ed1);
363          del.num_del++;
364       }
365       purge_files_from_jobs(ua, jobids.c_str());
366    }
367 }
368
369 /*
370  * Change the type of the next copy job to backup.
371  * We need to upgrade the next copy of a normal job,
372  * and also upgrade the next copy when the normal job
373  * already have been purged.
374  *
375  *   JobId: 1   PriorJobId: 0    (original)
376  *   JobId: 2   PriorJobId: 1    (first copy)
377  *   JobId: 3   PriorJobId: 1    (second copy)
378  *
379  *   JobId: 2   PriorJobId: 1    (first copy, now regular backup)
380  *   JobId: 3   PriorJobId: 1    (second copy)
381  *
382  *  => Search through PriorJobId in jobid and
383  *                    PriorJobId in PriorJobId (jobid)
384  */
385 void upgrade_copies(UAContext *ua, char *jobs)
386 {
387    POOL_MEM query(PM_MESSAGE);
388    
389    db_lock(ua->db);
390
391    /* Do it in two times for mysql */
392    Mmsg(query, uap_upgrade_copies_oldest_job[db_type], JT_JOB_COPY, jobs, jobs);
393    db_sql_query(ua->db, query.c_str(), NULL, (void *)NULL);
394    Dmsg1(050, "Upgrade copies Log sql=%s\n", query.c_str());
395
396    /* Now upgrade first copy to Backup */
397    Mmsg(query, "UPDATE Job SET Type='B' "      /* JT_JOB_COPY => JT_BACKUP  */
398                 "WHERE JobId IN ( SELECT JobId FROM cpy_tmp )");
399
400    db_sql_query(ua->db, query.c_str(), NULL, (void *)NULL);
401
402    Mmsg(query, "DROP TABLE cpy_tmp");
403    db_sql_query(ua->db, query.c_str(), NULL, (void *)NULL);
404
405    db_unlock(ua->db);
406 }
407
408 /*
409  * Remove all records from catalog for a list of JobIds
410  */
411 void purge_jobs_from_catalog(UAContext *ua, char *jobs)
412 {
413    POOL_MEM query(PM_MESSAGE);
414
415    /* Delete (or purge) records associated with the job */
416    purge_files_from_jobs(ua, jobs);
417
418    Mmsg(query, "DELETE FROM JobMedia WHERE JobId IN (%s)", jobs);
419    db_sql_query(ua->db, query.c_str(), NULL, (void *)NULL);
420    Dmsg1(050, "Delete JobMedia sql=%s\n", query.c_str());
421
422    Mmsg(query, "DELETE FROM Log WHERE JobId IN (%s)", jobs);
423    db_sql_query(ua->db, query.c_str(), NULL, (void *)NULL);
424    Dmsg1(050, "Delete Log sql=%s\n", query.c_str());
425
426    Mmsg(query, "DELETE FROM RestoreObject WHERE JobId IN (%s)", jobs);
427    db_sql_query(ua->db, query.c_str(), NULL, (void *)NULL);
428    Dmsg1(050, "Delete RestoreObject sql=%s\n", query.c_str());
429
430
431    upgrade_copies(ua, jobs);
432
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);
436
437    Dmsg1(050, "Delete Job sql=%s\n", query.c_str());
438 }
439
440 void purge_files_from_volume(UAContext *ua, MEDIA_DBR *mr )
441 {} /* ***FIXME*** implement */
442
443 /*
444  * Returns: 1 if Volume purged
445  *          0 if Volume not purged
446  */
447 bool purge_jobs_from_volume(UAContext *ua, MEDIA_DBR *mr, bool force)
448 {
449    POOL_MEM query(PM_MESSAGE);
450    struct del_ctx del;
451    int i;
452    bool purged = false;
453    bool stat;
454    JOB_DBR jr;
455    char ed1[50];
456
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;
461    if (!stat) {
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);
465       return 0;
466    }
467
468    memset(&jr, 0, sizeof(jr));
469    memset(&del, 0, sizeof(del));
470    del.max_ids = 1000;
471    del.JobId = (JobId_t *)malloc(sizeof(JobId_t) * del.max_ids);
472
473    /*
474     * Check if he wants to purge a single jobid
475     */
476    i = find_arg_with_value(ua, "jobid");
477    if (i >= 0) {
478       del.num_ids = 1;
479       del.JobId[0] = str_to_int64(ua->argv[i]);
480    } else {
481       /*
482        * Purge ALL JobIds
483        */
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");
489          goto bail_out;
490       }
491    }
492
493    purge_job_list_from_catalog(ua, del);
494
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);
497
498    purged = is_volume_purged(ua, mr, force); 
499
500 bail_out:
501    if (del.JobId) {
502       free(del.JobId);
503    }
504    return purged;
505 }
506
507 /*
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
510  *
511  * Returns: true if volume purged
512  *          false if not
513  *
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).
519  */
520 bool is_volume_purged(UAContext *ua, MEDIA_DBR *mr, bool force)
521 {
522    POOL_MEM query(PM_MESSAGE);
523    struct s_count_ctx cnt;
524    bool purged = false;
525    char ed1[50];
526
527    if (!force && (mr->FirstWritten == 0 || mr->LastWritten == 0)) {
528       goto bail_out;               /* not written cannot purge */
529    }
530
531    if (strcmp(mr->VolStatus, "Purged") == 0) {
532       purged = true;
533       goto bail_out;
534    }
535
536    /* If purged, mark it so */
537    cnt.count = 0;
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");
543       goto bail_out;
544    }
545
546    if (cnt.count == 0) {
547       ua->warning_msg(_("There are no more Jobs associated with Volume \"%s\". Marking it purged.\n"),
548          mr->VolumeName);
549       if (!(purged = mark_media_purged(ua, mr))) {
550          ua->error_msg("%s", db_strerror(ua->db));
551       }
552    }
553 bail_out:
554    return purged;
555 }
556
557 static BSOCK *open_sd_bsock(UAContext *ua)
558 {
559    STORE *store = ua->jcr->wstore;
560
561    if (!ua->jcr->store_bsock) {
562       ua->send_msg(_("Connecting to Storage daemon %s at %s:%d ...\n"),
563          store->name(), store->address, store->SDport);
564       if (!connect_to_storage_daemon(ua->jcr, 10, SDConnectTimeout, 1)) {
565          ua->error_msg(_("Failed to connect to Storage daemon.\n"));
566          return NULL;
567       }
568    }
569    return ua->jcr->store_bsock;
570 }
571
572 /* 
573  * Called here to send the appropriate commands to the SD
574  *  to do truncate on purge.
575  */
576 static void do_truncate_on_purge(UAContext *ua, MEDIA_DBR *mr, 
577                                  char *pool, char *storage,
578                                  int drive, BSOCK *sd)
579 {
580    int dvd;
581    bool ok=false;
582    uint64_t VolBytes = 0;
583    
584    /* TODO: Return if not mr->Recyle ? */
585    if (!mr->Recycle) {
586       return;
587    }
588
589    /* Do it only if action on purge = truncate is set */
590    if (!(mr->ActionOnPurge & ON_PURGE_TRUNCATE)) {
591       return;
592    }
593    /*
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.
596     */
597
598    /* Protect us from spaces */
599    bash_spaces(mr->VolumeName);
600    bash_spaces(mr->MediaType);
601    bash_spaces(pool);
602    bash_spaces(storage);
603       
604    /* Do it by relabeling the Volume, which truncates it */
605    sd->fsend("relabel %s OldName=%s NewName=%s PoolName=%s "
606              "MediaType=%s Slot=%d drive=%d\n",
607                 storage,
608                 mr->VolumeName, mr->VolumeName,
609                 pool, mr->MediaType, mr->Slot, drive);
610       
611    unbash_spaces(mr->VolumeName);
612    unbash_spaces(mr->MediaType);
613    unbash_spaces(pool);
614    unbash_spaces(storage);
615
616    /* Send relabel command, and check for valid response */
617    while (sd->recv() >= 0) {
618       ua->send_msg("%s", sd->msg);
619       if (sscanf(sd->msg, "3000 OK label. VolBytes=%llu DVD=%d ", &VolBytes, &dvd) == 2) {
620          ok = true;
621       }
622    }
623
624    if (ok) {
625       mr->VolBytes = VolBytes;
626       mr->VolFiles = 0;
627       if (!db_update_media_record(ua->jcr, ua->db, mr)) {
628          ua->error_msg(_("Can't update volume size in the catalog\n"));
629       }
630       ua->send_msg(_("The volume \"%s\" has been truncated\n"), mr->VolumeName);
631    } else {
632       ua->warning_msg(_("Unable to truncate volume \"%s\"\n"), mr->VolumeName);
633    }
634 }
635
636 /* 
637  * Implement Bacula bconsole command  purge action
638  *     purge action= pool= volume= storage= devicetype= 
639  */
640 static int action_on_purge_cmd(UAContext *ua, const char *cmd)
641 {
642    bool allpools = false;
643    int drive = -1;
644    int nb = 0;
645    uint32_t *results = NULL;
646    const char *action = "all";
647    STORE *store = NULL;
648    POOL *pool = NULL;
649    MEDIA_DBR mr;
650    POOL_DBR pr;
651    BSOCK *sd = NULL;
652    
653    memset(&pr, 0, sizeof(pr));
654    memset(&mr, 0, sizeof(mr));
655
656    /* Look at arguments */
657    for (int i=1; i<ua->argc; i++) {
658       if (strcasecmp(ua->argk[i], NT_("allpools")) == 0) {
659          allpools = true;
660             
661       } else if (strcasecmp(ua->argk[i], NT_("volume")) == 0 && ua->argv[i]) {
662          bstrncpy(mr.VolumeName, ua->argv[i], sizeof(mr.VolumeName));
663
664       } else if (strcasecmp(ua->argk[i], NT_("devicetype")) == 0 && ua->argv[i]) {
665          bstrncpy(mr.MediaType, ua->argv[i], sizeof(mr.MediaType));
666          
667       } else if (strcasecmp(ua->argk[i], NT_("drive")) == 0 && ua->argv[i]) {
668          drive = atoi(ua->argv[i]);
669
670       } else if (strcasecmp(ua->argk[i], NT_("action")) == 0 && ua->argv[i]) {
671          action=ua->argv[i];
672       }
673    }
674
675    /* Choose storage */
676    ua->jcr->wstore = store =  get_storage_resource(ua, false);
677    if (!store) {
678       goto bail_out;
679    }
680    mr.StorageId = store->StorageId;
681
682    if (!open_db(ua)) {
683       Dmsg0(100, "Can't open db\n");
684       goto bail_out;
685    }
686
687    if (!allpools) {
688       /* force pool selection */
689       pool = get_pool_resource(ua);
690       if (!pool) {
691          Dmsg0(100, "Can't get pool resource\n");
692          goto bail_out;
693       }
694       bstrncpy(pr.Name, pool->name(), sizeof(pr.Name));
695       if (!db_get_pool_record(ua->jcr, ua->db, &pr)) {
696          Dmsg0(100, "Can't get pool record\n");
697          goto bail_out;
698       }
699       mr.PoolId = pr.PoolId;
700    }
701
702    /* 
703     * Look for all Purged volumes that can be recycled, are enabled and
704     *  have more the 10,000 bytes.
705     */
706    mr.Recycle = 1;
707    mr.Enabled = 1;
708    mr.VolBytes = 10000;
709    bstrncpy(mr.VolStatus, "Purged", sizeof(mr.VolStatus));
710    if (!db_get_media_ids(ua->jcr, ua->db, &mr, &nb, &results)) {
711       Dmsg0(100, "No results from db_get_media_ids\n");
712       goto bail_out;
713    }
714    
715    if (!nb) {
716       ua->send_msg(_("No Volumes found to perform %s action.\n"), action);
717       goto bail_out;
718    }
719
720    if ((sd=open_sd_bsock(ua)) == NULL) {
721       Dmsg0(100, "Can't open connection to sd\n");
722       goto bail_out;
723    }
724
725    /*
726     * Loop over the candidate Volumes and actually truncate them
727     */
728    for (int i=0; i < nb; i++) {
729       memset(&mr, 0, sizeof(mr));
730       mr.MediaId = results[i];
731       if (db_get_media_record(ua->jcr, ua->db, &mr)) {         
732          /* TODO: ask for drive and change Pool */
733          if (!strcasecmp("truncate", action) || !strcasecmp("all", action)) {
734             do_truncate_on_purge(ua, &mr, pr.Name, store->dev_name(), drive, sd);
735          }
736       } else {
737          Dmsg1(0, "Can't find MediaId=%lld\n", (uint64_t) mr.MediaId);
738       }
739    }
740
741 bail_out:
742    close_db(ua);
743    if (sd) {
744       sd->signal(BNET_TERMINATE);
745       sd->close();
746       ua->jcr->store_bsock = NULL;
747    }
748    ua->jcr->wstore = NULL;
749    if (results) {
750       free(results);
751    }
752
753    return 1;
754 }
755
756 /*
757  * IF volume status is Append, Full, Used, or Error, mark it Purged
758  *   Purged volumes can then be recycled (if enabled).
759  */
760 bool mark_media_purged(UAContext *ua, MEDIA_DBR *mr)
761 {
762    JCR *jcr = ua->jcr;
763    if (strcmp(mr->VolStatus, "Append") == 0 ||
764        strcmp(mr->VolStatus, "Full")   == 0 ||
765        strcmp(mr->VolStatus, "Used")   == 0 ||
766        strcmp(mr->VolStatus, "Error")  == 0) {
767       bstrncpy(mr->VolStatus, "Purged", sizeof(mr->VolStatus));
768       if (!db_update_media_record(jcr, ua->db, mr)) {
769          return false;
770       }
771       pm_strcpy(jcr->VolumeName, mr->VolumeName);
772       generate_job_event(jcr, "VolumePurged");
773       generate_plugin_event(jcr, bEventVolumePurged);
774       /*
775        * If the RecyclePool is defined, move the volume there
776        */
777       if (mr->RecyclePoolId && mr->RecyclePoolId != mr->PoolId) {
778          POOL_DBR oldpr, newpr;
779          memset(&oldpr, 0, sizeof(POOL_DBR));
780          memset(&newpr, 0, sizeof(POOL_DBR));
781          newpr.PoolId = mr->RecyclePoolId;
782          oldpr.PoolId = mr->PoolId;
783          if (   db_get_pool_record(jcr, ua->db, &oldpr) 
784              && db_get_pool_record(jcr, ua->db, &newpr)) 
785          {
786             /* check if destination pool size is ok */
787             if (newpr.MaxVols > 0 && newpr.NumVols >= newpr.MaxVols) {
788                ua->error_msg(_("Unable move recycled Volume in full " 
789                               "Pool \"%s\" MaxVols=%d\n"),
790                         newpr.Name, newpr.MaxVols);
791
792             } else {            /* move media */
793                update_vol_pool(ua, newpr.Name, mr, &oldpr);
794             }
795          } else {
796             ua->error_msg("%s", db_strerror(ua->db));
797          }
798       }
799
800       /* Send message to Job report, if it is a *real* job */           
801       if (jcr && jcr->JobId > 0) {
802          Jmsg(jcr, M_INFO, 0, _("All records pruned from Volume \"%s\"; marking it \"Purged\"\n"),
803             mr->VolumeName); 
804       }
805       return true;
806    } else {
807       ua->error_msg(_("Cannot purge Volume with VolStatus=%s\n"), mr->VolStatus);
808    }
809    return strcmp(mr->VolStatus, "Purged") == 0;
810 }