]> git.sur5r.net Git - bacula/bacula/blob - bacula/src/dird/ua_prune.c
Big backport from Enterprise
[bacula/bacula] / bacula / src / dird / ua_prune.c
1 /*
2    Bacula(R) - The Network Backup Solution
3
4    Copyright (C) 2000-2017 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  *   Bacula Director -- User Agent Database prune Command
21  *      Applies retention periods
22  *
23  *     Kern Sibbald, February MMII
24  */
25
26 #include "bacula.h"
27 #include "dird.h"
28
29 /* Imported functions */
30
31 /* Forward referenced functions */
32 static bool grow_del_list(struct del_ctx *del);
33 static bool prune_expired_volumes(UAContext*);
34
35 /*
36  * Called here to count entries to be deleted
37  */
38 int del_count_handler(void *ctx, int num_fields, char **row)
39 {
40    struct s_count_ctx *cnt = (struct s_count_ctx *)ctx;
41
42    if (row[0]) {
43       cnt->count = str_to_int64(row[0]);
44    } else {
45       cnt->count = 0;
46    }
47    return 0;
48 }
49
50
51 /*
52  * Called here to make in memory list of JobIds to be
53  *  deleted and the associated PurgedFiles flag.
54  *  The in memory list will then be transversed
55  *  to issue the SQL DELETE commands.  Note, the list
56  *  is allowed to get to MAX_DEL_LIST_LEN to limit the
57  *  maximum malloc'ed memory.
58  */
59 int job_delete_handler(void *ctx, int num_fields, char **row)
60 {
61    struct del_ctx *del = (struct del_ctx *)ctx;
62
63    if (!grow_del_list(del)) {
64       return 1;
65    }
66    del->JobId[del->num_ids] = (JobId_t)str_to_int64(row[0]);
67    Dmsg2(60, "job_delete_handler row=%d val=%d\n", del->num_ids, del->JobId[del->num_ids]);
68    del->PurgedFiles[del->num_ids++] = (char)str_to_int64(row[1]);
69    return 0;
70 }
71
72 int file_delete_handler(void *ctx, int num_fields, char **row)
73 {
74    struct del_ctx *del = (struct del_ctx *)ctx;
75
76    if (!grow_del_list(del)) {
77       return 1;
78    }
79    del->JobId[del->num_ids++] = (JobId_t)str_to_int64(row[0]);
80 // Dmsg2(150, "row=%d val=%d\n", del->num_ids-1, del->JobId[del->num_ids-1]);
81    return 0;
82 }
83
84 /*
85  *   Prune records from database
86  *
87  *    prune files (from) client=xxx [pool=yyy]
88  *    prune jobs (from) client=xxx [pool=yyy]
89  *    prune volume=xxx
90  *    prune stats
91  */
92 int prunecmd(UAContext *ua, const char *cmd)
93 {
94    DIRRES *dir;
95    CLIENT *client;
96    POOL *pool;
97    POOL_DBR pr;
98    MEDIA_DBR mr;
99    utime_t retention;
100    int kw;
101
102    static const char *keywords[] = {
103       NT_("Files"),
104       NT_("Jobs"),
105       NT_("Volume"),
106       NT_("Stats"),
107       NT_("Snapshots"),
108       NULL};
109
110    if (!open_new_client_db(ua)) {
111       return false;
112    }
113
114    /* First search args */
115    kw = find_arg_keyword(ua, keywords);
116    if (kw < 0 || kw > 4) {
117       /* no args, so ask user */
118       kw = do_keyword_prompt(ua, _("Choose item to prune"), keywords);
119    }
120
121    switch (kw) {
122    case 0:  /* prune files */
123       /* We restrict the client list to ClientAcl, maybe something to change later */
124       if (!(client = get_client_resource(ua, JT_SYSTEM))) {
125          return false;
126       }
127       if (find_arg_with_value(ua, "pool") >= 0) {
128          pool = get_pool_resource(ua);
129       } else {
130          pool = NULL;
131       }
132       /* Pool File Retention takes precedence over client File Retention */
133       if (pool && pool->FileRetention > 0) {
134          if (!confirm_retention(ua, &pool->FileRetention, "File")) {
135             return false;
136          }
137       } else if (!confirm_retention(ua, &client->FileRetention, "File")) {
138          return false;
139       }
140       prune_files(ua, client, pool);
141       return true;
142
143    case 1:  /* prune jobs */
144       /* We restrict the client list to ClientAcl, maybe something to change later */
145       if (!(client = get_client_resource(ua, JT_SYSTEM))) {
146          return false;
147       }
148       if (find_arg_with_value(ua, "pool") >= 0) {
149          pool = get_pool_resource(ua);
150       } else {
151          pool = NULL;
152       }
153       /* Pool Job Retention takes precedence over client Job Retention */
154       if (pool && pool->JobRetention > 0) {
155          if (!confirm_retention(ua, &pool->JobRetention, "Job")) {
156             return false;
157          }
158       } else if (!confirm_retention(ua, &client->JobRetention, "Job")) {
159          return false;
160       }
161       /* ****FIXME**** allow user to select JobType */
162       prune_jobs(ua, client, pool, JT_BACKUP);
163       return 1;
164
165    case 2:  /* prune volume */
166
167       /* Look for All expired volumes, mostly designed for runscript */
168       if (find_arg(ua, "expired") >= 0) {
169          return prune_expired_volumes(ua);
170       }
171
172       if (!select_pool_and_media_dbr(ua, &pr, &mr)) {
173          return false;
174       }
175       if (mr.Enabled == 2) {
176          ua->error_msg(_("Cannot prune Volume \"%s\" because it is archived.\n"),
177             mr.VolumeName);
178          return false;
179       }
180       if (!confirm_retention(ua, &mr.VolRetention, "Volume")) {
181          return false;
182       }
183       prune_volume(ua, &mr);
184       return true;
185    case 3:  /* prune stats */
186       dir = (DIRRES *)GetNextRes(R_DIRECTOR, NULL);
187       if (!dir->stats_retention) {
188          return false;
189       }
190       retention = dir->stats_retention;
191       if (!confirm_retention(ua, &retention, "Statistics")) {
192          return false;
193       }
194       prune_stats(ua, retention);
195       return true;
196    case 4:  /* prune snapshots */
197       prune_snapshot(ua);
198       return true;
199    default:
200       break;
201    }
202
203    return true;
204 }
205
206 /* Prune Job stat records from the database.
207  *
208  */
209 int prune_stats(UAContext *ua, utime_t retention)
210 {
211    char ed1[50];
212    POOL_MEM query(PM_MESSAGE);
213    utime_t now = (utime_t)time(NULL);
214
215    db_lock(ua->db);
216    Mmsg(query, "DELETE FROM JobHisto WHERE JobTDate < %s",
217         edit_int64(now - retention, ed1));
218    db_sql_query(ua->db, query.c_str(), NULL, NULL);
219    db_unlock(ua->db);
220
221    ua->info_msg(_("Pruned Jobs from JobHisto catalog.\n"));
222
223    return true;
224 }
225
226 /*
227  * Use pool and client specified by user to select jobs to prune
228  * returns add_from string to add in FROM clause
229  *         add_where string to add in WHERE clause
230  */
231 bool prune_set_filter(UAContext *ua, CLIENT *client, POOL *pool, utime_t period,
232                       POOL_MEM *add_from, POOL_MEM *add_where)
233 {
234    utime_t now;
235    char ed1[50], ed2[MAX_ESCAPE_NAME_LENGTH];
236    POOL_MEM tmp(PM_MESSAGE);
237
238    now = (utime_t)time(NULL);
239    edit_int64(now - period, ed1);
240    Dmsg3(150, "now=%lld period=%lld JobTDate=%s\n", now, period, ed1);
241    Mmsg(tmp, " AND JobTDate < %s ", ed1);
242    pm_strcat(*add_where, tmp.c_str());
243
244    db_lock(ua->db);
245    if (client) {
246       db_escape_string(ua->jcr, ua->db, ed2,
247          client->name(), strlen(client->name()));
248       Mmsg(tmp, " AND Client.Name = '%s' ", ed2);
249       pm_strcat(*add_where, tmp.c_str());
250       pm_strcat(*add_from, " JOIN Client USING (ClientId) ");
251    }
252
253    if (pool) {
254       db_escape_string(ua->jcr, ua->db, ed2,
255               pool->name(), strlen(pool->name()));
256       Mmsg(tmp, " AND Pool.Name = '%s' ", ed2);
257       pm_strcat(*add_where, tmp.c_str());
258       /* Use ON() instead of USING for some old SQLite */
259       pm_strcat(*add_from, " JOIN Pool ON (Job.PoolId = Pool.PoolId) ");
260    }
261    Dmsg2(150, "f=%s w=%s\n", add_from->c_str(), add_where->c_str());
262    db_unlock(ua->db);
263    return true;
264 }
265
266 /*
267  * Prune File records from the database. For any Job which
268  * is older than the retention period, we unconditionally delete
269  * all File records for that Job.  This is simple enough that no
270  * temporary tables are needed. We simply make an in memory list of
271  * the JobIds meeting the prune conditions, then delete all File records
272  * pointing to each of those JobIds.
273  *
274  * This routine assumes you want the pruning to be done. All checking
275  *  must be done before calling this routine.
276  *
277  * Note: client or pool can possibly be NULL (not both).
278  */
279 int prune_files(UAContext *ua, CLIENT *client, POOL *pool)
280 {
281    struct del_ctx del;
282    struct s_count_ctx cnt;
283    POOL_MEM query(PM_MESSAGE);
284    POOL_MEM sql_where(PM_MESSAGE);
285    POOL_MEM sql_from(PM_MESSAGE);
286    utime_t period;
287    char ed1[50];
288
289    memset(&del, 0, sizeof(del));
290
291    if (pool && pool->FileRetention > 0) {
292       period = pool->FileRetention;
293
294    } else if (client) {
295       period = client->FileRetention;
296
297    } else {                     /* should specify at least pool or client */
298       return false;
299    }
300
301    db_lock(ua->db);
302    /* Specify JobTDate and Pool.Name= and/or Client.Name= in the query */
303    if (!prune_set_filter(ua, client, pool, period, &sql_from, &sql_where)) {
304       goto bail_out;
305    }
306
307 //   edit_utime(now-period, ed1, sizeof(ed1));
308 //   Jmsg(ua->jcr, M_INFO, 0, _("Begin pruning Jobs older than %s secs.\n"), ed1);
309    Jmsg(ua->jcr, M_INFO, 0, _("Begin pruning Files.\n"));
310    /* Select Jobs -- for counting */
311    Mmsg(query,
312         "SELECT COUNT(1) FROM Job %s WHERE PurgedFiles=0 %s",
313         sql_from.c_str(), sql_where.c_str());
314    Dmsg1(100, "select sql=%s\n", query.c_str());
315    cnt.count = 0;
316    if (!db_sql_query(ua->db, query.c_str(), del_count_handler, (void *)&cnt)) {
317       ua->error_msg("%s", db_strerror(ua->db));
318       Dmsg0(100, "Count failed\n");
319       goto bail_out;
320    }
321
322    if (cnt.count == 0) {
323       if (ua->verbose) {
324          ua->warning_msg(_("No Files found to prune.\n"));
325       }
326       goto bail_out;
327    }
328
329    if (cnt.count < MAX_DEL_LIST_LEN) {
330       del.max_ids = cnt.count + 1;
331    } else {
332       del.max_ids = MAX_DEL_LIST_LEN;
333    }
334    del.tot_ids = 0;
335
336    del.JobId = (JobId_t *)malloc(sizeof(JobId_t) * del.max_ids);
337
338    /* Now process same set but making a delete list */
339    Mmsg(query, "SELECT JobId FROM Job %s WHERE PurgedFiles=0 %s",
340         sql_from.c_str(), sql_where.c_str());
341    Dmsg1(100, "select sql=%s\n", query.c_str());
342    db_sql_query(ua->db, query.c_str(), file_delete_handler, (void *)&del);
343
344    purge_files_from_job_list(ua, del);
345
346    edit_uint64_with_commas(del.num_del, ed1);
347    ua->info_msg(_("Pruned Files from %s Jobs for client %s from catalog.\n"),
348       ed1, client->name());
349
350 bail_out:
351    db_unlock(ua->db);
352    if (del.JobId) {
353       free(del.JobId);
354    }
355    return 1;
356 }
357
358
359 static void drop_temp_tables(UAContext *ua)
360 {
361    int i;
362    for (i=0; drop_deltabs[i]; i++) {
363       db_sql_query(ua->db, drop_deltabs[i], NULL, (void *)NULL);
364    }
365 }
366
367 static bool create_temp_tables(UAContext *ua)
368 {
369    /* Create temp tables and indicies */
370    if (!db_sql_query(ua->db, create_deltabs[ua->db->bdb_get_type_index()], NULL, (void *)NULL)) {
371       ua->error_msg("%s", db_strerror(ua->db));
372       Dmsg0(100, "create DelTables table failed\n");
373       return false;
374    }
375    if (!db_sql_query(ua->db, create_delindex, NULL, (void *)NULL)) {
376        ua->error_msg("%s", db_strerror(ua->db));
377        Dmsg0(100, "create DelInx1 index failed\n");
378        return false;
379    }
380    return true;
381 }
382
383 static bool grow_del_list(struct del_ctx *del)
384 {
385    if (del->num_ids == MAX_DEL_LIST_LEN) {
386       return false;
387    }
388
389    if (del->num_ids == del->max_ids) {
390       del->max_ids = (del->max_ids * 3) / 2;
391       del->JobId = (JobId_t *)brealloc(del->JobId, sizeof(JobId_t) *
392          del->max_ids);
393       del->PurgedFiles = (char *)brealloc(del->PurgedFiles, del->max_ids);
394    }
395    return true;
396 }
397
398 struct accurate_check_ctx {
399    DBId_t ClientId;                   /* Id of client */
400    DBId_t FileSetId;                  /* Id of FileSet */
401 };
402
403 /* row: Job.Name, FileSet, Client.Name, FileSetId, ClientId, Type */
404 static int job_select_handler(void *ctx, int num_fields, char **row)
405 {
406    alist *lst = (alist *)ctx;
407    struct accurate_check_ctx *res;
408    ASSERT(num_fields == 6);
409
410    /* Quick fix for #5507, avoid locking res_head after db_lock() */
411
412 #ifdef bug5507
413    /* If this job doesn't exist anymore in the configuration, delete it */
414    if (GetResWithName(R_JOB, row[0]) == NULL) {
415       return 0;
416    }
417
418    /* If this fileset doesn't exist anymore in the configuration, delete it */
419    if (GetResWithName(R_FILESET, row[1]) == NULL) {
420       return 0;
421    }
422
423    /* If this client doesn't exist anymore in the configuration, delete it */
424    if (GetResWithName(R_CLIENT, row[2]) == NULL) {
425       return 0;
426    }
427 #endif
428
429    /* Don't compute accurate things for Verify jobs */
430    if (*row[5] == 'V') {
431       return 0;
432    }
433
434    res = (struct accurate_check_ctx*) malloc(sizeof(struct accurate_check_ctx));
435    res->FileSetId = str_to_int64(row[3]);
436    res->ClientId = str_to_int64(row[4]);
437    lst->append(res);
438
439 // Dmsg2(150, "row=%d val=%d\n", del->num_ids-1, del->JobId[del->num_ids-1]);
440    return 0;
441 }
442
443 /*
444  * Pruning Jobs is a bit more complicated than purging Files
445  * because we delete Job records only if there is a more current
446  * backup of the FileSet. Otherwise, we keep the Job record.
447  * In other words, we never delete the only Job record that
448  * contains a current backup of a FileSet. This prevents the
449  * Volume from being recycled and destroying a current backup.
450  *
451  * For Verify Jobs, we do not delete the last InitCatalog.
452  *
453  * For Restore Jobs there are no restrictions.
454  */
455 int prune_jobs(UAContext *ua, CLIENT *client, POOL *pool, int JobType)
456 {
457    POOL_MEM query(PM_MESSAGE);
458    POOL_MEM sql_where(PM_MESSAGE);
459    POOL_MEM sql_from(PM_MESSAGE);
460    utime_t period;
461    char ed1[50];
462    alist *jobids_check=NULL;
463    struct accurate_check_ctx *elt;
464    db_list_ctx jobids, tempids;
465    JOB_DBR jr;
466    struct del_ctx del;
467    memset(&del, 0, sizeof(del));
468
469    if (pool && pool->JobRetention > 0) {
470       period = pool->JobRetention;
471
472    } else if (client) {
473       period = client->JobRetention;
474
475    } else {                     /* should specify at least pool or client */
476       return false;
477    }
478
479    db_lock(ua->db);
480    if (!prune_set_filter(ua, client, pool, period, &sql_from, &sql_where)) {
481       goto bail_out;
482    }
483
484    /* Drop any previous temporary tables still there */
485    drop_temp_tables(ua);
486
487    /* Create temp tables and indicies */
488    if (!create_temp_tables(ua)) {
489       goto bail_out;
490    }
491
492    edit_utime(period, ed1, sizeof(ed1));
493    Jmsg(ua->jcr, M_INFO, 0, _("Begin pruning Jobs older than %s.\n"), ed1);
494
495    del.max_ids = 100;
496    del.JobId = (JobId_t *)malloc(sizeof(JobId_t) * del.max_ids);
497    del.PurgedFiles = (char *)malloc(del.max_ids);
498
499    /*
500     * Select all files that are older than the JobRetention period
501     *  and add them into the "DeletionCandidates" table.
502     */
503    Mmsg(query,
504         "INSERT INTO DelCandidates "
505           "SELECT JobId,PurgedFiles,FileSetId,JobFiles,JobStatus "
506             "FROM Job %s "      /* JOIN Pool/Client */
507            "WHERE Type IN ('B', 'C', 'M', 'V',  'D', 'R', 'c', 'm', 'g') "
508              " %s ",            /* Pool/Client + JobTDate */
509         sql_from.c_str(), sql_where.c_str());
510
511    Dmsg1(100, "select sql=%s\n", query.c_str());
512    if (!db_sql_query(ua->db, query.c_str(), NULL, (void *)NULL)) {
513       if (ua->verbose) {
514          ua->error_msg("%s", db_strerror(ua->db));
515       }
516       goto bail_out;
517    }
518
519    /* Now, for the selection, we discard some of them in order to be always
520     * able to restore files. (ie, last full, last diff, last incrs)
521     * Note: The DISTINCT could be more useful if we don't get FileSetId
522     */
523    jobids_check = New(alist(10, owned_by_alist));
524    Mmsg(query,
525 "SELECT DISTINCT Job.Name, FileSet, Client.Name, Job.FileSetId, "
526                 "Job.ClientId, Job.Type "
527   "FROM DelCandidates "
528        "JOIN Job USING (JobId) "
529        "JOIN Client USING (ClientId) "
530        "JOIN FileSet ON (Job.FileSetId = FileSet.FileSetId) "
531  "WHERE Job.Type IN ('B') "               /* Look only Backup jobs */
532    "AND Job.JobStatus IN ('T', 'W') "     /* Look only useful jobs */
533       );
534
535    /* The job_select_handler will skip jobs or filesets that are no longer
536     * in the configuration file. Interesting ClientId/FileSetId will be
537     * added to jobids_check (currently disabled in 6.0.7b)
538     */
539    if (!db_sql_query(ua->db, query.c_str(), job_select_handler, jobids_check)) {
540       ua->error_msg("%s", db_strerror(ua->db));
541    }
542
543    /* For this selection, we exclude current jobs used for restore or
544     * accurate. This will prevent to prune the last full backup used for
545     * current backup & restore
546     */
547    memset(&jr, 0, sizeof(jr));
548    /* To find useful jobs, we do like an incremental */
549    jr.JobLevel = L_INCREMENTAL;
550    foreach_alist(elt, jobids_check) {
551       jr.ClientId = elt->ClientId;   /* should be always the same */
552       jr.FileSetId = elt->FileSetId;
553       db_get_accurate_jobids(ua->jcr, ua->db, &jr, &tempids);
554       jobids.add(tempids);
555    }
556
557    /* Discard latest Verify level=InitCatalog job
558     * TODO: can have multiple fileset
559     */
560    Mmsg(query,
561         "SELECT JobId, JobTDate "
562           "FROM Job %s "                         /* JOIN Client/Pool */
563          "WHERE Type='V'    AND Level='V' "
564               " %s "                             /* Pool, JobTDate, Client */
565          "ORDER BY JobTDate DESC LIMIT 1",
566         sql_from.c_str(), sql_where.c_str());
567
568    if (!db_sql_query(ua->db, query.c_str(), db_list_handler, &jobids)) {
569       ua->error_msg("%s", db_strerror(ua->db));
570    }
571
572    /* If we found jobs to exclude from the DelCandidates list, we should
573     * also remove BaseJobs that can be linked with them
574     */
575    if (jobids.count > 0) {
576       Dmsg1(60, "jobids to exclude before basejobs = %s\n", jobids.list);
577       /* We also need to exclude all basejobs used */
578       db_get_used_base_jobids(ua->jcr, ua->db, jobids.list, &jobids);
579
580       /* Removing useful jobs from the DelCandidates list */
581       Mmsg(query, "DELETE FROM DelCandidates "
582                    "WHERE JobId IN (%s) "        /* JobId used in accurate */
583                      "AND JobFiles!=0",          /* Discard when JobFiles=0 */
584            jobids.list);
585
586       if (!db_sql_query(ua->db, query.c_str(), NULL, NULL)) {
587          ua->error_msg("%s", db_strerror(ua->db));
588          goto bail_out;         /* Don't continue if the list isn't clean */
589       }
590       Dmsg1(60, "jobids to exclude = %s\n", jobids.list);
591    }
592
593    /* We use DISTINCT because we can have two times the same job */
594    Mmsg(query,
595         "SELECT DISTINCT DelCandidates.JobId,DelCandidates.PurgedFiles "
596           "FROM DelCandidates");
597    if (!db_sql_query(ua->db, query.c_str(), job_delete_handler, (void *)&del)) {
598       ua->error_msg("%s", db_strerror(ua->db));
599    }
600
601    purge_job_list_from_catalog(ua, del);
602
603    if (del.num_del > 0) {
604       ua->info_msg(_("Pruned %d %s for client %s from catalog.\n"), del.num_del,
605          del.num_del==1?_("Job"):_("Jobs"), client->name());
606     } else if (ua->verbose) {
607        ua->info_msg(_("No Jobs found to prune.\n"));
608     }
609
610 bail_out:
611    drop_temp_tables(ua);
612    db_unlock(ua->db);
613    if (del.JobId) {
614       free(del.JobId);
615    }
616    if (del.PurgedFiles) {
617       free(del.PurgedFiles);
618    }
619    if (jobids_check) {
620       delete jobids_check;
621    }
622    return 1;
623 }
624
625
626 /*
627  * Prune a expired Volumes
628  */
629 static bool prune_expired_volumes(UAContext *ua)
630 {
631    bool ok=false;
632    POOL_MEM query(PM_MESSAGE);
633    POOL_MEM filter(PM_MESSAGE);
634    alist *lst=NULL;
635    int nb=0, i=0;
636    char *val;
637    MEDIA_DBR mr;
638
639    db_lock(ua->db);
640    /* We can restrict to a specific pool */
641    if ((i = find_arg_with_value(ua, "pool")) >= 0) {
642       POOL_DBR pdbr;
643       memset(&pdbr, 0, sizeof(pdbr));
644       bstrncpy(pdbr.Name, ua->argv[i], sizeof(pdbr.Name));
645       if (!db_get_pool_record(ua->jcr, ua->db, &pdbr)) {
646          ua->error_msg("%s", db_strerror(ua->db));
647          goto bail_out;
648       }
649       Mmsg(query, " AND PoolId = %lld ", (int64_t) pdbr.PoolId);
650       pm_strcat(filter, query.c_str());
651    }
652
653    /* We can restrict by MediaType */
654    if (((i = find_arg_with_value(ua, "mediatype")) >= 0) &&
655        (strlen(ua->argv[i]) <= MAX_NAME_LENGTH))
656    {
657       char ed1[MAX_ESCAPE_NAME_LENGTH];
658       db_escape_string(ua->jcr, ua->db, ed1,
659          ua->argv[i], strlen(ua->argv[i]));
660       Mmsg(query, " AND MediaType = '%s' ", ed1);
661       pm_strcat(filter, query.c_str());
662    }
663
664    /* Use a limit */
665    if ((i = find_arg_with_value(ua, "limit")) >= 0) {
666       if (is_an_integer(ua->argv[i])) {
667          Mmsg(query, " LIMIT %s ", ua->argv[i]);
668          pm_strcat(filter, query.c_str());
669       } else {
670          ua->error_msg(_("Expecting limit argument as integer\n"));
671          goto bail_out;
672       }
673    }
674
675    lst = New(alist(5, owned_by_alist));
676
677    Mmsg(query, expired_volumes[db_get_type_index(ua->db)], filter.c_str());
678    db_sql_query(ua->db, query.c_str(), db_string_list_handler, &lst);
679
680    foreach_alist(val, lst) {
681       nb++;
682       memset(&mr, 0, sizeof(mr));
683       bstrncpy(mr.VolumeName, val, sizeof(mr.VolumeName));
684       db_get_media_record(ua->jcr, ua->db, &mr);
685       Mmsg(query, _("Volume \"%s\""), val);
686       if (confirm_retention(ua, &mr.VolRetention, query.c_str())) {
687          prune_volume(ua, &mr);
688       }
689    }
690    ua->send_msg(_("%d expired volume%s found\n"),
691                 nb, nb>1?"s":"");
692    ok = true;
693
694 bail_out:
695    db_unlock(ua->db);
696    if (lst) {
697       delete lst;
698    }
699    return ok;
700 }
701
702 /*
703  * Prune a given Volume
704  */
705 bool prune_volume(UAContext *ua, MEDIA_DBR *mr)
706 {
707    POOL_MEM query(PM_MESSAGE);
708    struct del_ctx del;
709    bool ok = false;
710    int count;
711
712    if (mr->Enabled == 2) {
713       return false;                   /* Cannot prune archived volumes */
714    }
715
716    memset(&del, 0, sizeof(del));
717    del.max_ids = 10000;
718    del.JobId = (JobId_t *)malloc(sizeof(JobId_t) * del.max_ids);
719
720    db_lock(ua->db);
721
722    /* Prune only Volumes with status "Full", or "Used" */
723    if (strcmp(mr->VolStatus, "Full")   == 0 ||
724        strcmp(mr->VolStatus, "Used")   == 0) {
725       Dmsg2(100, "get prune list MediaId=%lu Volume %s\n", mr->MediaId, mr->VolumeName);
726       count = get_prune_list_for_volume(ua, mr, &del);
727       Dmsg1(100, "Num pruned = %d\n", count);
728       if (count != 0) {
729          ua->info_msg(_("Found %d Job(s) associated with the Volume \"%s\" that will be pruned\n"),
730                       count, mr->VolumeName);
731          purge_job_list_from_catalog(ua, del);
732
733       } else {
734          ua->info_msg(_("Found no Job associated with the Volume \"%s\" to prune\n"),
735                       mr->VolumeName);
736       }
737       ok = is_volume_purged(ua, mr);
738    }
739
740    db_unlock(ua->db);
741    if (del.JobId) {
742       free(del.JobId);
743    }
744    return ok;
745 }
746
747 /*
748  * Get prune list for a volume
749  */
750 int get_prune_list_for_volume(UAContext *ua, MEDIA_DBR *mr, del_ctx *del)
751 {
752    POOL_MEM query(PM_MESSAGE);
753    int count = 0;
754    utime_t now, period;
755    char ed1[50], ed2[50];
756
757    if (mr->Enabled == 2) {
758       return 0;                    /* cannot prune Archived volumes */
759    }
760
761    /*
762     * Now add to the  list of JobIds for Jobs written to this Volume
763     */
764    edit_int64(mr->MediaId, ed1);
765    period = mr->VolRetention;
766    now = (utime_t)time(NULL);
767    edit_int64(now-period, ed2);
768    Mmsg(query, sel_JobMedia, ed1, ed2);
769    Dmsg3(250, "Now=%d period=%d now-period=%s\n", (int)now, (int)period,
770       ed2);
771
772    Dmsg1(100, "Query=%s\n", query.c_str());
773    if (!db_sql_query(ua->db, query.c_str(), file_delete_handler, (void *)del)) {
774       if (ua->verbose) {
775          ua->error_msg("%s", db_strerror(ua->db));
776       }
777       Dmsg0(100, "Count failed\n");
778       goto bail_out;
779    }
780    count = exclude_running_jobs_from_list(del);
781
782 bail_out:
783    return count;
784 }
785
786 /*
787  * We have a list of jobs to prune or purge. If any of them is
788  *   currently running, we set its JobId to zero which effectively
789  *   excludes it.
790  *
791  * Returns the number of jobs that can be prunned or purged.
792  *
793  */
794 int exclude_running_jobs_from_list(del_ctx *prune_list)
795 {
796    int count = 0;
797    JCR *jcr;
798    bool skip;
799    int i;
800
801    /* Do not prune any job currently running */
802    for (i=0; i < prune_list->num_ids; i++) {
803       skip = false;
804       foreach_jcr(jcr) {
805          if (jcr->JobId == prune_list->JobId[i]) {
806             Dmsg2(100, "skip running job JobId[%d]=%d\n", i, (int)prune_list->JobId[i]);
807             prune_list->JobId[i] = 0;
808             skip = true;
809             break;
810          }
811       }
812       endeach_jcr(jcr);
813       if (skip) {
814          continue;  /* don't increment count */
815       }
816       Dmsg2(100, "accept JobId[%d]=%d\n", i, (int)prune_list->JobId[i]);
817       count++;
818    }
819    return count;
820 }