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