]> git.sur5r.net Git - bacula/bacula/blob - bacula/src/dird/ua_purge.c
kes Probably fix for bug #1188 where Volume is purged while writing
[bacula/bacula] / bacula / src / dird / ua_purge.c
1 /*
2    Bacula® - The Network Backup Solution
3
4    Copyright (C) 2002-2008 Free Software Foundation Europe e.V.
5
6    The main author of Bacula is Kern Sibbald, with contributions from
7    many others, a complete list can be found in the file AUTHORS.
8    This program is Free Software; you can redistribute it and/or
9    modify it under the terms of version two of the GNU 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    General Public License for more details.
17
18    You should have received a copy of the GNU 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    Bacula® is a registered trademark of Kern Sibbald.
24    The licensor of Bacula is the Free Software Foundation Europe
25    (FSFE), Fiduciary Program, Sumatrastrasse 25, 8006 Zürich,
26    Switzerland, email:ftf@fsfeurope.org.
27 */
28 /*
29  *
30  *   Bacula Director -- User Agent Database Purge Command
31  *
32  *      Purges Files from specific JobIds
33  * or
34  *      Purges Jobs from Volumes
35  *
36  *     Kern Sibbald, February MMII
37  *
38  *   Version $Id$
39  */
40
41 #include "bacula.h"
42 #include "dird.h"
43
44 /* Forward referenced functions */
45 static int purge_files_from_client(UAContext *ua, CLIENT *client);
46 static int purge_jobs_from_client(UAContext *ua, CLIENT *client);
47
48 static const char *select_jobsfiles_from_client =
49    "SELECT JobId FROM Job "
50    "WHERE ClientId=%s "
51    "AND PurgedFiles=0";
52
53 static const char *select_jobs_from_client =
54    "SELECT JobId, PurgedFiles FROM Job "
55    "WHERE ClientId=%s";
56
57 /*
58  *   Purge records from database
59  *
60  *     Purge Files (from) [Job|JobId|Client|Volume]
61  *     Purge Jobs  (from) [Client|Volume]
62  *
63  *  N.B. Not all above is implemented yet.
64  */
65 int purgecmd(UAContext *ua, const char *cmd)
66 {
67    int i;
68    CLIENT *client;
69    MEDIA_DBR mr;
70    JOB_DBR  jr;
71    static const char *keywords[] = {
72       NT_("files"),
73       NT_("jobs"),
74       NT_("volume"),
75       NULL};
76
77    static const char *files_keywords[] = {
78       NT_("Job"),
79       NT_("JobId"),
80       NT_("Client"),
81       NT_("Volume"),
82       NULL};
83
84    static const char *jobs_keywords[] = {
85       NT_("Client"),
86       NT_("Volume"),
87       NULL};
88
89    ua->warning_msg(_(
90       "\nThis command is can be DANGEROUS!!!\n\n"
91       "It purges (deletes) all Files from a Job,\n"
92       "JobId, Client or Volume; or it purges (deletes)\n"
93       "all Jobs from a Client or Volume without regard\n"
94       "for retention periods. Normally you should use the\n"
95       "PRUNE command, which respects retention periods.\n"));
96
97    if (!open_db(ua)) {
98       return 1;
99    }
100    switch (find_arg_keyword(ua, keywords)) {
101    /* Files */
102    case 0:
103       switch(find_arg_keyword(ua, files_keywords)) {
104       case 0:                         /* Job */
105       case 1:                         /* JobId */
106          if (get_job_dbr(ua, &jr)) {
107             char jobid[50];
108             edit_int64(jr.JobId, jobid);
109             purge_files_from_jobs(ua, jobid);
110          }
111          return 1;
112       case 2:                         /* client */
113          client = get_client_resource(ua);
114          if (client) {
115             purge_files_from_client(ua, client);
116          }
117          return 1;
118       case 3:                         /* Volume */
119          if (select_media_dbr(ua, &mr)) {
120             purge_files_from_volume(ua, &mr);
121          }
122          return 1;
123       }
124    /* Jobs */
125    case 1:
126       switch(find_arg_keyword(ua, jobs_keywords)) {
127       case 0:                         /* client */
128          client = get_client_resource(ua);
129          if (client) {
130             purge_jobs_from_client(ua, client);
131          }
132          return 1;
133       case 1:                         /* Volume */
134          if (select_media_dbr(ua, &mr)) {
135             purge_jobs_from_volume(ua, &mr);
136          }
137          return 1;
138       }
139    /* Volume */
140    case 2:
141       while ((i=find_arg(ua, NT_("volume"))) >= 0) {
142          if (select_media_dbr(ua, &mr)) {
143             purge_jobs_from_volume(ua, &mr);
144          }
145          *ua->argk[i] = 0;            /* zap keyword already seen */
146          ua->send_msg("\n");
147       }
148       return 1;
149    default:
150       break;
151    }
152    switch (do_keyword_prompt(ua, _("Choose item to purge"), keywords)) {
153    case 0:                            /* files */
154       client = get_client_resource(ua);
155       if (client) {
156          purge_files_from_client(ua, client);
157       }
158       break;
159    case 1:                            /* jobs */
160       client = get_client_resource(ua);
161       if (client) {
162          purge_jobs_from_client(ua, client);
163       }
164       break;
165    case 2:                            /* Volume */
166       if (select_media_dbr(ua, &mr)) {
167          purge_jobs_from_volume(ua, &mr);
168       }
169       break;
170    }
171    return 1;
172 }
173
174 /*
175  * Purge File records from the database. For any Job which
176  * is older than the retention period, we unconditionally delete
177  * all File records for that Job.  This is simple enough that no
178  * temporary tables are needed. We simply make an in memory list of
179  * the JobIds meeting the prune conditions, then delete all File records
180  * pointing to each of those JobIds.
181  */
182 static int purge_files_from_client(UAContext *ua, CLIENT *client)
183 {
184    struct del_ctx del;
185    POOL_MEM query(PM_MESSAGE);
186    CLIENT_DBR cr;
187    char ed1[50];
188
189    memset(&cr, 0, sizeof(cr));
190    bstrncpy(cr.Name, client->name(), sizeof(cr.Name));
191    if (!db_create_client_record(ua->jcr, ua->db, &cr)) {
192       return 0;
193    }
194
195    memset(&del, 0, sizeof(del));
196    del.max_ids = 1000;
197    del.JobId = (JobId_t *)malloc(sizeof(JobId_t) * del.max_ids);
198
199    ua->info_msg(_("Begin purging files for Client \"%s\"\n"), cr.Name);
200
201    Mmsg(query, select_jobsfiles_from_client, edit_int64(cr.ClientId, ed1));
202    Dmsg1(050, "select sql=%s\n", query.c_str());
203    db_sql_query(ua->db, query.c_str(), file_delete_handler, (void *)&del);
204
205    purge_files_from_job_list(ua, del);
206
207    if (del.num_ids == 0) {
208       ua->warning_msg(_("No Files found for client %s to purge from %s catalog.\n"),
209          client->name(), client->catalog->name());
210    } else {
211       ua->info_msg(_("Files for %d Jobs for client \"%s\" purged from %s catalog.\n"), del.num_ids,
212          client->name(), client->catalog->name());
213    }
214
215    if (del.JobId) {
216       free(del.JobId);
217    }
218    return 1;
219 }
220
221
222
223 /*
224  * Purge Job records from the database. For any Job which
225  * is older than the retention period, we unconditionally delete
226  * it and all File records for that Job.  This is simple enough that no
227  * temporary tables are needed. We simply make an in memory list of
228  * the JobIds then delete the Job, Files, and JobMedia records in that list.
229  */
230 static int purge_jobs_from_client(UAContext *ua, CLIENT *client)
231 {
232    struct del_ctx del;
233    POOL_MEM query(PM_MESSAGE);
234    CLIENT_DBR cr;
235    char ed1[50];
236
237    memset(&cr, 0, sizeof(cr));
238
239    bstrncpy(cr.Name, client->name(), sizeof(cr.Name));
240    if (!db_create_client_record(ua->jcr, ua->db, &cr)) {
241       return 0;
242    }
243
244    memset(&del, 0, sizeof(del));
245    del.max_ids = 1000;
246    del.JobId = (JobId_t *)malloc(sizeof(JobId_t) * del.max_ids);
247    del.PurgedFiles = (char *)malloc(del.max_ids);
248    
249    ua->info_msg(_("Begin purging jobs from Client \"%s\"\n"), cr.Name);
250
251    Mmsg(query, select_jobs_from_client, edit_int64(cr.ClientId, ed1));
252    Dmsg1(150, "select sql=%s\n", query.c_str());
253    db_sql_query(ua->db, query.c_str(), job_delete_handler, (void *)&del);
254
255    purge_job_list_from_catalog(ua, del);
256
257    if (del.num_ids == 0) {
258       ua->warning_msg(_("No Files found for client %s to purge from %s catalog.\n"),
259          client->name(), client->catalog->name());
260    } else {
261       ua->info_msg(_("%d Jobs for client %s purged from %s catalog.\n"), del.num_ids,
262          client->name(), client->catalog->name());
263    }
264
265    if (del.JobId) {
266       free(del.JobId);
267    }
268    if (del.PurgedFiles) {
269       free(del.PurgedFiles);
270    }
271    return 1;
272 }
273
274
275 /*
276  * Remove File records from a list of JobIds
277  */
278 void purge_files_from_jobs(UAContext *ua, char *jobs)
279 {
280    POOL_MEM query(PM_MESSAGE);
281
282    Mmsg(query, "DELETE FROM File WHERE JobId IN (%s)", jobs);
283    db_sql_query(ua->db, query.c_str(), NULL, (void *)NULL);
284    Dmsg1(050, "Delete File sql=%s\n", query.c_str());
285
286    /*
287     * Now mark Job as having files purged. This is necessary to
288     * avoid having too many Jobs to process in future prunings. If
289     * we don't do this, the number of JobId's in our in memory list
290     * could grow very large.
291     */
292    Mmsg(query, "UPDATE Job SET PurgedFiles=1 WHERE JobId IN (%s)", jobs);
293    db_sql_query(ua->db, query.c_str(), NULL, (void *)NULL);
294    Dmsg1(050, "Mark purged sql=%s\n", query.c_str());
295 }
296
297 /*
298  * Delete jobs (all records) from the catalog in groups of 1000
299  *  at a time.
300  */
301 void purge_job_list_from_catalog(UAContext *ua, del_ctx &del)
302 {
303    POOL_MEM jobids(PM_MESSAGE);
304    char ed1[50];
305
306    for (int i=0; del.num_ids; ) {
307       Dmsg1(150, "num_ids=%d\n", del.num_ids);
308       pm_strcat(jobids, "");
309       for (int j=0; j<1000 && del.num_ids>0; j++) {
310          del.num_ids--;
311          if (del.JobId[i] == 0 || ua->jcr->JobId == del.JobId[i]) {
312             Dmsg2(150, "skip JobId[%d]=%d\n", i, (int)del.JobId[i]);
313             i++;
314             continue;
315          }
316          if (*jobids.c_str() != 0) {
317             pm_strcat(jobids, ",");
318          }
319          pm_strcat(jobids, edit_int64(del.JobId[i++], ed1));
320          Dmsg1(150, "Add id=%s\n", ed1);
321          del.num_del++;
322       }
323       Dmsg1(150, "num_ids=%d\n", del.num_ids);
324       purge_jobs_from_catalog(ua, jobids.c_str());
325    }
326 }
327
328 /*
329  * Delete files from a list of jobs in groups of 1000
330  *  at a time.
331  */
332 void purge_files_from_job_list(UAContext *ua, del_ctx &del)
333 {
334    POOL_MEM jobids(PM_MESSAGE);
335    char ed1[50];
336    /*
337     * OK, now we have the list of JobId's to be pruned, send them
338     *   off to be deleted batched 1000 at a time.
339     */
340    for (int i=0; del.num_ids; ) {
341       pm_strcat(jobids, "");
342       for (int j=0; j<1000 && del.num_ids>0; j++) {
343          del.num_ids--;
344          if (del.JobId[i] == 0 || ua->jcr->JobId == del.JobId[i]) {
345             Dmsg2(150, "skip JobId[%d]=%d\n", i, (int)del.JobId[i]);
346             i++;
347             continue;
348          }
349          if (*jobids.c_str() != 0) {
350             pm_strcat(jobids, ",");
351          }
352          pm_strcat(jobids, edit_int64(del.JobId[i++], ed1));
353          Dmsg1(150, "Add id=%s\n", ed1);
354          del.num_del++;
355       }
356       purge_files_from_jobs(ua, jobids.c_str());
357    }
358 }
359
360 /*
361  * Remove all records from catalog for a list of JobIds
362  */
363 void purge_jobs_from_catalog(UAContext *ua, char *jobs)
364 {
365    POOL_MEM query(PM_MESSAGE);
366
367    /* Delete (or purge) records associated with the job */
368    purge_files_from_jobs(ua, jobs);
369
370    Mmsg(query, "DELETE FROM JobMedia WHERE JobId IN (%s)", jobs);
371    db_sql_query(ua->db, query.c_str(), NULL, (void *)NULL);
372    Dmsg1(050, "Delete JobMedia sql=%s\n", query.c_str());
373
374    Mmsg(query, "DELETE FROM Log WHERE JobId IN (%s)", jobs);
375    db_sql_query(ua->db, query.c_str(), NULL, (void *)NULL);
376    Dmsg1(050, "Delete Log sql=%s\n", query.c_str());
377
378    /* Now remove the Job record itself */
379    Mmsg(query, "DELETE FROM Job WHERE JobId IN (%s)", jobs);
380    db_sql_query(ua->db, query.c_str(), NULL, (void *)NULL);
381    Dmsg1(050, "Delete Job sql=%s\n", query.c_str());
382 }
383
384
385 void purge_files_from_volume(UAContext *ua, MEDIA_DBR *mr )
386 {} /* ***FIXME*** implement */
387
388 /*
389  * Returns: 1 if Volume purged
390  *          0 if Volume not purged
391  */
392 bool purge_jobs_from_volume(UAContext *ua, MEDIA_DBR *mr)
393 {
394    POOL_MEM query(PM_MESSAGE);
395    struct del_ctx del;
396    int i;
397    bool purged = false;
398    bool stat;
399    JOB_DBR jr;
400    char ed1[50];
401
402    stat = strcmp(mr->VolStatus, "Append") == 0 ||
403           strcmp(mr->VolStatus, "Full")   == 0 ||
404           strcmp(mr->VolStatus, "Used")   == 0 ||
405           strcmp(mr->VolStatus, "Error")  == 0;
406    if (!stat) {
407       ua->error_msg(_("\nVolume \"%s\" has VolStatus \"%s\" and cannot be purged.\n"
408                      "The VolStatus must be: Append, Full, Used, or Error to be purged.\n"),
409                      mr->VolumeName, mr->VolStatus);
410       return 0;
411    }
412
413    memset(&jr, 0, sizeof(jr));
414    memset(&del, 0, sizeof(del));
415    del.max_ids = 1000;
416    del.JobId = (JobId_t *)malloc(sizeof(JobId_t) * del.max_ids);
417
418    /*
419     * Check if he wants to purge a single jobid
420     */
421    i = find_arg_with_value(ua, "jobid");
422    if (i >= 0) {
423       del.num_ids = 1;
424       del.JobId[0] = str_to_int64(ua->argv[i]);
425    } else {
426       /*
427        * Purge ALL JobIds
428        */
429       Mmsg(query, "SELECT DISTINCT JobId FROM JobMedia WHERE MediaId=%s", 
430            edit_int64(mr->MediaId, ed1));
431       if (!db_sql_query(ua->db, query.c_str(), file_delete_handler, (void *)&del)) {
432          ua->error_msg("%s", db_strerror(ua->db));
433          Dmsg0(050, "Count failed\n");
434          goto bail_out;
435       }
436    }
437
438    purge_job_list_from_catalog(ua, del);
439
440    ua->info_msg(_("%d File%s on Volume \"%s\" purged from catalog.\n"), del.num_del,
441       del.num_del==1?"":"s", mr->VolumeName);
442
443    purged = is_volume_purged(ua, mr);
444
445 bail_out:
446    if (del.JobId) {
447       free(del.JobId);
448    }
449    return purged;
450 }
451
452 /*
453  * This routine will check the JobMedia records to see if the
454  *   Volume has been purged. If so, it marks it as such and
455  *
456  * Returns: true if volume purged
457  *          false if not
458  */
459 bool is_volume_purged(UAContext *ua, MEDIA_DBR *mr)
460 {
461    POOL_MEM query(PM_MESSAGE);
462    struct s_count_ctx cnt;
463    bool purged = false;
464    char ed1[50];
465
466    if (mr->FirstWritten == 0 || mr->LastWritten == 0) {
467       goto bail_out;               /* not written cannot purge */
468    }
469    if (strcmp(mr->VolStatus, "Purged") == 0) {
470       purged = true;
471       goto bail_out;
472    }
473
474    /* If purged, mark it so */
475    cnt.count = 0;
476    Mmsg(query, "SELECT count(*) FROM JobMedia WHERE MediaId=%s", 
477         edit_int64(mr->MediaId, ed1));
478    if (!db_sql_query(ua->db, query.c_str(), del_count_handler, (void *)&cnt)) {
479       ua->error_msg("%s", db_strerror(ua->db));
480       Dmsg0(050, "Count failed\n");
481       goto bail_out;
482    }
483
484    if (cnt.count == 0) {
485       ua->warning_msg(_("There are no more Jobs associated with Volume \"%s\". Marking it purged.\n"),
486          mr->VolumeName);
487       if (!(purged = mark_media_purged(ua, mr))) {
488          ua->error_msg("%s", db_strerror(ua->db));
489       }
490    }
491 bail_out:
492    return purged;
493 }
494
495 /*
496  * IF volume status is Append, Full, Used, or Error, mark it Purged
497  *   Purged volumes can then be recycled (if enabled).
498  */
499 bool mark_media_purged(UAContext *ua, MEDIA_DBR *mr)
500 {
501    JCR *jcr = ua->jcr;
502    if (strcmp(mr->VolStatus, "Append") == 0 ||
503        strcmp(mr->VolStatus, "Full")   == 0 ||
504        strcmp(mr->VolStatus, "Used")   == 0 ||
505        strcmp(mr->VolStatus, "Error")  == 0) {
506       bstrncpy(mr->VolStatus, "Purged", sizeof(mr->VolStatus));
507       if (!db_update_media_record(jcr, ua->db, mr)) {
508          return false;
509       }
510       pm_strcpy(jcr->VolumeName, mr->VolumeName);
511       generate_job_event(jcr, "VolumePurged");
512       generate_plugin_event(jcr, bEventVolumePurged);
513       /*
514        * If the RecyclePool is defined, move the volume there
515        */
516       if (mr->RecyclePoolId && mr->RecyclePoolId != mr->PoolId) {
517          POOL_DBR oldpr, newpr;
518          memset(&oldpr, 0, sizeof(POOL_DBR));
519          memset(&newpr, 0, sizeof(POOL_DBR));
520          newpr.PoolId = mr->RecyclePoolId;
521          oldpr.PoolId = mr->PoolId;
522          if (   db_get_pool_record(jcr, ua->db, &oldpr) 
523              && db_get_pool_record(jcr, ua->db, &newpr)) 
524          {
525             /* check if destination pool size is ok */
526             if (newpr.MaxVols > 0 && newpr.NumVols >= newpr.MaxVols) {
527                ua->error_msg(_("Unable move recycled Volume in full " 
528                               "Pool \"%s\" MaxVols=%d\n"),
529                         newpr.Name, newpr.MaxVols);
530
531             } else {            /* move media */
532                update_vol_pool(ua, newpr.Name, mr, &oldpr);
533             }
534          } else {
535             ua->error_msg("%s", db_strerror(ua->db));
536          }
537       }
538       /* Send message to Job report, if it is a *real* job */           
539       if (jcr && jcr->JobId > 0) {
540          Jmsg(jcr, M_INFO, 0, _("All records pruned from Volume \"%s\"; marking it \"Purged\"\n"),
541             mr->VolumeName); 
542       }
543       return true;
544    } else {
545       ua->error_msg(_("Cannot purge Volume with VolStatus=%s\n"), mr->VolStatus);
546    }
547    return strcmp(mr->VolStatus, "Purged") == 0;
548 }