]> git.sur5r.net Git - bacula/bacula/blob - bacula/src/dird/ua_purge.c
Implement new "purge volume action" command to trigg ActionOnPurge.
[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 static int aop_cmd(UAContext *ua, const char *cmd);
48
49 static const char *select_jobsfiles_from_client =
50    "SELECT JobId FROM Job "
51    "WHERE ClientId=%s "
52    "AND PurgedFiles=0";
53
54 static const char *select_jobs_from_client =
55    "SELECT JobId, PurgedFiles FROM Job "
56    "WHERE ClientId=%s";
57
58 /*
59  *   Purge records from database
60  *
61  *     Purge Files (from) [Job|JobId|Client|Volume]
62  *     Purge Jobs  (from) [Client|Volume]
63  *
64  *  N.B. Not all above is implemented yet.
65  */
66 int purgecmd(UAContext *ua, const char *cmd)
67 {
68    int i;
69    CLIENT *client;
70    MEDIA_DBR mr;
71    JOB_DBR  jr;
72    static const char *keywords[] = {
73       NT_("files"),
74       NT_("jobs"),
75       NT_("volume"),
76       NULL};
77
78    static const char *files_keywords[] = {
79       NT_("Job"),
80       NT_("JobId"),
81       NT_("Client"),
82       NT_("Volume"),
83       NULL};
84
85    static const char *jobs_keywords[] = {
86       NT_("Client"),
87       NT_("Volume"),
88       NULL};
89
90    ua->warning_msg(_(
91       "\nThis command can be DANGEROUS!!!\n\n"
92       "It purges (deletes) all Files from a Job,\n"
93       "JobId, Client or Volume; or it purges (deletes)\n"
94       "all Jobs from a Client or Volume without regard\n"
95       "to retention periods. Normally you should use the\n"
96       "PRUNE command, which respects retention periods.\n"));
97
98    if (!open_db(ua)) {
99       return 1;
100    }
101    switch (find_arg_keyword(ua, keywords)) {
102    /* Files */
103    case 0:
104       switch(find_arg_keyword(ua, files_keywords)) {
105       case 0:                         /* Job */
106       case 1:                         /* JobId */
107          if (get_job_dbr(ua, &jr)) {
108             char jobid[50];
109             edit_int64(jr.JobId, jobid);
110             purge_files_from_jobs(ua, jobid);
111          }
112          return 1;
113       case 2:                         /* client */
114          client = get_client_resource(ua);
115          if (client) {
116             purge_files_from_client(ua, client);
117          }
118          return 1;
119       case 3:                         /* Volume */
120          if (select_media_dbr(ua, &mr)) {
121             purge_files_from_volume(ua, &mr);
122          }
123          return 1;
124       }
125    /* Jobs */
126    case 1:
127       switch(find_arg_keyword(ua, jobs_keywords)) {
128       case 0:                         /* client */
129          client = get_client_resource(ua);
130          if (client) {
131             purge_jobs_from_client(ua, client);
132          }
133          return 1;
134       case 1:                         /* Volume */
135          if (select_media_dbr(ua, &mr)) {
136             purge_jobs_from_volume(ua, &mr, /*force*/true);
137          }
138          return 1;
139       }
140    /* Volume */
141    case 2:
142       /* Perform ActionOnPurge (action=truncate) */
143       if (find_arg(ua, "action") >= 0) {
144          return aop_cmd(ua, ua->cmd);
145       }
146
147       while ((i=find_arg(ua, NT_("volume"))) >= 0) {
148          if (select_media_dbr(ua, &mr)) {
149             purge_jobs_from_volume(ua, &mr, /*force*/true);
150          }
151          *ua->argk[i] = 0;            /* zap keyword already seen */
152          ua->send_msg("\n");
153       }
154       return 1;
155    default:
156       break;
157    }
158    switch (do_keyword_prompt(ua, _("Choose item to purge"), keywords)) {
159    case 0:                            /* files */
160       client = get_client_resource(ua);
161       if (client) {
162          purge_files_from_client(ua, client);
163       }
164       break;
165    case 1:                            /* jobs */
166       client = get_client_resource(ua);
167       if (client) {
168          purge_jobs_from_client(ua, client);
169       }
170       break;
171    case 2:                            /* Volume */
172       if (select_media_dbr(ua, &mr)) {
173          purge_jobs_from_volume(ua, &mr, /*force*/true);
174       }
175       break;
176    }
177    return 1;
178 }
179
180 /*
181  * Purge File records from the database. For any Job which
182  * is older than the retention period, we unconditionally delete
183  * all File records for that Job.  This is simple enough that no
184  * temporary tables are needed. We simply make an in memory list of
185  * the JobIds meeting the prune conditions, then delete all File records
186  * pointing to each of those JobIds.
187  */
188 static int purge_files_from_client(UAContext *ua, CLIENT *client)
189 {
190    struct del_ctx del;
191    POOL_MEM query(PM_MESSAGE);
192    CLIENT_DBR cr;
193    char ed1[50];
194
195    memset(&cr, 0, sizeof(cr));
196    bstrncpy(cr.Name, client->name(), sizeof(cr.Name));
197    if (!db_create_client_record(ua->jcr, ua->db, &cr)) {
198       return 0;
199    }
200
201    memset(&del, 0, sizeof(del));
202    del.max_ids = 1000;
203    del.JobId = (JobId_t *)malloc(sizeof(JobId_t) * del.max_ids);
204
205    ua->info_msg(_("Begin purging files for Client \"%s\"\n"), cr.Name);
206
207    Mmsg(query, select_jobsfiles_from_client, edit_int64(cr.ClientId, ed1));
208    Dmsg1(050, "select sql=%s\n", query.c_str());
209    db_sql_query(ua->db, query.c_str(), file_delete_handler, (void *)&del);
210
211    purge_files_from_job_list(ua, del);
212
213    if (del.num_ids == 0) {
214       ua->warning_msg(_("No Files found for client %s to purge from %s catalog.\n"),
215          client->name(), client->catalog->name());
216    } else {
217       ua->info_msg(_("Files for %d Jobs for client \"%s\" purged from %s catalog.\n"), del.num_ids,
218          client->name(), client->catalog->name());
219    }
220
221    if (del.JobId) {
222       free(del.JobId);
223    }
224    return 1;
225 }
226
227
228
229 /*
230  * Purge Job records from the database. For any Job which
231  * is older than the retention period, we unconditionally delete
232  * it and all File records for that Job.  This is simple enough that no
233  * temporary tables are needed. We simply make an in memory list of
234  * the JobIds then delete the Job, Files, and JobMedia records in that list.
235  */
236 static int purge_jobs_from_client(UAContext *ua, CLIENT *client)
237 {
238    struct del_ctx del;
239    POOL_MEM query(PM_MESSAGE);
240    CLIENT_DBR cr;
241    char ed1[50];
242
243    memset(&cr, 0, sizeof(cr));
244
245    bstrncpy(cr.Name, client->name(), sizeof(cr.Name));
246    if (!db_create_client_record(ua->jcr, ua->db, &cr)) {
247       return 0;
248    }
249
250    memset(&del, 0, sizeof(del));
251    del.max_ids = 1000;
252    del.JobId = (JobId_t *)malloc(sizeof(JobId_t) * del.max_ids);
253    del.PurgedFiles = (char *)malloc(del.max_ids);
254    
255    ua->info_msg(_("Begin purging jobs from Client \"%s\"\n"), cr.Name);
256
257    Mmsg(query, select_jobs_from_client, edit_int64(cr.ClientId, ed1));
258    Dmsg1(150, "select sql=%s\n", query.c_str());
259    db_sql_query(ua->db, query.c_str(), job_delete_handler, (void *)&del);
260
261    purge_job_list_from_catalog(ua, del);
262
263    if (del.num_ids == 0) {
264       ua->warning_msg(_("No Files found for client %s to purge from %s catalog.\n"),
265          client->name(), client->catalog->name());
266    } else {
267       ua->info_msg(_("%d Jobs for client %s purged from %s catalog.\n"), del.num_ids,
268          client->name(), client->catalog->name());
269    }
270
271    if (del.JobId) {
272       free(del.JobId);
273    }
274    if (del.PurgedFiles) {
275       free(del.PurgedFiles);
276    }
277    return 1;
278 }
279
280
281 /*
282  * Remove File records from a list of JobIds
283  */
284 void purge_files_from_jobs(UAContext *ua, char *jobs)
285 {
286    POOL_MEM query(PM_MESSAGE);
287
288    Mmsg(query, "DELETE FROM File WHERE JobId IN (%s)", jobs);
289    db_sql_query(ua->db, query.c_str(), NULL, (void *)NULL);
290    Dmsg1(050, "Delete File sql=%s\n", query.c_str());
291
292    Mmsg(query, "DELETE FROM BaseFiles WHERE JobId IN (%s)", jobs);
293    db_sql_query(ua->db, query.c_str(), NULL, (void *)NULL);
294    Dmsg1(050, "Delete BaseFiles sql=%s\n", query.c_str());
295
296    /*
297     * Now mark Job as having files purged. This is necessary to
298     * avoid having too many Jobs to process in future prunings. If
299     * we don't do this, the number of JobId's in our in memory list
300     * could grow very large.
301     */
302    Mmsg(query, "UPDATE Job SET PurgedFiles=1 WHERE JobId IN (%s)", jobs);
303    db_sql_query(ua->db, query.c_str(), NULL, (void *)NULL);
304    Dmsg1(050, "Mark purged sql=%s\n", query.c_str());
305 }
306
307 /*
308  * Delete jobs (all records) from the catalog in groups of 1000
309  *  at a time.
310  */
311 void purge_job_list_from_catalog(UAContext *ua, del_ctx &del)
312 {
313    POOL_MEM jobids(PM_MESSAGE);
314    char ed1[50];
315
316    for (int i=0; del.num_ids; ) {
317       Dmsg1(150, "num_ids=%d\n", del.num_ids);
318       pm_strcat(jobids, "");
319       for (int j=0; j<1000 && del.num_ids>0; j++) {
320          del.num_ids--;
321          if (del.JobId[i] == 0 || ua->jcr->JobId == del.JobId[i]) {
322             Dmsg2(150, "skip JobId[%d]=%d\n", i, (int)del.JobId[i]);
323             i++;
324             continue;
325          }
326          if (*jobids.c_str() != 0) {
327             pm_strcat(jobids, ",");
328          }
329          pm_strcat(jobids, edit_int64(del.JobId[i++], ed1));
330          Dmsg1(150, "Add id=%s\n", ed1);
331          del.num_del++;
332       }
333       Dmsg1(150, "num_ids=%d\n", del.num_ids);
334       purge_jobs_from_catalog(ua, jobids.c_str());
335    }
336 }
337
338 /*
339  * Delete files from a list of jobs in groups of 1000
340  *  at a time.
341  */
342 void purge_files_from_job_list(UAContext *ua, del_ctx &del)
343 {
344    POOL_MEM jobids(PM_MESSAGE);
345    char ed1[50];
346    /*
347     * OK, now we have the list of JobId's to be pruned, send them
348     *   off to be deleted batched 1000 at a time.
349     */
350    for (int i=0; del.num_ids; ) {
351       pm_strcat(jobids, "");
352       for (int j=0; j<1000 && del.num_ids>0; j++) {
353          del.num_ids--;
354          if (del.JobId[i] == 0 || ua->jcr->JobId == del.JobId[i]) {
355             Dmsg2(150, "skip JobId[%d]=%d\n", i, (int)del.JobId[i]);
356             i++;
357             continue;
358          }
359          if (*jobids.c_str() != 0) {
360             pm_strcat(jobids, ",");
361          }
362          pm_strcat(jobids, edit_int64(del.JobId[i++], ed1));
363          Dmsg1(150, "Add id=%s\n", ed1);
364          del.num_del++;
365       }
366       purge_files_from_jobs(ua, jobids.c_str());
367    }
368 }
369
370 /*
371  * Change the type of the next copy job to backup.
372  * We need to upgrade the next copy of a normal job,
373  * and also upgrade the next copy when the normal job
374  * already have been purged.
375  *
376  *   JobId: 1   PriorJobId: 0    (original)
377  *   JobId: 2   PriorJobId: 1    (first copy)
378  *   JobId: 3   PriorJobId: 1    (second copy)
379  *
380  *   JobId: 2   PriorJobId: 1    (first copy, now regular backup)
381  *   JobId: 3   PriorJobId: 1    (second copy)
382  *
383  *  => Search through PriorJobId in jobid and
384  *                    PriorJobId in PriorJobId (jobid)
385  */
386 void upgrade_copies(UAContext *ua, char *jobs)
387 {
388    POOL_MEM query(PM_MESSAGE);
389    
390    db_lock(ua->db);
391    /* Do it in two times for mysql */
392    Mmsg(query, "CREATE TEMPORARY TABLE cpy_tmp AS "
393                   "SELECT MIN(JobId) AS JobId FROM Job "     /* Choose the oldest job */
394                    "WHERE Type='%c' "
395                      "AND ( PriorJobId IN (%s) "
396                          "OR "
397                           " PriorJobId IN ( "
398                              "SELECT PriorJobId "
399                                "FROM Job "
400                               "WHERE JobId IN (%s) "
401                                " AND Type='B' "
402                             ") "
403                          ") "
404                    "GROUP BY PriorJobId ",           /* one result per copy */
405         JT_JOB_COPY, jobs, jobs);
406    db_sql_query(ua->db, query.c_str(), NULL, (void *)NULL);
407    Dmsg1(050, "Upgrade copies Log sql=%s\n", query.c_str());
408
409    /* Now upgrade first copy to Backup */
410    Mmsg(query, "UPDATE Job SET Type='B' "           /* JT_JOB_COPY => JT_BACKUP  */
411                 "WHERE JobId IN ( SELECT JobId FROM cpy_tmp )");
412
413    db_sql_query(ua->db, query.c_str(), NULL, (void *)NULL);
414
415    Mmsg(query, "DROP TABLE cpy_tmp");
416    db_sql_query(ua->db, query.c_str(), NULL, (void *)NULL);
417
418    db_unlock(ua->db);
419 }
420
421 /*
422  * Remove all records from catalog for a list of JobIds
423  */
424 void purge_jobs_from_catalog(UAContext *ua, char *jobs)
425 {
426    POOL_MEM query(PM_MESSAGE);
427
428    /* Delete (or purge) records associated with the job */
429    purge_files_from_jobs(ua, jobs);
430
431    Mmsg(query, "DELETE FROM JobMedia WHERE JobId IN (%s)", jobs);
432    db_sql_query(ua->db, query.c_str(), NULL, (void *)NULL);
433    Dmsg1(050, "Delete JobMedia sql=%s\n", query.c_str());
434
435    Mmsg(query, "DELETE FROM Log WHERE JobId IN (%s)", jobs);
436    db_sql_query(ua->db, query.c_str(), NULL, (void *)NULL);
437    Dmsg1(050, "Delete Log sql=%s\n", query.c_str());
438
439    upgrade_copies(ua, jobs);
440
441    /* Now remove the Job record itself */
442    Mmsg(query, "DELETE FROM Job WHERE JobId IN (%s)", jobs);
443    db_sql_query(ua->db, query.c_str(), NULL, (void *)NULL);
444
445    Dmsg1(050, "Delete Job sql=%s\n", query.c_str());
446 }
447
448 void purge_files_from_volume(UAContext *ua, MEDIA_DBR *mr )
449 {} /* ***FIXME*** implement */
450
451 /*
452  * Returns: 1 if Volume purged
453  *          0 if Volume not purged
454  */
455 bool purge_jobs_from_volume(UAContext *ua, MEDIA_DBR *mr, bool force)
456 {
457    POOL_MEM query(PM_MESSAGE);
458    struct del_ctx del;
459    int i;
460    bool purged = false;
461    bool stat;
462    JOB_DBR jr;
463    char ed1[50];
464
465    stat = strcmp(mr->VolStatus, "Append") == 0 ||
466           strcmp(mr->VolStatus, "Full")   == 0 ||
467           strcmp(mr->VolStatus, "Used")   == 0 ||
468           strcmp(mr->VolStatus, "Error")  == 0;
469    if (!stat) {
470       ua->error_msg(_("\nVolume \"%s\" has VolStatus \"%s\" and cannot be purged.\n"
471                      "The VolStatus must be: Append, Full, Used, or Error to be purged.\n"),
472                      mr->VolumeName, mr->VolStatus);
473       return 0;
474    }
475
476    memset(&jr, 0, sizeof(jr));
477    memset(&del, 0, sizeof(del));
478    del.max_ids = 1000;
479    del.JobId = (JobId_t *)malloc(sizeof(JobId_t) * del.max_ids);
480
481    /*
482     * Check if he wants to purge a single jobid
483     */
484    i = find_arg_with_value(ua, "jobid");
485    if (i >= 0) {
486       del.num_ids = 1;
487       del.JobId[0] = str_to_int64(ua->argv[i]);
488    } else {
489       /*
490        * Purge ALL JobIds
491        */
492       Mmsg(query, "SELECT DISTINCT JobId FROM JobMedia WHERE MediaId=%s", 
493            edit_int64(mr->MediaId, ed1));
494       if (!db_sql_query(ua->db, query.c_str(), file_delete_handler, (void *)&del)) {
495          ua->error_msg("%s", db_strerror(ua->db));
496          Dmsg0(050, "Count failed\n");
497          goto bail_out;
498       }
499    }
500
501    purge_job_list_from_catalog(ua, del);
502
503    ua->info_msg(_("%d File%s on Volume \"%s\" purged from catalog.\n"), del.num_del,
504       del.num_del==1?"":"s", mr->VolumeName);
505
506    purged = is_volume_purged(ua, mr, force); 
507
508 bail_out:
509    if (del.JobId) {
510       free(del.JobId);
511    }
512    return purged;
513 }
514
515 /*
516  * This routine will check the JobMedia records to see if the
517  *   Volume has been purged. If so, it marks it as such and
518  *
519  * Returns: true if volume purged
520  *          false if not
521  *
522  * Note, we normally will not purge a volume that has Firstor LastWritten
523  *   zero, because it means the volume is most likely being written
524  *   however, if the user manually purges using the purge command in
525  *   the console, he has been warned, and we go ahead and purge
526  *   the volume anyway, if possible).
527  */
528 bool is_volume_purged(UAContext *ua, MEDIA_DBR *mr, bool force)
529 {
530    POOL_MEM query(PM_MESSAGE);
531    struct s_count_ctx cnt;
532    bool purged = false;
533    char ed1[50];
534
535    if (!force && (mr->FirstWritten == 0 || mr->LastWritten == 0)) {
536       goto bail_out;               /* not written cannot purge */
537    }
538
539    if (strcmp(mr->VolStatus, "Purged") == 0) {
540       purged = true;
541       goto bail_out;
542    }
543
544    /* If purged, mark it so */
545    cnt.count = 0;
546    Mmsg(query, "SELECT count(*) FROM JobMedia WHERE MediaId=%s", 
547         edit_int64(mr->MediaId, ed1));
548    if (!db_sql_query(ua->db, query.c_str(), del_count_handler, (void *)&cnt)) {
549       ua->error_msg("%s", db_strerror(ua->db));
550       Dmsg0(050, "Count failed\n");
551       goto bail_out;
552    }
553
554    if (cnt.count == 0) {
555       ua->warning_msg(_("There are no more Jobs associated with Volume \"%s\". Marking it purged.\n"),
556          mr->VolumeName);
557       if (!(purged = mark_media_purged(ua, mr))) {
558          ua->error_msg("%s", db_strerror(ua->db));
559       }
560    }
561 bail_out:
562    return purged;
563 }
564
565 static BSOCK *open_sd_bsock(UAContext *ua)
566 {
567    STORE *store = ua->jcr->wstore;
568
569    if (!ua->jcr->store_bsock) {
570       ua->send_msg(_("Connecting to Storage daemon %s at %s:%d ...\n"),
571          store->name(), store->address, store->SDport);
572       if (!connect_to_storage_daemon(ua->jcr, 10, SDConnectTimeout, 1)) {
573          ua->error_msg(_("Failed to connect to Storage daemon.\n"));
574          return NULL;
575       }
576    }
577    return ua->jcr->store_bsock;
578 }
579
580 static void do_truncate_on_purge(UAContext *ua, MEDIA_DBR *mr, 
581                                  char *pool, char *storage,
582                                  int drive, BSOCK *sd)
583 {
584    int dvd;
585    bool ok=false;
586    uint64_t VolBytes = 0;
587    
588    /* TODO: Return if not mr->Recyle ? */
589    if (!mr->Recycle) {
590       return;
591    }
592
593    if (mr->ActionOnPurge & AOP_TRUNCATE) {
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       sd->fsend("relabel %s OldName=%s NewName=%s PoolName=%s "
605                 "MediaType=%s Slot=%d drive=%d\n",
606                    storage,
607                    mr->VolumeName, mr->VolumeName,
608                    pool, mr->MediaType, mr->Slot, drive);
609          
610       unbash_spaces(mr->VolumeName);
611       unbash_spaces(mr->MediaType);
612       unbash_spaces(pool);
613       unbash_spaces(storage);
614
615       while (sd->recv() >= 0) {
616          ua->send_msg("%s", sd->msg);
617          if (sscanf(sd->msg, "3000 OK label. VolBytes=%llu DVD=%d ",
618                     &VolBytes, &dvd) == 2) 
619          {
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 /* purge action= pool= volume= storage= devicetype= */
638 static int aop_cmd(UAContext *ua, const char *cmd)
639 {
640    bool allpools=false;
641    int drive=-1;
642    int nb=0;
643
644    uint32_t *results=NULL;
645    const char *action="all";
646    STORE *store=NULL;
647    POOL *pool=NULL;
648    MEDIA_DBR mr;
649    POOL_DBR pr;
650
651    BSOCK *sd=NULL;
652    
653    memset(&pr, 0, sizeof(pr));
654    memset(&mr, 0, sizeof(mr));
655
656    /* Look 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    mr.Recycle = 1;
703    mr.Enabled = 1;
704    mr.VolBytes = 10000;
705    bstrncpy(mr.VolStatus, "Purged", sizeof(mr.VolStatus));
706
707    if (!db_get_media_ids(ua->jcr, ua->db, &mr, &nb, &results)) {
708       Dmsg0(100, "No results from db_get_media_ids\n");
709       goto bail_out;
710    }
711    
712    if (!nb) {
713       ua->send_msg(_("No volume founds to perform %s action(s)\n"), action);
714       goto bail_out;
715    }
716
717    if ((sd=open_sd_bsock(ua)) == NULL) {
718       Dmsg0(100, "Can't open connection to sd\n");
719       goto bail_out;
720    }
721
722    for (int i=0; i < nb; i++) {
723       memset(&mr, 0, sizeof(mr));
724       mr.MediaId = results[i];
725       if (db_get_media_record(ua->jcr, ua->db, &mr)) {         
726          /* TODO: ask for drive and change Pool */
727          if (!strcasecmp("truncate", action) || !strcasecmp("all", action)) {
728             do_truncate_on_purge(ua, &mr, pr.Name, store->dev_name(), drive, sd);
729          }
730       } else {
731          Dmsg1(0, "Can't find MediaId=%lld\n", (uint64_t) mr.MediaId);
732       }
733    }
734
735 bail_out:
736    close_db(ua);
737    if (sd) {
738       sd->signal(BNET_TERMINATE);
739       sd->close();
740       ua->jcr->store_bsock = NULL;
741    }
742    ua->jcr->wstore = NULL;
743    if (results) {
744       free(results);
745    }
746
747    return 1;
748 }
749
750 /*
751  * IF volume status is Append, Full, Used, or Error, mark it Purged
752  *   Purged volumes can then be recycled (if enabled).
753  */
754 bool mark_media_purged(UAContext *ua, MEDIA_DBR *mr)
755 {
756    JCR *jcr = ua->jcr;
757    if (strcmp(mr->VolStatus, "Append") == 0 ||
758        strcmp(mr->VolStatus, "Full")   == 0 ||
759        strcmp(mr->VolStatus, "Used")   == 0 ||
760        strcmp(mr->VolStatus, "Error")  == 0) {
761       bstrncpy(mr->VolStatus, "Purged", sizeof(mr->VolStatus));
762       if (!db_update_media_record(jcr, ua->db, mr)) {
763          return false;
764       }
765       pm_strcpy(jcr->VolumeName, mr->VolumeName);
766       generate_job_event(jcr, "VolumePurged");
767       generate_plugin_event(jcr, bEventVolumePurged);
768       /*
769        * If the RecyclePool is defined, move the volume there
770        */
771       if (mr->RecyclePoolId && mr->RecyclePoolId != mr->PoolId) {
772          POOL_DBR oldpr, newpr;
773          memset(&oldpr, 0, sizeof(POOL_DBR));
774          memset(&newpr, 0, sizeof(POOL_DBR));
775          newpr.PoolId = mr->RecyclePoolId;
776          oldpr.PoolId = mr->PoolId;
777          if (   db_get_pool_record(jcr, ua->db, &oldpr) 
778              && db_get_pool_record(jcr, ua->db, &newpr)) 
779          {
780             /* check if destination pool size is ok */
781             if (newpr.MaxVols > 0 && newpr.NumVols >= newpr.MaxVols) {
782                ua->error_msg(_("Unable move recycled Volume in full " 
783                               "Pool \"%s\" MaxVols=%d\n"),
784                         newpr.Name, newpr.MaxVols);
785
786             } else {            /* move media */
787                update_vol_pool(ua, newpr.Name, mr, &oldpr);
788             }
789          } else {
790             ua->error_msg("%s", db_strerror(ua->db));
791          }
792       }
793
794       /* Send message to Job report, if it is a *real* job */           
795       if (jcr && jcr->JobId > 0) {
796          Jmsg(jcr, M_INFO, 0, _("All records pruned from Volume \"%s\"; marking it \"Purged\"\n"),
797             mr->VolumeName); 
798       }
799       return true;
800    } else {
801       ua->error_msg(_("Cannot purge Volume with VolStatus=%s\n"), mr->VolStatus);
802    }
803    return strcmp(mr->VolStatus, "Purged") == 0;
804 }