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 -- Automatic Pruning
21  *      Applies retention periods
22  *
23  *     Kern Sibbald, May MMII
24  */
25 
26 #include "bacula.h"
27 #include "dird.h"
28 #include "ua.h"
29 
30 /* Forward referenced functions */
31 
32 
33 /*
34  * Auto Prune Jobs and Files. This is called at the end of every
35  *   Job.  We do not prune volumes here.
36  */
do_autoprune(JCR * jcr)37 void do_autoprune(JCR *jcr)
38 {
39    UAContext *ua;
40    CLIENT *client;
41    POOL *pool;
42    bool pruned;
43 
44    if (!director->AutoPrune) {
45       Dmsg0(100, "AutoPrune globally switched off\n");
46       return;
47    }
48 
49    if (!jcr->client) {                /* temp -- remove me */
50       return;
51    }
52 
53    ua = new_ua_context(jcr);
54    client = jcr->client;
55    pool = jcr->pool;
56 
57    if (jcr->job->PruneJobs || jcr->client->AutoPrune) {
58       prune_jobs(ua, client, pool, jcr->getJobType());
59       pruned = true;
60    } else {
61       pruned = false;
62    }
63 
64    if (jcr->job->PruneFiles || jcr->client->AutoPrune) {
65       prune_files(ua, client, pool);
66       pruned = true;
67    }
68    if (pruned) {
69       Jmsg(jcr, M_INFO, 0, _("End auto prune.\n\n"));
70    }
71    free_ua_context(ua);
72    return;
73 }
74 
75 /*
76  * Prune at least one Volume in current Pool. This is called from
77  *   catreq.c => next_vol.c when the Storage daemon is asking for another
78  *   volume and no appendable volumes are available.
79  *
80  */
prune_volumes(JCR * jcr,bool InChanger,MEDIA_DBR * mr,STORE * store)81 void prune_volumes(JCR *jcr, bool InChanger, MEDIA_DBR *mr,
82         STORE *store)
83 {
84    int count;
85    int i;
86    dbid_list ids;
87    struct del_ctx prune_list;
88    POOL_MEM query(PM_MESSAGE), changer(PM_MESSAGE);
89    UAContext *ua;
90    char ed1[50], ed2[100], ed3[50];
91 
92    POOL_DBR spr;
93 
94    if (!director->AutoPrune) {
95       Dmsg0(100, "AutoPrune globally switched off\n");
96       return;
97    }
98 
99    Dmsg1(100, "Prune volumes PoolId=%d\n", jcr->jr.PoolId);
100    if (!jcr->job->PruneVolumes && !jcr->pool->AutoPrune) {
101       Dmsg0(100, "AutoPrune not set in Pool or Job.\n");
102       return;
103    }
104 
105    bmemset(&prune_list, 0, sizeof(prune_list));
106    prune_list.max_ids = 10000;
107    prune_list.JobId = (JobId_t *)malloc(sizeof(JobId_t) * prune_list.max_ids);
108 
109    ua = new_ua_context(jcr);
110    db_lock(jcr->db);
111 
112    /* Edit PoolId */
113    edit_int64(mr->PoolId, ed1);
114    /*
115     * Get Pool record for Scratch Pool
116     */
117    bmemset(&spr, 0, sizeof(spr));
118    bstrncpy(spr.Name, "Scratch", sizeof(spr.Name));
119    if (db_get_pool_record(jcr, jcr->db, &spr)) {
120       edit_int64(spr.PoolId, ed2);
121       bstrncat(ed2, ",", sizeof(ed2));
122    } else {
123       ed2[0] = 0;
124    }
125 
126    if (mr->ScratchPoolId) {
127       edit_int64(mr->ScratchPoolId, ed3);
128       bstrncat(ed2, ed3, sizeof(ed2));
129       bstrncat(ed2, ",", sizeof(ed2));
130    }
131 
132    Dmsg1(100, "Scratch pool(s)=%s\n", ed2);
133    /*
134     * ed2 ends up with scratch poolid and current poolid or
135     *   just current poolid if there is no scratch pool
136     */
137    bstrncat(ed2, ed1, sizeof(ed2));
138 
139    /*
140     * Get the List of all media ids in the current Pool or whose
141     *  RecyclePoolId is the current pool or the scratch pool
142     */
143    const char *select = "SELECT DISTINCT MediaId,LastWritten FROM Media WHERE "
144         "(PoolId=%s OR RecyclePoolId IN (%s)) AND MediaType='%s' %s"
145         "ORDER BY LastWritten ASC,MediaId";
146 
147    set_storageid_in_mr(store, mr);
148    if (InChanger) {
149       Mmsg(changer, "AND InChanger=1 AND StorageId IN (%s) ", mr->sid_group);
150    }
151 
152    Mmsg(query, select, ed1, ed2, mr->MediaType, changer.c_str());
153 
154    Dmsg1(100, "query=%s\n", query.c_str());
155    if (!db_get_query_dbids(ua->jcr, ua->db, query, ids)) {
156       Jmsg(jcr, M_ERROR, 0, "%s", db_strerror(jcr->db));
157       goto bail_out;
158    }
159 
160    Dmsg1(100, "Volume prune num_ids=%d\n", ids.num_ids);
161 
162    /* Visit each Volume and Prune it until we find one that is purged */
163    for (i=0; i<ids.num_ids; i++) {
164       MEDIA_DBR lmr;
165       lmr.MediaId = ids.DBId[i];
166       Dmsg1(100, "Get record MediaId=%lu\n", lmr.MediaId);
167       if (!db_get_media_record(jcr, jcr->db, &lmr)) {
168          Jmsg(jcr, M_ERROR, 0, "%s", db_strerror(jcr->db));
169          continue;
170       }
171       Dmsg1(100, "Examine vol=%s\n", lmr.VolumeName);
172       /* Don't prune archived volumes */
173       if (lmr.Enabled == 2) {
174          Dmsg1(100, "Vol=%s disabled\n", lmr.VolumeName);
175          continue;
176       }
177       /* Prune only Volumes with status "Full", or "Used" */
178       if (strcmp(lmr.VolStatus, "Full")   == 0 ||
179           strcmp(lmr.VolStatus, "Used")   == 0) {
180          Dmsg2(100, "Add prune list MediaId=%lu Volume %s\n", lmr.MediaId, lmr.VolumeName);
181          count = get_prune_list_for_volume(ua, &lmr, &prune_list);
182          Dmsg1(100, "Num pruned = %d\n", count);
183          if (count != 0) {
184             purge_job_list_from_catalog(ua, prune_list);
185             prune_list.num_ids = 0;             /* reset count */
186          }
187          if (!is_volume_purged(ua, &lmr)) {
188             Dmsg1(100, "Vol=%s not pruned\n", lmr.VolumeName);
189             continue;
190          }
191          Dmsg1(100, "Vol=%s is purged\n", lmr.VolumeName);
192 
193          /*
194           * Since we are also pruning the Scratch pool, continue
195           *   until and check if this volume is available (InChanger + StorageId)
196           * If not, just skip this volume and try the next one
197           */
198          if (InChanger) {
199             /* ***FIXME*** should be any StorageId in sid_group */
200             if (!lmr.InChanger || (lmr.StorageId != mr->StorageId)) {
201                Dmsg1(100, "Vol=%s not inchanger\n", lmr.VolumeName);
202                continue;                  /* skip this volume, ie not loadable */
203             }
204          }
205 
206          if (!lmr.Recycle) {
207             Dmsg1(100, "Vol=%s not recyclable\n", lmr.VolumeName);
208             continue;
209          }
210 
211          if (has_volume_expired(jcr, &lmr)) {
212             Dmsg1(100, "Vol=%s has expired\n", lmr.VolumeName);
213             continue;                     /* Volume not usable */
214          }
215 
216          /*
217           * If purged and not moved to another Pool,
218           *   then we stop pruning and take this volume.
219           */
220          if (lmr.PoolId == mr->PoolId) {
221             Dmsg2(100, "Got Vol=%s MediaId=%lu purged.\n", lmr.VolumeName, lmr.MediaId);
222             mr->copy(&lmr);
223             set_storageid_in_mr(store, mr);
224             break;                        /* got a volume */
225          }
226       }
227    }
228 
229 bail_out:
230    Dmsg0(100, "Leave prune volumes\n");
231    db_unlock(jcr->db);
232    free_ua_context(ua);
233    if (prune_list.JobId) {
234       free(prune_list.JobId);
235    }
236    return;
237 }
238