]> git.sur5r.net Git - bacula/bacula/blob - bacula/src/dird/ua_purge.c
408af578d3145263f69d735f2399a1593edab715
[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
6    The original author of Bacula is Kern Sibbald, with contributions
7    from many others, a complete list can be found in the file AUTHORS.
8
9    You may use this file and others of this release according to the
10    license defined in the LICENSE file, which includes the Affero General
11    Public License, v3.0 ("AGPLv3") and some additional permissions and
12    terms pursuant to its AGPLv3 Section 7.
13
14    This notice must be preserved when any source code is 
15    conveyed and/or propagated.
16
17    Bacula(R) is a registered trademark of Kern Sibbald.
18 */
19 /*
20  *
21  *   Bacula Director -- User Agent Database Purge Command
22  *
23  *      Purges Files from specific JobIds
24  * or
25  *      Purges Jobs from Volumes
26  *
27  *     Kern Sibbald, February MMII
28  *
29  */
30
31 #include "bacula.h"
32 #include "dird.h"
33
34 /* Forward referenced functions */
35 static int purge_files_from_client(UAContext *ua, CLIENT *client);
36 static int purge_jobs_from_client(UAContext *ua, CLIENT *client);
37 int truncate_cmd(UAContext *ua, const char *cmd);
38
39 static const char *select_jobsfiles_from_client =
40    "SELECT JobId FROM Job "
41    "WHERE ClientId=%s "
42    "AND PurgedFiles=0";
43
44 static const char *select_jobs_from_client =
45    "SELECT JobId, PurgedFiles FROM Job "
46    "WHERE ClientId=%s";
47
48 /*
49  *   Purge records from database
50  *
51  *     Purge Files (from) [Job|JobId|Client|Volume]
52  *     Purge Jobs  (from) [Client|Volume]
53  *     Purge Volumes
54  *
55  *  N.B. Not all above is implemented yet.
56  */
57 int purge_cmd(UAContext *ua, const char *cmd)
58 {
59    int i;
60    CLIENT *client;
61    MEDIA_DBR mr;
62    JOB_DBR  jr;
63    memset(&jr, 0, sizeof(jr));
64
65    static const char *keywords[] = {
66       NT_("files"),
67       NT_("jobs"),
68       NT_("volume"),
69       NULL};
70
71    static const char *files_keywords[] = {
72       NT_("Job"),
73       NT_("JobId"),
74       NT_("Client"),
75       NT_("Volume"),
76       NULL};
77
78    static const char *jobs_keywords[] = {
79       NT_("Client"),
80       NT_("Volume"),
81       NULL};
82
83    /* Special case for the "Action On Purge", this option is working only on
84     * Purged volume, so no jobs or files will be purged.
85     * We are skiping this message if "purge volume action=xxx"
86     */
87    if (!(find_arg(ua, "volume") >= 0 && find_arg(ua, "action") >= 0)) {
88       ua->warning_msg(_(
89         "\nThis command can be DANGEROUS!!!\n\n"
90         "It purges (deletes) all Files from a Job,\n"
91         "JobId, Client or Volume; or it purges (deletes)\n"
92         "all Jobs from a Client or Volume without regard\n"
93         "to retention periods. Normally you should use the\n"
94         "PRUNE command, which respects retention periods.\n"));
95    }
96
97    if (!open_new_client_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 truncate_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_del == 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_del,
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_del == 0) {
263       ua->warning_msg(_("No Jobs 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_del,
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    Mmsg(query, "DELETE FROM PathVisibility WHERE JobId IN (%s)", jobs);
296    db_sql_query(ua->db, query.c_str(), NULL, (void *)NULL);
297    Dmsg1(050, "Delete PathVisibility sql=%s\n", query.c_str());
298
299    /*
300     * Now mark Job as having files purged. This is necessary to
301     * avoid having too many Jobs to process in future prunings. If
302     * we don't do this, the number of JobId's in our in memory list
303     * could grow very large.
304     */
305    Mmsg(query, "UPDATE Job SET PurgedFiles=1 WHERE JobId IN (%s)", jobs);
306    db_sql_query(ua->db, query.c_str(), NULL, (void *)NULL);
307    Dmsg1(050, "Mark purged sql=%s\n", query.c_str());
308 }
309
310 /*
311  * Delete jobs (all records) from the catalog in groups of 1000
312  *  at a time.
313  */
314 void purge_job_list_from_catalog(UAContext *ua, del_ctx &del)
315 {
316    POOL_MEM jobids(PM_MESSAGE);
317    char ed1[50];
318
319    for (int i=0; del.num_ids; ) {
320       Dmsg1(150, "num_ids=%d\n", del.num_ids);
321       pm_strcat(jobids, "");
322       for (int j=0; j<1000 && del.num_ids>0; j++) {
323          del.num_ids--;
324          if (del.JobId[i] == 0 || ua->jcr->JobId == del.JobId[i]) {
325             Dmsg2(150, "skip JobId[%d]=%d\n", i, (int)del.JobId[i]);
326             i++;
327             continue;
328          }
329          if (*jobids.c_str() != 0) {
330             pm_strcat(jobids, ",");
331          }
332          pm_strcat(jobids, edit_int64(del.JobId[i++], ed1));
333          Dmsg1(150, "Add id=%s\n", ed1);
334          del.num_del++;
335       }
336       Dmsg1(150, "num_ids=%d\n", del.num_ids);
337       purge_jobs_from_catalog(ua, jobids.c_str());
338    }
339 }
340
341 /*
342  * Delete files from a list of jobs in groups of 1000
343  *  at a time.
344  */
345 void purge_files_from_job_list(UAContext *ua, del_ctx &del)
346 {
347    POOL_MEM jobids(PM_MESSAGE);
348    char ed1[50];
349    /*
350     * OK, now we have the list of JobId's to be pruned, send them
351     *   off to be deleted batched 1000 at a time.
352     */
353    for (int i=0; del.num_ids; ) {
354       pm_strcat(jobids, "");
355       for (int j=0; j<1000 && del.num_ids>0; j++) {
356          del.num_ids--;
357          if (del.JobId[i] == 0 || ua->jcr->JobId == del.JobId[i]) {
358             Dmsg2(150, "skip JobId[%d]=%d\n", i, (int)del.JobId[i]);
359             i++;
360             continue;
361          }
362          if (*jobids.c_str() != 0) {
363             pm_strcat(jobids, ",");
364          }
365          pm_strcat(jobids, edit_int64(del.JobId[i++], ed1));
366          Dmsg1(150, "Add id=%s\n", ed1);
367          del.num_del++;
368       }
369       purge_files_from_jobs(ua, jobids.c_str());
370    }
371 }
372
373 /*
374  * Change the type of the next copy job to backup.
375  * We need to upgrade the next copy of a normal job,
376  * and also upgrade the next copy when the normal job
377  * already have been purged.
378  *
379  *   JobId: 1   PriorJobId: 0    (original)
380  *   JobId: 2   PriorJobId: 1    (first copy)
381  *   JobId: 3   PriorJobId: 1    (second copy)
382  *
383  *   JobId: 2   PriorJobId: 1    (first copy, now regular backup)
384  *   JobId: 3   PriorJobId: 1    (second copy)
385  *
386  *  => Search through PriorJobId in jobid and
387  *                    PriorJobId in PriorJobId (jobid)
388  */
389 void upgrade_copies(UAContext *ua, char *jobs)
390 {
391    POOL_MEM query(PM_MESSAGE);
392    int dbtype = ua->db->bdb_get_type_index();
393
394    db_lock(ua->db);
395    
396    Mmsg(query, uap_upgrade_copies_oldest_job[dbtype], JT_JOB_COPY, jobs, jobs);
397    db_sql_query(ua->db, query.c_str(), NULL, (void *)NULL);
398    Dmsg1(050, "Upgrade copies Log sql=%s\n", query.c_str());
399
400    /* Now upgrade first copy to Backup */
401    Mmsg(query, "UPDATE Job SET Type='B' "      /* JT_JOB_COPY => JT_BACKUP  */
402                 "WHERE JobId IN ( SELECT JobId FROM cpy_tmp )");
403
404    db_sql_query(ua->db, query.c_str(), NULL, (void *)NULL);
405
406    Mmsg(query, "DROP TABLE cpy_tmp");
407    db_sql_query(ua->db, query.c_str(), NULL, (void *)NULL);
408
409    db_unlock(ua->db);
410 }
411
412 /*
413  * Remove all records from catalog for a list of JobIds
414  */
415 void purge_jobs_from_catalog(UAContext *ua, char *jobs)
416 {
417    POOL_MEM query(PM_MESSAGE);
418
419    /* Delete (or purge) records associated with the job */
420    purge_files_from_jobs(ua, jobs);
421
422    Mmsg(query, "DELETE FROM JobMedia WHERE JobId IN (%s)", jobs);
423    db_sql_query(ua->db, query.c_str(), NULL, (void *)NULL);
424    Dmsg1(050, "Delete JobMedia sql=%s\n", query.c_str());
425
426    Mmsg(query, "DELETE FROM Log WHERE JobId IN (%s)", jobs);
427    db_sql_query(ua->db, query.c_str(), NULL, (void *)NULL);
428    Dmsg1(050, "Delete Log sql=%s\n", query.c_str());
429
430    Mmsg(query, "DELETE FROM RestoreObject WHERE JobId IN (%s)", jobs);
431    db_sql_query(ua->db, query.c_str(), NULL, (void *)NULL);
432    Dmsg1(050, "Delete RestoreObject sql=%s\n", query.c_str());
433
434    /* The JobId of the Snapshot record is no longer usable 
435     * TODO: Migth want to use a copy for the jobid? 
436     */
437    Mmsg(query, "UPDATE Snapshot SET JobId=0 WHERE JobId IN (%s)", jobs);
438    db_sql_query(ua->db, query.c_str(), NULL, (void *)NULL);
439
440    upgrade_copies(ua, jobs);
441
442    /* Now remove the Job record itself */
443    Mmsg(query, "DELETE FROM Job WHERE JobId IN (%s)", jobs);
444    db_sql_query(ua->db, query.c_str(), NULL, (void *)NULL);
445
446    Dmsg1(050, "Delete Job sql=%s\n", query.c_str());
447 }
448
449 void purge_files_from_volume(UAContext *ua, MEDIA_DBR *mr )
450 {} /* ***FIXME*** implement */
451
452 /*
453  * Returns: 1 if Volume purged
454  *          0 if Volume not purged
455  */
456 bool purge_jobs_from_volume(UAContext *ua, MEDIA_DBR *mr, bool force)
457 {
458    POOL_MEM query(PM_MESSAGE);
459    db_list_ctx lst;
460    char *jobids=NULL;
461    int i;
462    bool purged = false;
463    bool stat;
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    /*
477     * Check if he wants to purge a single jobid
478     */
479    i = find_arg_with_value(ua, "jobid");
480    if (i >= 0 && is_a_number_list(ua->argv[i])) {
481       jobids = ua->argv[i];
482    } else {
483       /*
484        * Purge ALL JobIds
485        */
486       if (!db_get_volume_jobids(ua->jcr, ua->db, mr, &lst)) {
487          ua->error_msg("%s", db_strerror(ua->db));
488          Dmsg0(050, "Count failed\n");
489          goto bail_out;
490       }
491       jobids = lst.list;
492    }
493
494    if (*jobids) {
495       purge_jobs_from_catalog(ua, jobids);
496    }
497
498    ua->info_msg(_("%d File%s on Volume \"%s\" purged from catalog.\n"),
499                 lst.count, lst.count<=1?"":"s", mr->VolumeName);
500
501    purged = is_volume_purged(ua, mr, force);
502
503 bail_out:
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       Dmsg1(100, "Volume=%s already purged.\n", mr->VolumeName);
533       purged = true;
534       goto bail_out;
535    }
536
537    /* If purged, mark it so */
538    cnt.count = 0;
539    Mmsg(query, "SELECT 1 FROM JobMedia WHERE MediaId=%s LIMIT 1",
540         edit_int64(mr->MediaId, ed1));
541    if (!db_sql_query(ua->db, query.c_str(), del_count_handler, (void *)&cnt)) {
542       ua->error_msg("%s", db_strerror(ua->db));
543       Dmsg0(050, "Count failed\n");
544       goto bail_out;
545    }
546
547    if (cnt.count == 0) {
548       ua->warning_msg(_("There are no more Jobs associated with Volume \"%s\". Marking it purged.\n"),
549          mr->VolumeName);
550       Dmsg1(100, "There are no more Jobs associated with Volume \"%s\". Marking it purged.\n",
551          mr->VolumeName);
552       if (!(purged = mark_media_purged(ua, mr))) {
553          ua->error_msg("%s", db_strerror(ua->db));
554       }
555    }
556 bail_out:
557    return purged;
558 }
559
560 /*
561  * Called here to send the appropriate commands to the SD
562  *  to do truncate on purge.
563  */
564 static void do_truncate_on_purge(UAContext *ua, MEDIA_DBR *mr,
565                                  char *pool, char *storage,
566                                  int drive, BSOCK *sd)
567 {
568    bool ok = false;
569    uint64_t VolBytes = 0;
570    uint64_t VolABytes = 0;
571    uint32_t VolType = 0;
572
573    /* TODO: Return if not mr->Recyle ? */
574    if (!mr->Recycle) {
575       return;
576    }
577
578    /* Do it only if action on purge = truncate is set */
579    if (!(mr->ActionOnPurge & ON_PURGE_TRUNCATE)) {
580       ua->error_msg(_("\nThe option \"Action On Purge = Truncate\" was not defined in the Pool resource.\n"
581          "Unable to truncate volume \"%s\"\n"), mr->VolumeName);
582       return;
583    }
584    /*
585     * Send the command to truncate the volume after purge. If this feature
586     * is disabled for the specific device, this will be a no-op.
587     */
588
589    /* Protect us from spaces */
590    bash_spaces(mr->VolumeName);
591    bash_spaces(mr->MediaType);
592    bash_spaces(pool);
593    bash_spaces(storage);
594
595    /* Do it by relabeling the Volume, which truncates it */
596    sd->fsend("relabel %s OldName=%s NewName=%s PoolName=%s "
597              "MediaType=%s Slot=%d drive=%d\n",
598                 storage,
599                 mr->VolumeName, mr->VolumeName,
600                 pool, mr->MediaType, mr->Slot, drive);
601
602    unbash_spaces(mr->VolumeName);
603    unbash_spaces(mr->MediaType);
604    unbash_spaces(pool);
605    unbash_spaces(storage);
606
607    /* Send relabel command, and check for valid response */
608    while (sd->recv() >= 0) {
609       ua->send_msg("%s", sd->msg);
610       if (sscanf(sd->msg, "3000 OK label. VolBytes=%llu VolABytes=%lld VolType=%d ",
611          &VolBytes, &VolABytes, &VolType) == 3) {
612          ok = true;
613       }
614    }
615
616    if (ok) {
617       mr->VolBytes = VolBytes;
618       mr->VolABytes = VolABytes;
619       mr->VolType = VolType;
620       mr->VolFiles = 0;
621       set_storageid_in_mr(NULL, mr);
622       if (!db_update_media_record(ua->jcr, ua->db, mr)) {
623          ua->error_msg(_("Can't update volume size in the catalog\n"));
624       }
625       ua->send_msg(_("The volume \"%s\" has been truncated\n"), mr->VolumeName);
626    } else {
627       ua->warning_msg(_("Unable to truncate volume \"%s\"\n"), mr->VolumeName);
628    }
629 }
630
631 /*
632  * Implement Bacula bconsole command  purge action
633  *     purge action=truncate pool= volume= storage= mediatype=
634  * or
635  *     truncate pool= volume= storage= mediatype=
636  *
637  * Note, later we might want to rename this action_on_purge_cmd() as
638  *  was the original, but only if we add additional actions such as
639  *  erase, ... For the moment, we only do a truncate.
640  *
641  */
642 int truncate_cmd(UAContext *ua, const char *cmd)
643 {
644    bool allpools = false;
645    int drive = -1;
646    int nb = 0;
647    uint32_t *results = NULL;
648    const char *action = "truncate";
649    STORE *store = NULL;
650    POOL *pool = NULL;
651    MEDIA_DBR mr;
652    POOL_DBR pr;
653    BSOCK *sd = NULL;
654
655    memset(&pr, 0, sizeof(pr));
656
657    /* Look at arguments */
658    for (int i=1; i<ua->argc; i++) {
659       if (strcasecmp(ua->argk[i], NT_("allpools")) == 0) {
660          allpools = true;
661
662       } else if (strcasecmp(ua->argk[i], NT_("volume")) == 0
663                  && is_name_valid(ua->argv[i], NULL)) {
664          bstrncpy(mr.VolumeName, ua->argv[i], sizeof(mr.VolumeName));
665
666       } else if (strcasecmp(ua->argk[i], NT_("mediatype")) == 0
667                  && ua->argv[i]) {
668          bstrncpy(mr.MediaType, ua->argv[i], sizeof(mr.MediaType));
669
670       } else if (strcasecmp(ua->argk[i], NT_("drive")) == 0 && ua->argv[i]) {
671          drive = atoi(ua->argv[i]);
672
673       } else if (strcasecmp(ua->argk[i], NT_("action")) == 0
674                  && is_name_valid(ua->argv[i], NULL)) {
675          action = ua->argv[i];
676       }
677    }
678
679    /* Choose storage */
680    ua->jcr->wstore = store =  get_storage_resource(ua, false);
681    if (!store) {
682       goto bail_out;
683    }
684
685    if (!open_db(ua)) {
686       Dmsg0(100, "Can't open db\n");
687       goto bail_out;
688    }
689
690    if (!allpools) {
691       /* force pool selection */
692       pool = get_pool_resource(ua);
693       if (!pool) {
694          Dmsg0(100, "Can't get pool resource\n");
695          goto bail_out;
696       }
697       bstrncpy(pr.Name, pool->name(), sizeof(pr.Name));
698       if (!db_get_pool_record(ua->jcr, ua->db, &pr)) {
699          Dmsg0(100, "Can't get pool record\n");
700          goto bail_out;
701       }
702       mr.PoolId = pr.PoolId;
703    }
704
705    /*
706     * Look for all Purged volumes that can be recycled, are enabled and
707     *  have more the 10,000 bytes.
708     */
709    mr.Recycle = 1;
710    mr.Enabled = 1;
711    mr.VolBytes = 200;
712    set_storageid_in_mr(store, &mr);
713    bstrncpy(mr.VolStatus, "Purged", sizeof(mr.VolStatus));
714    if (!db_get_media_ids(ua->jcr, ua->db, &mr, &nb, &results)) {
715       Dmsg0(100, "No results from db_get_media_ids\n");
716       goto bail_out;
717    }
718
719    if (!nb) {
720       ua->send_msg(_("No Volumes found to perform \"truncate\" command.\n"));
721       goto bail_out;
722    }
723
724    if ((sd=open_sd_bsock(ua)) == NULL) {
725       Dmsg0(100, "Can't open connection to sd\n");
726       goto bail_out;
727    }
728
729    /*
730     * Loop over the candidate Volumes and actually truncate them
731     */
732    for (int i=0; i < nb; i++) {
733       mr.clear();
734       mr.MediaId = results[i];
735       if (db_get_media_record(ua->jcr, ua->db, &mr)) {
736          /* TODO: ask for drive and change Pool */
737          if (strcasecmp("truncate", action) == 0) {
738             do_truncate_on_purge(ua, &mr, pr.Name, store->dev_name(), drive, sd);
739          }
740       } else {
741          Dmsg1(0, "Can't find MediaId=%lld\n", (uint64_t)mr.MediaId);
742       }
743    }
744
745 bail_out:
746    close_db(ua);
747    close_sd_bsock(ua);
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       set_storageid_in_mr(NULL, mr);
769       if (!db_update_media_record(jcr, ua->db, mr)) {
770          return false;
771       }
772       pm_strcpy(jcr->VolumeName, mr->VolumeName);
773       generate_plugin_event(jcr, bDirEventVolumePurged);
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_numvols(jcr, ua->db, &oldpr)
784              && db_get_pool_numvols(jcr, ua->db, &newpr)) {
785             /* check if destination pool size is ok */
786             if (newpr.MaxVols > 0 && newpr.NumVols >= newpr.MaxVols) {
787                ua->error_msg(_("Unable move recycled Volume in full "
788                               "Pool \"%s\" MaxVols=%d\n"),
789                         newpr.Name, newpr.MaxVols);
790
791             } else {            /* move media */
792                update_vol_pool(ua, newpr.Name, mr, &oldpr);
793             }
794          } else {
795             ua->error_msg("%s", db_strerror(ua->db));
796          }
797       }
798
799       /* Send message to Job report, if it is a *real* job */
800       if (jcr && jcr->JobId > 0) {
801          Jmsg(jcr, M_INFO, 0, _("All records pruned from Volume \"%s\"; marking it \"Purged\"\n"),
802             mr->VolumeName);
803       }
804       return true;
805    } else {
806       ua->error_msg(_("Cannot purge Volume with VolStatus=%s\n"), mr->VolStatus);
807    }
808    return strcmp(mr->VolStatus, "Purged") == 0;
809 }