1 /*
2    BAREOS® - Backup Archiving REcovery Open Sourced
3 
4    Copyright (C) 2016-2019 Bareos GmbH & Co. KG
5 
6    This program is Free Software; you can redistribute it and/or
7    modify it under the terms of version three of the GNU Affero General Public
8    License as published by the Free Software Foundation and included
9    in the file LICENSE.
10 
11    This program is distributed in the hope that it will be useful, but
12    WITHOUT ANY WARRANTY; without even the implied warranty of
13    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14    Affero General Public License for more details.
15 
16    You should have received a copy of the GNU Affero General Public License
17    along with this program; if not, write to the Free Software
18    Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
19    02110-1301, USA.
20 */
21 /*
22  * Philipp Storz, May 2016
23  */
24 /** @file
25  * responsible for doing consolidation jobs
26  *
27  * Basic tasks done here:
28  *   run a virtual full job for all jobs that are configured to be always incremental
29  * based on admin.c
30  *
31  */
32 
33 #include "include/bareos.h"
34 #include "dird.h"
35 #include "dird/consolidate.h"
36 #include "dird/job.h"
37 #include "dird/storage.h"
38 #include "dird/ua_input.h"
39 #include "dird/ua_server.h"
40 #include "dird/ua_run.h"
41 #include "dird/dird_globals.h"
42 #include "lib/edit.h"
43 
44 namespace directordaemon {
45 
46 static const int debuglevel = 100;
47 
DoConsolidateInit(JobControlRecord * jcr)48 bool DoConsolidateInit(JobControlRecord *jcr)
49 {
50    FreeRstorage(jcr);
51    if (!AllowDuplicateJob(jcr)) {
52       return false;
53    }
54    return true;
55 }
56 
57 /**
58  * Start a Virtual(Full) Job that creates a new virtual backup
59  * containing the jobids given in jcr->vf_jobids
60  */
StartNewConsolidationJob(JobControlRecord * jcr,char * jobname)61 static inline void StartNewConsolidationJob(JobControlRecord *jcr, char *jobname)
62 {
63    JobId_t jobid;
64    UaContext *ua;
65    PoolMem cmd(PM_MESSAGE);
66 
67    ua = new_ua_context(jcr);
68    ua->batch = true;
69    Mmsg(ua->cmd, "run job=\"%s\" jobid=%s level=VirtualFull %s", jobname, jcr->vf_jobids, jcr->accurate ? "accurate=yes" : "accurate=no");
70 
71    Dmsg1(debuglevel, "=============== consolidate cmd=%s\n", ua->cmd);
72    ParseUaArgs(ua);                 /* parse command */
73 
74    jobid = DoRunCmd(ua, ua->cmd);
75    if (jobid == 0) {
76       Jmsg(jcr, M_ERROR, 0, _("Could not start %s job.\n"), jcr->get_OperationName());
77    } else {
78       Jmsg(jcr, M_INFO, 0, _("%s JobId %d started.\n"), jcr->get_OperationName(), (int)jobid);
79    }
80 
81    FreeUaContext(ua);
82 }
83 
84 /**
85  * The actual consolidation worker
86  *
87  * Returns: false on failure
88  *          true  on success
89  */
DoConsolidate(JobControlRecord * jcr)90 bool DoConsolidate(JobControlRecord *jcr)
91 {
92    char *p;
93    JobResource *job;
94    JobResource *tmpjob;
95    bool retval = true;
96    char *jobids = NULL;
97    time_t now = time(NULL);
98    int32_t fullconsolidations_started = 0;
99    int32_t max_full_consolidations = 0;
100 
101    tmpjob = jcr->res.job; /* Memorize job */
102 
103    /*
104     * Get Value for MaxFullConsolidations from Consolidation job
105     */
106    max_full_consolidations = jcr->res.job->MaxFullConsolidations;
107 
108    jcr->jr.JobId = jcr->JobId;
109    jcr->fname = (char *)GetPoolMemory(PM_FNAME);
110 
111    /*
112     * Print Job Start message
113     */
114    Jmsg(jcr, M_INFO, 0, _("Start Consolidate JobId %d, Job=%s\n"), jcr->JobId, jcr->Job);
115 
116    jcr->setJobStatus(JS_Running);
117 
118    foreach_res(job, R_JOB) {
119       if (job->AlwaysIncremental) {
120          db_list_ctx jobids_ctx;
121          int32_t incrementals_total;
122          int32_t incrementals_to_consolidate;
123          int32_t max_incrementals_to_consolidate;
124 
125          Jmsg(jcr, M_INFO, 0, _("Looking at always incremental job %s\n"), job->name());
126 
127          /*
128           * Fake always incremental job as job of current jcr.
129           */
130          jcr->res.job = job;
131          jcr->res.fileset = job->fileset;
132          jcr->res.client = job->client;
133          jcr->jr.JobLevel = L_INCREMENTAL;
134          jcr->jr.limit = 0;
135          jcr->jr.StartTime = 0;
136 
137          if (!GetOrCreateFilesetRecord(jcr)) {
138             Jmsg(jcr, M_FATAL, 0, _("JobId=%d no FileSet\n"), (int)jcr->JobId);
139             retval = false;
140             goto bail_out;
141          }
142 
143          if (!GetOrCreateClientRecord(jcr)) {
144             Jmsg(jcr, M_FATAL, 0, _("JobId=%d no ClientId\n"), (int)jcr->JobId);
145             retval = false;
146             goto bail_out;
147          }
148 
149          /*
150           * First determine the number of total incrementals
151           */
152          jcr->db->AccurateGetJobids(jcr, &jcr->jr, &jobids_ctx);
153          incrementals_total = jobids_ctx.count - 1;
154          Dmsg1(10, "unlimited jobids list:  %s.\n", jobids_ctx.list);
155 
156          /*
157           * If we are doing always incremental, we need to limit the search to
158           * only include incrementals that are older than (now - AlwaysIncrementalJobRetention)
159           */
160          if (job->AlwaysIncrementalJobRetention) {
161             char sdt[50];
162 
163             jcr->jr.StartTime = now - job->AlwaysIncrementalJobRetention;
164             bstrftimes(sdt, sizeof(sdt), jcr->jr.StartTime);
165             Jmsg(jcr, M_INFO, 0, _("%s: considering jobs older than %s for consolidation.\n"), job->name(), sdt);
166             Dmsg4(10, _("%s: considering jobs with ClientId %d and FilesetId %d older than %s for consolidation.\n"),
167                   job->name(), jcr->jr.ClientId, jcr->jr.FileSetId, sdt);
168          }
169 
170          jcr->db->AccurateGetJobids(jcr, &jcr->jr, &jobids_ctx);
171          Dmsg1(10, "consolidate candidates:  %s.\n", jobids_ctx.list);
172 
173          /**
174           * Consolidation of zero or one job does not make sense, we leave it like it is
175           */
176          if (incrementals_total < 1) {
177             Jmsg(jcr, M_INFO, 0, _("%s: less than two jobs to consolidate found, doing nothing.\n"), job->name());
178             continue;
179          }
180 
181          /**
182           * Calculate limit for query. We specify how many incrementals should be left.
183           * the limit is total number of incrementals - number required - 1
184           */
185          max_incrementals_to_consolidate = incrementals_total - job->AlwaysIncrementalKeepNumber;
186 
187          Dmsg2(10, "Incrementals found/required. (%d/%d).\n", incrementals_total, job->AlwaysIncrementalKeepNumber);
188          if ((max_incrementals_to_consolidate + 1 ) > 1) {
189             jcr->jr.limit = max_incrementals_to_consolidate + 1;
190             Dmsg3(10, "total: %d, to_consolidate: %d, limit: %d.\n", incrementals_total, max_incrementals_to_consolidate, jcr->jr.limit);
191             jobids_ctx.reset();
192             jcr->db->AccurateGetJobids(jcr, &jcr->jr, &jobids_ctx);
193             incrementals_to_consolidate = jobids_ctx.count - 1;
194             Dmsg2(10, "%d consolidate ids after limit: %s.\n", jobids_ctx.count, jobids_ctx.list);
195             if (incrementals_to_consolidate < 1) {
196                Jmsg(jcr, M_INFO, 0, _("%s: After limited query: less incrementals than required, not consolidating\n"), job->name());
197                continue;
198             }
199          } else {
200             Jmsg(jcr, M_INFO, 0, _("%s: less incrementals than required, not consolidating\n"), job->name());
201             continue;
202          }
203 
204          if (jobids) {
205             free(jobids);
206             jobids = NULL;
207          }
208 
209          jobids = bstrdup(jobids_ctx.list);
210          p = jobids;
211 
212          /**
213           * Check if we need to skip the first (full) job from consolidation
214           */
215          if (job->AlwaysIncrementalMaxFullAge) {
216             char sdt_allowed[50];
217             char sdt_starttime[50];
218             time_t starttime,
219                    oldest_allowed_starttime;
220 
221             if (incrementals_to_consolidate < 2) {
222                Jmsg(jcr, M_INFO, 0, _("%s: less incrementals than required to consolidate without full, not consolidating\n"), job->name());
223                continue;
224             }
225             Jmsg(jcr, M_INFO, 0, _("before ConsolidateFull: jobids: %s\n"), jobids);
226 
227             p = strchr(jobids, ',');                /* find oldest jobid and optionally skip it */
228             if (p) {
229                *p = '\0';
230             }
231 
232             /**
233              * Get db record of oldest jobid and check its age
234              */
235             memset(&jcr->previous_jr, 0, sizeof(jcr->previous_jr));
236             jcr->previous_jr.JobId = str_to_int64(jobids);
237             Dmsg1(10, "Previous JobId=%s\n", jobids);
238 
239             if (!jcr->db->GetJobRecord(jcr, &jcr->previous_jr)) {
240                Jmsg(jcr, M_FATAL, 0, _("Error getting Job record for first Job: ERR=%s"), jcr->db->strerror());
241                goto bail_out;
242             }
243 
244             starttime = jcr->previous_jr.JobTDate;
245             oldest_allowed_starttime = now - job->AlwaysIncrementalMaxFullAge;
246             bstrftimes(sdt_allowed, sizeof(sdt_allowed), oldest_allowed_starttime);
247             bstrftimes(sdt_starttime, sizeof(sdt_starttime), starttime);
248 
249             /**
250              * Check if job is older than AlwaysIncrementalMaxFullAge
251              */
252             Jmsg(jcr, M_INFO, 0,  _("check full age: full is %s, allowed is %s\n"), sdt_starttime, sdt_allowed);
253             if (starttime > oldest_allowed_starttime) {
254                Jmsg(jcr, M_INFO, 0, _("Full is newer than AlwaysIncrementalMaxFullAge -> skipping first jobid %s because of age\n"), jobids);
255                if (p) {
256                   *p++ = ','; /* Restore , and point to rest of list */
257                }
258 
259             } else if (max_full_consolidations &&
260                        fullconsolidations_started >= max_full_consolidations) {
261                Jmsg(jcr, M_INFO, 0, _("%d AlwaysIncrementalFullConsolidations reached -> skipping first jobid %s independent of age\n"),
262                        max_full_consolidations, jobids);
263                if (p) {
264                   *p++ = ','; /* Restore , and point to rest of list */
265                }
266 
267             } else {
268                Jmsg(jcr, M_INFO, 0, _("Full is older than AlwaysIncrementalMaxFullAge -> also consolidating Full jobid %s\n"), jobids);
269                if (p) {
270                   *p = ',';   /* Restore ,*/
271                   p = jobids; /* Point to full list */
272                }
273                fullconsolidations_started++;
274             }
275             Jmsg(jcr, M_INFO, 0, _("after ConsolidateFull: jobids: %s\n"), p);
276          }
277 
278          /**
279           * Set the virtualfull jobids to be consolidated
280           */
281          if (!jcr->vf_jobids) {
282             jcr->vf_jobids = GetPoolMemory(PM_MESSAGE);
283          }
284          PmStrcpy(jcr->vf_jobids, p);
285 
286          Jmsg(jcr, M_INFO, 0, _("%s: Start new consolidation\n"), job->name());
287          StartNewConsolidationJob(jcr, job->name());
288       }
289    }
290 
291 bail_out:
292    /**
293     * Restore original job back to jcr.
294     */
295    jcr->res.job = tmpjob;
296    jcr->setJobStatus(JS_Terminated);
297    ConsolidateCleanup(jcr, JS_Terminated);
298 
299    if (jobids) {
300       free(jobids);
301    }
302 
303    return retval;
304 }
305 
306 /**
307  * Release resources allocated during backup.
308  */
ConsolidateCleanup(JobControlRecord * jcr,int TermCode)309 void ConsolidateCleanup(JobControlRecord *jcr, int TermCode)
310 {
311    int msg_type;
312    char term_code[100];
313    const char *TermMsg;
314    char sdt[50], edt[50], schedt[50];
315 
316    Dmsg0(debuglevel, "Enter backup_cleanup()\n");
317 
318    UpdateJobEnd(jcr, TermCode);
319 
320    if (!jcr->db->GetJobRecord(jcr, &jcr->jr)) {
321       Jmsg(jcr, M_WARNING, 0, _("Error getting Job record for Job report: ERR=%s"), jcr->db->strerror());
322       jcr->setJobStatus(JS_ErrorTerminated);
323    }
324 
325    msg_type = M_INFO;                 /* by default INFO message */
326    switch (jcr->JobStatus) {
327    case JS_Terminated:
328       TermMsg = _("Consolidate OK");
329       break;
330    case JS_FatalError:
331    case JS_ErrorTerminated:
332       TermMsg = _("*** Consolidate Error ***");
333       msg_type = M_ERROR;          /* Generate error message */
334       break;
335    case JS_Canceled:
336       TermMsg = _("Consolidate Canceled");
337       break;
338    default:
339       TermMsg = term_code;
340       sprintf(term_code, _("Inappropriate term code: %c\n"), jcr->JobStatus);
341       break;
342    }
343    bstrftimes(schedt, sizeof(schedt), jcr->jr.SchedTime);
344    bstrftimes(sdt, sizeof(sdt), jcr->jr.StartTime);
345    bstrftimes(edt, sizeof(edt), jcr->jr.EndTime);
346 
347    Jmsg(jcr, msg_type, 0, _("BAREOS " VERSION " (" LSMDATE "): %s\n"
348         "  JobId:                  %d\n"
349         "  Job:                    %s\n"
350         "  Scheduled time:         %s\n"
351         "  Start time:             %s\n"
352         "  End time:               %s\n"
353         "  Bareos binary info:     %s\n"
354         "  Termination:            %s\n\n"),
355         edt,
356         jcr->jr.JobId,
357         jcr->jr.Job,
358         schedt,
359         sdt,
360         edt,
361         BAREOS_JOBLOG_MESSAGE,
362         TermMsg);
363 
364    Dmsg0(debuglevel, "Leave ConsolidateCleanup()\n");
365 }
366 } /* namespace directordaemon */
367