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