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