]> git.sur5r.net Git - bacula/bacula/blob - bacula/src/dird/ua_purge.c
Backport from Bacula Enterprise
[bacula/bacula] / bacula / src / dird / ua_purge.c
1 /*
2    Bacula(R) - The Network Backup Solution
3
4    Copyright (C) 2000-2015 Kern Sibbald
5    Copyright (C) 2002-2014 Free Software Foundation Europe e.V.
6
7    The original author of Bacula is Kern Sibbald, with contributions
8    from many others, a complete list can be found in the file AUTHORS.
9
10    You may use this file and others of this release according to the
11    license defined in the LICENSE file, which includes the Affero General
12    Public License, v3.0 ("AGPLv3") and some additional permissions and
13    terms pursuant to its AGPLv3 Section 7.
14
15    This notice must be preserved when any source code is 
16    conveyed and/or propagated.
17
18    Bacula(R) is a registered trademark of Kern Sibbald.
19 */
20 /*
21  *
22  *   Bacula Director -- User Agent Database Purge Command
23  *
24  *      Purges Files from specific JobIds
25  * or
26  *      Purges Jobs from Volumes
27  *
28  *     Kern Sibbald, February MMII
29  *
30  */
31
32 #include "bacula.h"
33 #include "dird.h"
34
35 /* Forward referenced functions */
36 static int purge_files_from_client(UAContext *ua, CLIENT *client);
37 static int purge_jobs_from_client(UAContext *ua, CLIENT *client);
38 int truncate_cmd(UAContext *ua, const char *cmd);
39
40 static const char *select_jobsfiles_from_client =
41    "SELECT JobId FROM Job "
42    "WHERE ClientId=%s "
43    "AND PurgedFiles=0";
44
45 static const char *select_jobs_from_client =
46    "SELECT JobId, PurgedFiles FROM Job "
47    "WHERE ClientId=%s";
48
49 /*
50  *   Purge records from database
51  *
52  *     Purge Files (from) [Job|JobId|Client|Volume]
53  *     Purge Jobs  (from) [Client|Volume]
54  *     Purge Volumes
55  *
56  *  N.B. Not all above is implemented yet.
57  */
58 int purge_cmd(UAContext *ua, const char *cmd)
59 {
60    int i;
61    CLIENT *client;
62    MEDIA_DBR mr;
63    JOB_DBR  jr;
64    static const char *keywords[] = {
65       NT_("files"),
66       NT_("jobs"),
67       NT_("volume"),
68       NULL};
69
70    static const char *files_keywords[] = {
71       NT_("Job"),
72       NT_("JobId"),
73       NT_("Client"),
74       NT_("Volume"),
75       NULL};
76
77    static const char *jobs_keywords[] = {
78       NT_("Client"),
79       NT_("Volume"),
80       NULL};
81
82    /* Special case for the "Action On Purge", this option is working only on
83     * Purged volume, so no jobs or files will be purged.
84     * We are skiping this message if "purge volume action=xxx"
85     */
86    if (!(find_arg(ua, "volume") >= 0 && find_arg(ua, "action") >= 0)) {
87       ua->warning_msg(_(
88         "\nThis command can be DANGEROUS!!!\n\n"
89         "It purges (deletes) all Files from a Job,\n"
90         "JobId, Client or Volume; or it purges (deletes)\n"
91         "all Jobs from a Client or Volume without regard\n"
92         "to retention periods. Normally you should use the\n"
93         "PRUNE command, which respects retention periods.\n"));
94    }
95
96    if (!open_new_client_db(ua)) {
97       return 1;
98    }
99    switch (find_arg_keyword(ua, keywords)) {
100    /* Files */
101    case 0:
102       switch(find_arg_keyword(ua, files_keywords)) {
103       case 0:                         /* Job */
104       case 1:                         /* JobId */
105          if (get_job_dbr(ua, &jr)) {
106             char jobid[50];
107             edit_int64(jr.JobId, jobid);
108             purge_files_from_jobs(ua, jobid);
109          }
110          return 1;
111       case 2:                         /* client */
112          client = get_client_resource(ua);
113          if (client) {
114             purge_files_from_client(ua, client);
115          }
116          return 1;
117       case 3:                         /* Volume */
118          if (select_media_dbr(ua, &mr)) {
119             purge_files_from_volume(ua, &mr);
120          }
121          return 1;
122       }
123    /* Jobs */
124    case 1:
125       switch(find_arg_keyword(ua, jobs_keywords)) {
126       case 0:                         /* client */
127          client = get_client_resource(ua);
128          if (client) {
129             purge_jobs_from_client(ua, client);
130          }
131          return 1;
132       case 1:                         /* Volume */
133          if (select_media_dbr(ua, &mr)) {
134             purge_jobs_from_volume(ua, &mr, /*force*/true);
135          }
136          return 1;
137       }
138    /* Volume */
139    case 2:
140       /* Perform ActionOnPurge (action=truncate) */
141       if (find_arg(ua, "action") >= 0) {
142          return truncate_cmd(ua, ua->cmd);
143       }
144
145       while ((i=find_arg(ua, NT_("volume"))) >= 0) {
146          if (select_media_dbr(ua, &mr)) {
147             purge_jobs_from_volume(ua, &mr, /*force*/true);
148          }
149          *ua->argk[i] = 0;            /* zap keyword already seen */
150          ua->send_msg("\n");
151       }
152       return 1;
153    default:
154       break;
155    }
156    switch (do_keyword_prompt(ua, _("Choose item to purge"), keywords)) {
157    case 0:                            /* files */
158       client = get_client_resource(ua);
159       if (client) {
160          purge_files_from_client(ua, client);
161       }
162       break;
163    case 1:                            /* jobs */
164       client = get_client_resource(ua);
165       if (client) {
166          purge_jobs_from_client(ua, client);
167       }
168       break;
169    case 2:                            /* Volume */
170       if (select_media_dbr(ua, &mr)) {
171          purge_jobs_from_volume(ua, &mr, /*force*/true);
172       }
173       break;
174    }
175    return 1;
176 }
177
178 /*
179  * Purge File records from the database. For any Job which
180  * is older than the retention period, we unconditionally delete
181  * all File records for that Job.  This is simple enough that no
182  * temporary tables are needed. We simply make an in memory list of
183  * the JobIds meeting the prune conditions, then delete all File records
184  * pointing to each of those JobIds.
185  */
186 static int purge_files_from_client(UAContext *ua, CLIENT *client)
187 {
188    struct del_ctx del;
189    POOL_MEM query(PM_MESSAGE);
190    CLIENT_DBR cr;
191    char ed1[50];
192
193    memset(&cr, 0, sizeof(cr));
194    bstrncpy(cr.Name, client->name(), sizeof(cr.Name));
195    if (!db_create_client_record(ua->jcr, ua->db, &cr)) {
196       return 0;
197    }
198
199    memset(&del, 0, sizeof(del));
200    del.max_ids = 1000;
201    del.JobId = (JobId_t *)malloc(sizeof(JobId_t) * del.max_ids);
202
203    ua->info_msg(_("Begin purging files for Client \"%s\"\n"), cr.Name);
204
205    Mmsg(query, select_jobsfiles_from_client, edit_int64(cr.ClientId, ed1));
206    Dmsg1(050, "select sql=%s\n", query.c_str());
207    db_sql_query(ua->db, query.c_str(), file_delete_handler, (void *)&del);
208
209    purge_files_from_job_list(ua, del);
210
211    if (del.num_ids == 0) {
212       ua->warning_msg(_("No Files found for client %s to purge from %s catalog.\n"),
213          client->name(), client->catalog->name());
214    } else {
215       ua->info_msg(_("Files for %d Jobs for client \"%s\" purged from %s catalog.\n"), del.num_ids,
216          client->name(), client->catalog->name());
217    }
218
219    if (del.JobId) {
220       free(del.JobId);
221    }
222    return 1;
223 }
224
225
226
227 /*
228  * Purge Job records from the database. For any Job which
229  * is older than the retention period, we unconditionally delete
230  * it and all File records for that Job.  This is simple enough that no
231  * temporary tables are needed. We simply make an in memory list of
232  * the JobIds then delete the Job, Files, and JobMedia records in that list.
233  */
234 static int purge_jobs_from_client(UAContext *ua, CLIENT *client)
235 {
236    struct del_ctx del;
237    POOL_MEM query(PM_MESSAGE);
238    CLIENT_DBR cr;
239    char ed1[50];
240
241    memset(&cr, 0, sizeof(cr));
242
243    bstrncpy(cr.Name, client->name(), sizeof(cr.Name));
244    if (!db_create_client_record(ua->jcr, ua->db, &cr)) {
245       return 0;
246    }
247
248    memset(&del, 0, sizeof(del));
249    del.max_ids = 1000;
250    del.JobId = (JobId_t *)malloc(sizeof(JobId_t) * del.max_ids);
251    del.PurgedFiles = (char *)malloc(del.max_ids);
252
253    ua->info_msg(_("Begin purging jobs from Client \"%s\"\n"), cr.Name);
254
255    Mmsg(query, select_jobs_from_client, edit_int64(cr.ClientId, ed1));
256    Dmsg1(150, "select sql=%s\n", query.c_str());
257    db_sql_query(ua->db, query.c_str(), job_delete_handler, (void *)&del);
258
259    purge_job_list_from_catalog(ua, del);
260
261    if (del.num_ids == 0) {
262       ua->warning_msg(_("No Files found for client %s to purge from %s catalog.\n"),
263          client->name(), client->catalog->name());
264    } else {
265       ua->info_msg(_("%d Jobs for client %s purged from %s catalog.\n"), del.num_ids,
266          client->name(), client->catalog->name());
267    }
268
269    if (del.JobId) {
270       free(del.JobId);
271    }
272    if (del.PurgedFiles) {
273       free(del.PurgedFiles);
274    }
275    return 1;
276 }
277
278
279 /*
280  * Remove File records from a list of JobIds
281  */
282 void purge_files_from_jobs(UAContext *ua, char *jobs)
283 {
284    POOL_MEM query(PM_MESSAGE);
285
286    Mmsg(query, "DELETE FROM File WHERE JobId IN (%s)", jobs);
287    db_sql_query(ua->db, query.c_str(), NULL, (void *)NULL);
288    Dmsg1(050, "Delete File sql=%s\n", query.c_str());
289
290    Mmsg(query, "DELETE FROM BaseFiles WHERE JobId IN (%s)", jobs);
291    db_sql_query(ua->db, query.c_str(), NULL, (void *)NULL);
292    Dmsg1(050, "Delete BaseFiles sql=%s\n", query.c_str());
293
294    Mmsg(query, "DELETE FROM PathVisibility WHERE JobId IN (%s)", jobs);
295    db_sql_query(ua->db, query.c_str(), NULL, (void *)NULL);
296    Dmsg1(050, "Delete PathVisibility sql=%s\n", query.c_str());
297
298    /*
299     * Now mark Job as having files purged. This is necessary to
300     * avoid having too many Jobs to process in future prunings. If
301     * we don't do this, the number of JobId's in our in memory list
302     * could grow very large.
303     */
304    Mmsg(query, "UPDATE Job SET PurgedFiles=1 WHERE JobId IN (%s)", jobs);
305    db_sql_query(ua->db, query.c_str(), NULL, (void *)NULL);
306    Dmsg1(050, "Mark purged sql=%s\n", query.c_str());
307 }
308
309 /*
310  * Delete jobs (all records) from the catalog in groups of 1000
311  *  at a time.
312  */
313 void purge_job_list_from_catalog(UAContext *ua, del_ctx &del)
314 {
315    POOL_MEM jobids(PM_MESSAGE);
316    char ed1[50];
317
318    for (int i=0; del.num_ids; ) {
319       Dmsg1(150, "num_ids=%d\n", del.num_ids);
320       pm_strcat(jobids, "");
321       for (int j=0; j<1000 && del.num_ids>0; j++) {
322          del.num_ids--;
323          if (del.JobId[i] == 0 || ua->jcr->JobId == del.JobId[i]) {
324             Dmsg2(150, "skip JobId[%d]=%d\n", i, (int)del.JobId[i]);
325             i++;
326             continue;
327          }
328          if (*jobids.c_str() != 0) {
329             pm_strcat(jobids, ",");
330          }
331          pm_strcat(jobids, edit_int64(del.JobId[i++], ed1));
332          Dmsg1(150, "Add id=%s\n", ed1);
333          del.num_del++;
334       }
335       Dmsg1(150, "num_ids=%d\n", del.num_ids);
336       purge_jobs_from_catalog(ua, jobids.c_str());
337    }
338 }
339
340 /*
341  * Delete files from a list of jobs in groups of 1000
342  *  at a time.
343  */
344 void purge_files_from_job_list(UAContext *ua, del_ctx &del)
345 {
346    POOL_MEM jobids(PM_MESSAGE);
347    char ed1[50];
348    /*
349     * OK, now we have the list of JobId's to be pruned, send them
350     *   off to be deleted batched 1000 at a time.
351     */
352    for (int i=0; del.num_ids; ) {
353       pm_strcat(jobids, "");
354       for (int j=0; j<1000 && del.num_ids>0; j++) {
355          del.num_ids--;
356          if (del.JobId[i] == 0 || ua->jcr->JobId == del.JobId[i]) {
357             Dmsg2(150, "skip JobId[%d]=%d\n", i, (int)del.JobId[i]);
358             i++;
359             continue;
360          }
361          if (*jobids.c_str() != 0) {
362             pm_strcat(jobids, ",");
363          }
364          pm_strcat(jobids, edit_int64(del.JobId[i++], ed1));
365          Dmsg1(150, "Add id=%s\n", ed1);
366          del.num_del++;
367       }
368       purge_files_from_jobs(ua, jobids.c_str());
369    }
370 }
371
372 /*
373  * Change the type of the next copy job to backup.
374  * We need to upgrade the next copy of a normal job,
375  * and also upgrade the next copy when the normal job
376  * already have been purged.
377  *
378  *   JobId: 1   PriorJobId: 0    (original)
379  *   JobId: 2   PriorJobId: 1    (first copy)
380  *   JobId: 3   PriorJobId: 1    (second copy)
381  *
382  *   JobId: 2   PriorJobId: 1    (first copy, now regular backup)
383  *   JobId: 3   PriorJobId: 1    (second copy)
384  *
385  *  => Search through PriorJobId in jobid and
386  *                    PriorJobId in PriorJobId (jobid)
387  */
388 void upgrade_copies(UAContext *ua, char *jobs)
389 {
390    POOL_MEM query(PM_MESSAGE);
391    int dbtype = ua->db->bdb_get_type_index();
392
393    db_lock(ua->db);
394    
395    Mmsg(query, uap_upgrade_copies_oldest_job[dbtype], JT_JOB_COPY, jobs, jobs);
396    db_sql_query(ua->db, query.c_str(), NULL, (void *)NULL);
397    Dmsg1(050, "Upgrade copies Log sql=%s\n", query.c_str());
398
399    /* Now upgrade first copy to Backup */
400    Mmsg(query, "UPDATE Job SET Type='B' "      /* JT_JOB_COPY => JT_BACKUP  */
401                 "WHERE JobId IN ( SELECT JobId FROM cpy_tmp )");
402
403    db_sql_query(ua->db, query.c_str(), NULL, (void *)NULL);
404
405    Mmsg(query, "DROP TABLE cpy_tmp");
406    db_sql_query(ua->db, query.c_str(), NULL, (void *)NULL);
407
408    db_unlock(ua->db);
409 }
410
411 /*
412  * Remove all records from catalog for a list of JobIds
413  */
414 void purge_jobs_from_catalog(UAContext *ua, char *jobs)
415 {
416    POOL_MEM query(PM_MESSAGE);
417
418    /* Delete (or purge) records associated with the job */
419    purge_files_from_jobs(ua, jobs);
420
421    Mmsg(query, "DELETE FROM JobMedia WHERE JobId IN (%s)", jobs);
422    db_sql_query(ua->db, query.c_str(), NULL, (void *)NULL);
423    Dmsg1(050, "Delete JobMedia sql=%s\n", query.c_str());
424
425    Mmsg(query, "DELETE FROM Log WHERE JobId IN (%s)", jobs);
426    db_sql_query(ua->db, query.c_str(), NULL, (void *)NULL);
427    Dmsg1(050, "Delete Log sql=%s\n", query.c_str());
428
429    Mmsg(query, "DELETE FROM RestoreObject WHERE JobId IN (%s)", jobs);
430    db_sql_query(ua->db, query.c_str(), NULL, (void *)NULL);
431    Dmsg1(050, "Delete RestoreObject sql=%s\n", query.c_str());
432
433    /* The JobId of the Snapshot record is no longer usable 
434     * TODO: Migth want to use a copy for the jobid? 
435     */
436    Mmsg(query, "UPDATE Snapshot SET JobId=0 WHERE JobId IN (%s)", jobs);
437    db_sql_query(ua->db, query.c_str(), NULL, (void *)NULL);
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    db_list_ctx lst;
459    char *jobids=NULL;
460    int i;
461    bool purged = false;
462    bool stat;
463
464    stat = strcmp(mr->VolStatus, "Append") == 0 ||
465           strcmp(mr->VolStatus, "Full")   == 0 ||
466           strcmp(mr->VolStatus, "Used")   == 0 ||
467           strcmp(mr->VolStatus, "Error")  == 0;
468    if (!stat) {
469       ua->error_msg(_("\nVolume \"%s\" has VolStatus \"%s\" and cannot be purged.\n"
470                      "The VolStatus must be: Append, Full, Used, or Error to be purged.\n"),
471                      mr->VolumeName, mr->VolStatus);
472       return 0;
473    }
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 && is_a_number_list(ua->argv[i])) {
480       jobids = ua->argv[i];
481    } else {
482       /*
483        * Purge ALL JobIds
484        */
485       if (!db_get_volume_jobids(ua->jcr, ua->db, mr, &lst)) {
486          ua->error_msg("%s", db_strerror(ua->db));
487          Dmsg0(050, "Count failed\n");
488          goto bail_out;
489       }
490       jobids = lst.list;
491    }
492
493    if (*jobids) {
494       purge_jobs_from_catalog(ua, jobids);
495    }
496
497    ua->info_msg(_("%d File%s on Volume \"%s\" purged from catalog.\n"),
498                 lst.count, lst.count<=1?"":"s", mr->VolumeName);
499
500    purged = is_volume_purged(ua, mr, force);
501
502 bail_out:
503    return purged;
504 }
505
506 /*
507  * This routine will check the JobMedia records to see if the
508  *   Volume has been purged. If so, it marks it as such and
509  *
510  * Returns: true if volume purged
511  *          false if not
512  *
513  * Note, we normally will not purge a volume that has Firstor LastWritten
514  *   zero, because it means the volume is most likely being written
515  *   however, if the user manually purges using the purge command in
516  *   the console, he has been warned, and we go ahead and purge
517  *   the volume anyway, if possible).
518  */
519 bool is_volume_purged(UAContext *ua, MEDIA_DBR *mr, bool force)
520 {
521    POOL_MEM query(PM_MESSAGE);
522    struct s_count_ctx cnt;
523    bool purged = false;
524    char ed1[50];
525
526    if (!force && (mr->FirstWritten == 0 || mr->LastWritten == 0)) {
527       goto bail_out;               /* not written cannot purge */
528    }
529
530    if (strcmp(mr->VolStatus, "Purged") == 0) {
531       Dmsg1(100, "Volume=%s already purged.\n", mr->VolumeName);
532       purged = true;
533       goto bail_out;
534    }
535
536    /* If purged, mark it so */
537    cnt.count = 0;
538    Mmsg(query, "SELECT 1 FROM JobMedia WHERE MediaId=%s LIMIT 1",
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       Dmsg1(100, "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 /*
560  * Called here to send the appropriate commands to the SD
561  *  to do truncate on purge.
562  */
563 static void do_truncate_on_purge(UAContext *ua, MEDIA_DBR *mr,
564                                  char *pool, char *storage,
565                                  int drive, BSOCK *sd)
566 {
567    bool ok = false;
568    uint64_t VolBytes = 0;
569    uint64_t VolABytes = 0;
570    uint32_t VolType = 0;
571
572    /* TODO: Return if not mr->Recyle ? */
573    if (!mr->Recycle) {
574       return;
575    }
576
577    /* Do it only if action on purge = truncate is set */
578    if (!(mr->ActionOnPurge & ON_PURGE_TRUNCATE)) {
579       return;
580    }
581    /*
582     * Send the command to truncate the volume after purge. If this feature
583     * is disabled for the specific device, this will be a no-op.
584     */
585
586    /* Protect us from spaces */
587    bash_spaces(mr->VolumeName);
588    bash_spaces(mr->MediaType);
589    bash_spaces(pool);
590    bash_spaces(storage);
591
592    /* Do it by relabeling the Volume, which truncates it */
593    sd->fsend("relabel %s OldName=%s NewName=%s PoolName=%s "
594              "MediaType=%s Slot=%d drive=%d\n",
595                 storage,
596                 mr->VolumeName, mr->VolumeName,
597                 pool, mr->MediaType, mr->Slot, drive);
598
599    unbash_spaces(mr->VolumeName);
600    unbash_spaces(mr->MediaType);
601    unbash_spaces(pool);
602    unbash_spaces(storage);
603
604    /* Send relabel command, and check for valid response */
605    while (sd->recv() >= 0) {
606       ua->send_msg("%s", sd->msg);
607       if (sscanf(sd->msg, "3000 OK label. VolBytes=%llu VolABytes=%lld VolType=%d ",
608          &VolBytes, &VolABytes, &VolType) == 3) {
609          ok = true;
610       }
611    }
612
613    if (ok) {
614       mr->VolBytes = VolBytes;
615       mr->VolABytes = VolABytes;
616       mr->VolType = VolType;
617       mr->VolFiles = 0;
618       set_storageid_in_mr(NULL, mr);
619       if (!db_update_media_record(ua->jcr, ua->db, mr)) {
620          ua->error_msg(_("Can't update volume size in the catalog\n"));
621       }
622       ua->send_msg(_("The volume \"%s\" has been truncated\n"), mr->VolumeName);
623    } else {
624       ua->warning_msg(_("Unable to truncate volume \"%s\"\n"), mr->VolumeName);
625    }
626 }
627
628 /*
629  * Implement Bacula bconsole command  purge action
630  *     purge action=truncate pool= volume= storage= mediatype=
631  * or
632  *     truncate pool= volume= storage= mediatype=
633  *
634  * Note, later we might want to rename this action_on_purge_cmd() as
635  *  was the original, but only if we add additional actions such as
636  *  erase, ... For the moment, we only do a truncate.
637  *
638  */
639 int truncate_cmd(UAContext *ua, const char *cmd)
640 {
641    bool allpools = false;
642    int drive = -1;
643    int nb = 0;
644    uint32_t *results = NULL;
645    const char *action = "truncate";
646    STORE *store = NULL;
647    POOL *pool = NULL;
648    MEDIA_DBR mr;
649    POOL_DBR pr;
650    BSOCK *sd = NULL;
651
652    memset(&pr, 0, sizeof(pr));
653
654    /* Look at arguments */
655    for (int i=1; i<ua->argc; i++) {
656       if (strcasecmp(ua->argk[i], NT_("allpools")) == 0) {
657          allpools = true;
658
659       } else if (strcasecmp(ua->argk[i], NT_("volume")) == 0
660                  && is_name_valid(ua->argv[i], NULL)) {
661          bstrncpy(mr.VolumeName, ua->argv[i], sizeof(mr.VolumeName));
662
663       } else if (strcasecmp(ua->argk[i], NT_("mediatype")) == 0
664                  && 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
671                  && is_name_valid(ua->argv[i], NULL)) {
672          action = ua->argv[i];
673       }
674    }
675
676    /* Choose storage */
677    ua->jcr->wstore = store =  get_storage_resource(ua, false);
678    if (!store) {
679       goto bail_out;
680    }
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    set_storageid_in_mr(store, &mr);
710    bstrncpy(mr.VolStatus, "Purged", sizeof(mr.VolStatus));
711    if (!db_get_media_ids(ua->jcr, ua->db, &mr, &nb, &results)) {
712       Dmsg0(100, "No results from db_get_media_ids\n");
713       goto bail_out;
714    }
715
716    if (!nb) {
717       ua->send_msg(_("No Volumes found to perform \"truncate\" command.\n"));
718       goto bail_out;
719    }
720
721    if ((sd=open_sd_bsock(ua)) == NULL) {
722       Dmsg0(100, "Can't open connection to sd\n");
723       goto bail_out;
724    }
725
726    /*
727     * Loop over the candidate Volumes and actually truncate them
728     */
729    for (int i=0; i < nb; i++) {
730       mr.clear();
731       mr.MediaId = results[i];
732       if (db_get_media_record(ua->jcr, ua->db, &mr)) {
733          /* TODO: ask for drive and change Pool */
734          if (strcasecmp("truncate", action) == 0) {
735             do_truncate_on_purge(ua, &mr, pr.Name, store->dev_name(), drive, sd);
736          }
737       } else {
738          Dmsg1(0, "Can't find MediaId=%lld\n", (uint64_t)mr.MediaId);
739       }
740    }
741
742 bail_out:
743    close_db(ua);
744    close_sd_bsock(ua);
745    ua->jcr->wstore = NULL;
746    if (results) {
747       free(results);
748    }
749
750    return 1;
751 }
752
753 /*
754  * IF volume status is Append, Full, Used, or Error, mark it Purged
755  *   Purged volumes can then be recycled (if enabled).
756  */
757 bool mark_media_purged(UAContext *ua, MEDIA_DBR *mr)
758 {
759    JCR *jcr = ua->jcr;
760    if (strcmp(mr->VolStatus, "Append") == 0 ||
761        strcmp(mr->VolStatus, "Full")   == 0 ||
762        strcmp(mr->VolStatus, "Used")   == 0 ||
763        strcmp(mr->VolStatus, "Error")  == 0) {
764       bstrncpy(mr->VolStatus, "Purged", sizeof(mr->VolStatus));
765       set_storageid_in_mr(NULL, mr);
766       if (!db_update_media_record(jcr, ua->db, mr)) {
767          return false;
768       }
769       pm_strcpy(jcr->VolumeName, mr->VolumeName);
770       generate_plugin_event(jcr, bDirEventVolumePurged);
771       /*
772        * If the RecyclePool is defined, move the volume there
773        */
774       if (mr->RecyclePoolId && mr->RecyclePoolId != mr->PoolId) {
775          POOL_DBR oldpr, newpr;
776          memset(&oldpr, 0, sizeof(POOL_DBR));
777          memset(&newpr, 0, sizeof(POOL_DBR));
778          newpr.PoolId = mr->RecyclePoolId;
779          oldpr.PoolId = mr->PoolId;
780          if (   db_get_pool_numvols(jcr, ua->db, &oldpr)
781              && db_get_pool_numvols(jcr, ua->db, &newpr)) {
782             /* check if destination pool size is ok */
783             if (newpr.MaxVols > 0 && newpr.NumVols >= newpr.MaxVols) {
784                ua->error_msg(_("Unable move recycled Volume in full "
785                               "Pool \"%s\" MaxVols=%d\n"),
786                         newpr.Name, newpr.MaxVols);
787
788             } else {            /* move media */
789                update_vol_pool(ua, newpr.Name, mr, &oldpr);
790             }
791          } else {
792             ua->error_msg("%s", db_strerror(ua->db));
793          }
794       }
795
796       /* Send message to Job report, if it is a *real* job */
797       if (jcr && jcr->JobId > 0) {
798          Jmsg(jcr, M_INFO, 0, _("All records pruned from Volume \"%s\"; marking it \"Purged\"\n"),
799             mr->VolumeName);
800       }
801       return true;
802    } else {
803       ua->error_msg(_("Cannot purge Volume with VolStatus=%s\n"), mr->VolStatus);
804    }
805    return strcmp(mr->VolStatus, "Purged") == 0;
806 }