1 /*
2    BAREOS® - Backup Archiving REcovery Open Sourced
3 
4    Copyright (C) 2001-2012 Free Software Foundation Europe e.V.
5    Copyright (C) 2011-2016 Planets Communications B.V.
6    Copyright (C) 2013-2018 Bareos GmbH & Co. KG
7 
8    This program is Free Software; you can redistribute it and/or
9    modify it under the terms of version three of the GNU Affero General Public
10    License as published by the Free Software Foundation and included
11    in the file LICENSE.
12 
13    This program is distributed in the hope that it will be useful, but
14    WITHOUT ANY WARRANTY; without even the implied warranty of
15    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
16    Affero General Public License for more details.
17 
18    You should have received a copy of the GNU Affero General Public License
19    along with this program; if not, write to the Free Software
20    Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
21    02110-1301, USA.
22 */
23 /*
24  * Kern Sibbald, March MMI
25  */
26 /**
27  * @file
28  * handles finding the next volume for append.
29  *
30  * Split out of catreq.c August MMIII catalog request from the Storage daemon.
31  */
32 
33 #include "include/bareos.h"
34 #include "dird.h"
35 #include "dird/autoprune.h"
36 #include "dird/autorecycle.h"
37 #include "dird/next_vol.h"
38 #include "dird/newvol.h"
39 #include "dird/ua_db.h"
40 #include "dird/ua_server.h"
41 #include "dird/ua_prune.h"
42 #include "dird/ua_purge.h"
43 #include "lib/edit.h"
44 
45 namespace directordaemon {
46 
47 static int const debuglevel = 50;   /* debug level */
48 
49 /**
50  * Set storage id if possible
51  */
SetStorageidInMr(StorageResource * store,MediaDbRecord * mr)52 void SetStorageidInMr(StorageResource *store, MediaDbRecord *mr)
53 {
54    if (store != NULL) {
55       mr->StorageId = store->StorageId;
56    }
57 }
58 
59 /**
60  *  Items needed:
61  *
62  *  mr.PoolId must be set
63  *  mr.ScratchPoolId could be set (used if create==true)
64  *  jcr->write_storage
65  *  jcr->db
66  *  jcr->pool
67  *  MediaDbRecord mr with PoolId set
68  *  unwanted_volumes -- list of volumes we don't want
69  *  create -- whether or not to create a new volume
70  *  prune -- whether or not to prune volumes
71  */
FindNextVolumeForAppend(JobControlRecord * jcr,MediaDbRecord * mr,int index,const char * unwanted_volumes,bool create,bool prune)72 int FindNextVolumeForAppend(JobControlRecord *jcr, MediaDbRecord *mr, int index,
73                                 const char *unwanted_volumes, bool create, bool prune)
74 {
75    int retry = 0;
76    bool ok;
77    bool InChanger;
78    StorageResource *store = jcr->res.write_storage;
79 
80    bstrncpy(mr->MediaType, store->media_type, sizeof(mr->MediaType));
81    Dmsg3(debuglevel, "find_next_vol_for_append: JobId=%u PoolId=%d, MediaType=%s\n",
82          (uint32_t)jcr->JobId, (int)mr->PoolId, mr->MediaType);
83 
84    /*
85     * If we are using an Autochanger, restrict Volume search to the Autochanger on the first pass
86     */
87    InChanger = store->autochanger;
88 
89    /*
90     * Find the Next Volume for Append
91     */
92    DbLock(jcr->db);
93    while (1) {
94       /*
95        *  1. Look for volume with "Append" status.
96        */
97       SetStorageidInMr(store, mr);
98 
99       bstrncpy(mr->VolStatus, "Append", sizeof(mr->VolStatus));
100       ok = jcr->db->FindNextVolume(jcr, index, InChanger, mr, unwanted_volumes);
101       if (!ok) {
102          /*
103           * No volume found, apply algorithm
104           */
105          Dmsg4(debuglevel, "after find_next_vol ok=%d index=%d InChanger=%d Vstat=%s\n",
106                ok, index, InChanger, mr->VolStatus);
107 
108          /*
109           * 2. Try finding a recycled volume
110           */
111          ok = FindRecycledVolume(jcr, InChanger, mr, store, unwanted_volumes);
112          SetStorageidInMr(store, mr);
113          Dmsg2(debuglevel, "FindRecycledVolume ok=%d FW=%d\n", ok, mr->FirstWritten);
114          if (!ok) {
115             /*
116              * 3. Try recycling any purged volume
117              */
118             ok = RecycleOldestPurgedVolume(jcr, InChanger, mr, store, unwanted_volumes);
119             SetStorageidInMr(store, mr);
120             if (!ok) {
121                /*
122                 * 4. Try pruning Volumes
123                 */
124                if (prune) {
125                   Dmsg0(debuglevel, "Call PruneVolumes\n");
126                   PruneVolumes(jcr, InChanger, mr, store);
127                }
128                ok = RecycleOldestPurgedVolume(jcr, InChanger, mr, store, unwanted_volumes);
129                SetStorageidInMr(store, mr);  /* put StorageId in new record */
130                if (!ok && create) {
131                   Dmsg4(debuglevel, "after prune volumes_vol ok=%d index=%d InChanger=%d Vstat=%s\n",
132                         ok, index, InChanger, mr->VolStatus);
133                   /*
134                    * 5. Try pulling a volume from the Scratch pool
135                    */
136                   ok = GetScratchVolume(jcr, InChanger, mr, store);
137                   SetStorageidInMr(store, mr);  /* put StorageId in new record */
138                   Dmsg4(debuglevel, "after get scratch volume ok=%d index=%d InChanger=%d Vstat=%s\n",
139                         ok, index, InChanger, mr->VolStatus);
140                }
141                /*
142                 * If we are using an Autochanger and have not found
143                 * a volume, retry looking for any volume.
144                 */
145                if (!ok && InChanger) {
146                   InChanger = false;
147                   continue;           /* retry again accepting any volume */
148                }
149             }
150          }
151 
152          if (!ok && create) {
153             /*
154              * 6. Try "creating" a new Volume
155              */
156             ok = newVolume(jcr, mr, store);
157          }
158 
159          /*
160           *  Look at more drastic ways to find an Appendable Volume
161           */
162          if (!ok && (jcr->res.pool->purge_oldest_volume ||
163                      jcr->res.pool->recycle_oldest_volume)) {
164             Dmsg2(debuglevel, "No next volume found. PurgeOldest=%d\n RecyleOldest=%d",
165                   jcr->res.pool->purge_oldest_volume, jcr->res.pool->recycle_oldest_volume);
166 
167             /*
168              * Find oldest volume to recycle
169              */
170             SetStorageidInMr(store, mr);
171             ok = jcr->db->FindNextVolume(jcr, -1, InChanger, mr, unwanted_volumes);
172             SetStorageidInMr(store, mr);
173             Dmsg1(debuglevel, "Find oldest=%d Volume\n", ok);
174             if (ok && prune) {
175                UaContext *ua;
176                Dmsg0(debuglevel, "Try purge Volume.\n");
177                /*
178                 * 7.  Try to purging oldest volume only if not UA calling us.
179                 */
180                ua = new_ua_context(jcr);
181                if (jcr->res.pool->purge_oldest_volume && create) {
182                   Jmsg(jcr, M_INFO, 0, _("Purging oldest volume \"%s\"\n"), mr->VolumeName);
183                   ok = PurgeJobsFromVolume(ua, mr);
184                } else if (jcr->res.pool->recycle_oldest_volume) {
185                   /*
186                    * 8. Try recycling the oldest volume
187                    */
188                   Jmsg(jcr, M_INFO, 0, _("Pruning oldest volume \"%s\"\n"), mr->VolumeName);
189                   ok = PruneVolume(ua, mr);
190                }
191                FreeUaContext(ua);
192 
193                if (ok) {
194                   ok = RecycleVolume(jcr, mr);
195                   Dmsg1(debuglevel, "Recycle after purge oldest=%d\n", ok);
196                }
197             }
198          }
199       }
200 
201       Dmsg2(debuglevel, "VolJobs=%d FirstWritten=%d\n", mr->VolJobs, mr->FirstWritten);
202       if (ok) {
203          /*
204           * If we can use the volume, check if it is expired
205           */
206          if (bstrcmp(mr->VolStatus, "Append") && HasVolumeExpired(jcr, mr)) {
207             if (retry++ < 200) {            /* sanity check */
208                continue;                    /* try again from the top */
209             } else {
210                Jmsg(jcr, M_ERROR, 0, _("We seem to be looping trying to find the next volume. I give up.\n"));
211             }
212          }
213       }
214 
215       break;
216    }
217 
218    DbUnlock(jcr->db);
219    Dmsg1(debuglevel, "return ok=%d find_next_vol\n", ok);
220 
221    return ok;
222 }
223 
224 /**
225  * Check if any time limits or use limits have expired if so,
226  * set the VolStatus appropriately.
227  */
HasVolumeExpired(JobControlRecord * jcr,MediaDbRecord * mr)228 bool HasVolumeExpired(JobControlRecord *jcr, MediaDbRecord *mr)
229 {
230    bool expired = false;
231    char ed1[50];
232 
233    /*
234     * Check limits and expirations if "Append" and it has been used i.e. mr->VolJobs > 0
235     */
236    if (bstrcmp(mr->VolStatus, "Append") && mr->VolJobs > 0) {
237       /*
238        * First handle Max Volume Bytes
239        */
240       if ((mr->MaxVolBytes > 0 && mr->VolBytes >= mr->MaxVolBytes)) {
241          Jmsg(jcr, M_INFO, 0, _("Max Volume bytes=%s exceeded. Marking Volume \"%s\" as Full.\n"),
242               edit_uint64_with_commas(mr->MaxVolBytes, ed1), mr->VolumeName);
243          bstrncpy(mr->VolStatus, "Full", sizeof(mr->VolStatus));
244          expired = true;
245       } else if (mr->VolBytes > 0 && jcr->res.pool->use_volume_once) {
246          /*
247           * Volume should only be used once
248           */
249          Jmsg(jcr, M_INFO, 0, _("Volume used once. Marking Volume \"%s\" as Used.\n"),
250               mr->VolumeName);
251          bstrncpy(mr->VolStatus, "Used", sizeof(mr->VolStatus));
252          expired = true;
253       } else if (mr->MaxVolJobs > 0 && mr->MaxVolJobs <= mr->VolJobs) {
254          /*
255           * Max Jobs written to volume
256           */
257          Jmsg(jcr, M_INFO, 0, _("Max Volume jobs=%s exceeded. Marking Volume \"%s\" as Used.\n"),
258               edit_uint64_with_commas(mr->MaxVolJobs, ed1), mr->VolumeName);
259          Dmsg3(debuglevel, "MaxVolJobs=%d JobId=%d Vol=%s\n", mr->MaxVolJobs,
260                (uint32_t)jcr->JobId, mr->VolumeName);
261          bstrncpy(mr->VolStatus, "Used", sizeof(mr->VolStatus));
262          expired = true;
263       } else if (mr->MaxVolFiles > 0 && mr->MaxVolFiles <= mr->VolFiles) {
264          /*
265           * Max Files written to volume
266           */
267          Jmsg(jcr, M_INFO, 0, _("Max Volume files=%s exceeded. Marking Volume \"%s\" as Used.\n"),
268               edit_uint64_with_commas(mr->MaxVolFiles, ed1), mr->VolumeName);
269          bstrncpy(mr->VolStatus, "Used", sizeof(mr->VolStatus));
270          expired = true;
271       } else if (mr->VolUseDuration > 0) {
272          /*
273           * Use duration expiration
274           */
275          utime_t now = time(NULL);
276          if (mr->VolUseDuration <= (now - mr->FirstWritten)) {
277             Jmsg(jcr, M_INFO, 0, _("Max configured use duration=%s sec. exceeded. Marking Volume \"%s\" as Used.\n"),
278                  edit_uint64_with_commas(mr->VolUseDuration, ed1), mr->VolumeName);
279             bstrncpy(mr->VolStatus, "Used", sizeof(mr->VolStatus));
280             expired = true;
281          }
282       }
283    }
284 
285    if (expired) {
286       /*
287        * Need to update media
288        */
289       Dmsg1(debuglevel, "Vol=%s has expired update media record\n", mr->VolumeName);
290       SetStorageidInMr(NULL, mr);
291       if (!jcr->db->UpdateMediaRecord(jcr, mr)) {
292          Jmsg(jcr, M_ERROR, 0, _("Catalog error updating volume \"%s\". ERR=%s"),
293               mr->VolumeName, jcr->db->strerror());
294       }
295    }
296    Dmsg2(debuglevel, "Vol=%s expired=%d\n", mr->VolumeName, expired);
297 
298    return expired;
299 }
300 
301 /**
302  * Try hard to recycle the current volume
303  *
304  * Returns: on failure - reason = NULL
305  *          on success - reason - pointer to reason
306  */
CheckIfVolumeValidOrRecyclable(JobControlRecord * jcr,MediaDbRecord * mr,const char ** reason)307 void CheckIfVolumeValidOrRecyclable(JobControlRecord *jcr, MediaDbRecord *mr, const char **reason)
308 {
309    int ok;
310 
311    *reason = NULL;
312 
313    /*
314     * Check if a duration or limit has expired
315     */
316    if (bstrcmp(mr->VolStatus, "Append") && HasVolumeExpired(jcr, mr)) {
317       *reason = _("volume has expired");
318       /*
319        * Keep going because we may be able to recycle volume
320        */
321    }
322 
323    /*
324     * Now see if we can use the volume as is
325     */
326    if (bstrcmp(mr->VolStatus, "Append") ||
327        bstrcmp(mr->VolStatus, "Recycle")) {
328       *reason = NULL;
329       return;
330    }
331 
332    /*
333     * Check if the Volume is already marked for recycling
334     */
335    if (bstrcmp(mr->VolStatus, "Purged")) {
336       if (RecycleVolume(jcr, mr)) {
337          Jmsg(jcr, M_INFO, 0, _("Recycled current volume \"%s\"\n"), mr->VolumeName);
338          *reason = NULL;
339          return;
340       } else {
341          /*
342           * In principle this shouldn't happen
343           */
344          *reason = _("and recycling of current volume failed");
345          return;
346       }
347    }
348 
349    /*
350     * At this point, the volume is not valid for writing
351     */
352    *reason = _("but should be Append, Purged or Recycle");
353 
354    /*
355     * What we're trying to do here is see if the current volume is
356     * "recyclable" - ie. if we prune all expired jobs off it, is
357     * it now possible to reuse it for the job that it is currently
358     * needed for?
359     */
360    if (!mr->Recycle) {
361       *reason = _("volume has recycling disabled");
362       return;
363    }
364 
365    /*
366     * Check retention period from last written, but recycle to within a minute to try to catch close calls ...
367     */
368    if ((mr->LastWritten + mr->VolRetention - 60) < (utime_t)time(NULL) &&
369        jcr->res.pool->recycle_current_volume &&
370        (bstrcmp(mr->VolStatus, "Full") || bstrcmp(mr->VolStatus, "Used"))) {
371       /*
372        * Attempt prune of current volume to see if we can recycle it for use.
373        */
374       UaContext *ua;
375 
376       ua = new_ua_context(jcr);
377       ok = PruneVolume(ua, mr);
378       FreeUaContext(ua);
379 
380       if (ok) {
381          /*
382           * If fully purged, recycle current volume
383           */
384          if (RecycleVolume(jcr, mr)) {
385             Jmsg(jcr, M_INFO, 0, _("Recycled current volume \"%s\"\n"), mr->VolumeName);
386             *reason = NULL;
387          } else {
388             *reason = _("but should be Append, Purged or Recycle (recycling of the "
389                         "current volume failed)");
390          }
391       } else {
392          *reason = _("but should be Append, Purged or Recycle (cannot automatically "
393                      "recycle current volume, as it still contains unpruned data "
394                      "or the Volume Retention time has not expired.)");
395       }
396    }
397 }
398 
399 static pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
400 
GetScratchVolume(JobControlRecord * jcr,bool InChanger,MediaDbRecord * mr,StorageResource * store)401 bool GetScratchVolume(JobControlRecord *jcr, bool InChanger, MediaDbRecord *mr, StorageResource *store)
402 {
403    MediaDbRecord smr;                        /* for searching scratch pool */
404    PoolDbRecord spr, pr;
405    bool ok = false;
406    bool found = false;
407 
408    /*
409     * Only one thread at a time can pull from the scratch pool
410     */
411    P(mutex);
412 
413    /*
414     * Get Pool record for Scratch Pool
415     * choose between ScratchPoolId and Scratch
416     * GetPoolRecord will first try ScratchPoolId,
417     * and then try the pool named Scratch
418     */
419    memset(&smr, 0, sizeof(smr));
420    memset(&spr, 0, sizeof(spr));
421 
422    bstrncpy(spr.Name, "Scratch", sizeof(spr.Name));
423    spr.PoolId = mr->ScratchPoolId;
424    if (jcr->db->GetPoolRecord(jcr, &spr)) {
425       smr.PoolId = spr.PoolId;
426       if (InChanger) {
427          smr.StorageId = mr->StorageId;  /* want only Scratch Volumes in changer */
428       }
429 
430       bstrncpy(smr.VolStatus, "Append", sizeof(smr.VolStatus));  /* want only appendable volumes */
431       bstrncpy(smr.MediaType, mr->MediaType, sizeof(smr.MediaType));
432 
433       /*
434        * If we do not find a valid Scratch volume, try recycling any existing purged volumes,
435        * then try to take the oldest volume.
436        */
437       SetStorageidInMr(store, &smr);  /* put StorageId in new record */
438       if (jcr->db->FindNextVolume(jcr, 1, InChanger, &smr, NULL)) {
439          found = true;
440       } else if (FindRecycledVolume(jcr, InChanger, &smr, store, NULL)) {
441          found = true;
442       } else if (RecycleOldestPurgedVolume(jcr, InChanger, &smr, store, NULL)) {
443          found = true;
444       }
445 
446       if (found) {
447          PoolMem query(PM_MESSAGE);
448 
449          /*
450           * Get pool record where the Scratch Volume will go to ensure that we can add a Volume.
451           */
452          memset(&pr, 0, sizeof(pr));
453          bstrncpy(pr.Name, jcr->res.pool->name(), sizeof(pr.Name));
454 
455          if (!jcr->db->GetPoolRecord(jcr, &pr)) {
456             Jmsg(jcr, M_WARNING, 0, _("Unable to get Pool record: ERR=%s"), jcr->db->strerror());
457             goto bail_out;
458          }
459 
460          /*
461           * Make sure there is room for another volume
462           */
463          if (pr.MaxVols > 0 && pr.NumVols >= pr.MaxVols) {
464             Jmsg(jcr, M_WARNING, 0, _("Unable add Scratch Volume, Pool \"%s\" full MaxVols=%d\n"),
465                  jcr->res.pool->name(), pr.MaxVols);
466             goto bail_out;
467          }
468 
469          memcpy(mr, &smr, sizeof(MediaDbRecord));
470          SetStorageidInMr(store, mr);
471 
472          /*
473           * Set default parameters from current pool
474           */
475          SetPoolDbrDefaultsInMediaDbr(mr, &pr);
476 
477          /*
478           * SetPoolDbrDefaultsInMediaDbr set VolStatus to Append, we could have Recycled media,
479           * also, we retain the old RecyclePoolId.
480           */
481          bstrncpy(mr->VolStatus, smr.VolStatus, sizeof(smr.VolStatus));
482          mr->RecyclePoolId = smr.RecyclePoolId;
483 
484          if (!jcr->db->UpdateMediaRecord(jcr, mr)) {
485             Jmsg(jcr, M_WARNING, 0, _("Failed to move Scratch Volume. ERR=%s\n"), jcr->db->strerror());
486             goto bail_out;
487          }
488 
489          Jmsg(jcr, M_INFO, 0, _("Using Volume \"%s\" from 'Scratch' pool.\n"), mr->VolumeName);
490 
491          ok = true;
492       }
493    }
494 
495 bail_out:
496    V(mutex);
497    return ok;
498 }
499 } /* namespace directordaemon */
500