]> git.sur5r.net Git - bacula/bacula/blob - bacula/src/dird/ua_purge.c
ebl tweak debug
[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, /*force*/true);
136             
137 purge_jobs_from_volume(ua, &mr);
138          }
139          return 1;
140       }
141    /* Volume */
142    case 2:
143       while ((i=find_arg(ua, NT_("volume"))) >= 0) {
144          if (select_media_dbr(ua, &mr)) {
145             purge_jobs_from_volume(ua, &mr, /*force*/true);
146          }
147          *ua->argk[i] = 0;            /* zap keyword already seen */
148          ua->send_msg("\n");
149       }
150       return 1;
151    default:
152       break;
153    }
154    switch (do_keyword_prompt(ua, _("Choose item to purge"), keywords)) {
155    case 0:                            /* files */
156       client = get_client_resource(ua);
157       if (client) {
158          purge_files_from_client(ua, client);
159       }
160       break;
161    case 1:                            /* jobs */
162       client = get_client_resource(ua);
163       if (client) {
164          purge_jobs_from_client(ua, client);
165       }
166       break;
167    case 2:                            /* Volume */
168       if (select_media_dbr(ua, &mr)) {
169          purge_jobs_from_volume(ua, &mr, /*force*/true);
170       }
171       break;
172    }
173    return 1;
174 }
175
176 /*
177  * Purge File records from the database. For any Job which
178  * is older than the retention period, we unconditionally delete
179  * all File records for that Job.  This is simple enough that no
180  * temporary tables are needed. We simply make an in memory list of
181  * the JobIds meeting the prune conditions, then delete all File records
182  * pointing to each of those JobIds.
183  */
184 static int purge_files_from_client(UAContext *ua, CLIENT *client)
185 {
186    struct del_ctx del;
187    POOL_MEM query(PM_MESSAGE);
188    CLIENT_DBR cr;
189    char ed1[50];
190
191    memset(&cr, 0, sizeof(cr));
192    bstrncpy(cr.Name, client->name(), sizeof(cr.Name));
193    if (!db_create_client_record(ua->jcr, ua->db, &cr)) {
194       return 0;
195    }
196
197    memset(&del, 0, sizeof(del));
198    del.max_ids = 1000;
199    del.JobId = (JobId_t *)malloc(sizeof(JobId_t) * del.max_ids);
200
201    ua->info_msg(_("Begin purging files for Client \"%s\"\n"), cr.Name);
202
203    Mmsg(query, select_jobsfiles_from_client, edit_int64(cr.ClientId, ed1));
204    Dmsg1(050, "select sql=%s\n", query.c_str());
205    db_sql_query(ua->db, query.c_str(), file_delete_handler, (void *)&del);
206
207    purge_files_from_job_list(ua, del);
208
209    if (del.num_ids == 0) {
210       ua->warning_msg(_("No Files found for client %s to purge from %s catalog.\n"),
211          client->name(), client->catalog->name());
212    } else {
213       ua->info_msg(_("Files for %d Jobs for client \"%s\" purged from %s catalog.\n"), del.num_ids,
214          client->name(), client->catalog->name());
215    }
216
217    if (del.JobId) {
218       free(del.JobId);
219    }
220    return 1;
221 }
222
223
224
225 /*
226  * Purge Job records from the database. For any Job which
227  * is older than the retention period, we unconditionally delete
228  * it and all File records for that Job.  This is simple enough that no
229  * temporary tables are needed. We simply make an in memory list of
230  * the JobIds then delete the Job, Files, and JobMedia records in that list.
231  */
232 static int purge_jobs_from_client(UAContext *ua, CLIENT *client)
233 {
234    struct del_ctx del;
235    POOL_MEM query(PM_MESSAGE);
236    CLIENT_DBR cr;
237    char ed1[50];
238
239    memset(&cr, 0, sizeof(cr));
240
241    bstrncpy(cr.Name, client->name(), sizeof(cr.Name));
242    if (!db_create_client_record(ua->jcr, ua->db, &cr)) {
243       return 0;
244    }
245
246    memset(&del, 0, sizeof(del));
247    del.max_ids = 1000;
248    del.JobId = (JobId_t *)malloc(sizeof(JobId_t) * del.max_ids);
249    del.PurgedFiles = (char *)malloc(del.max_ids);
250    
251    ua->info_msg(_("Begin purging jobs from Client \"%s\"\n"), cr.Name);
252
253    Mmsg(query, select_jobs_from_client, edit_int64(cr.ClientId, ed1));
254    Dmsg1(150, "select sql=%s\n", query.c_str());
255    db_sql_query(ua->db, query.c_str(), job_delete_handler, (void *)&del);
256
257    purge_job_list_from_catalog(ua, del);
258
259    if (del.num_ids == 0) {
260       ua->warning_msg(_("No Files found for client %s to purge from %s catalog.\n"),
261          client->name(), client->catalog->name());
262    } else {
263       ua->info_msg(_("%d Jobs for client %s purged from %s catalog.\n"), del.num_ids,
264          client->name(), client->catalog->name());
265    }
266
267    if (del.JobId) {
268       free(del.JobId);
269    }
270    if (del.PurgedFiles) {
271       free(del.PurgedFiles);
272    }
273    return 1;
274 }
275
276
277 /*
278  * Remove File records from a list of JobIds
279  */
280 void purge_files_from_jobs(UAContext *ua, char *jobs)
281 {
282    POOL_MEM query(PM_MESSAGE);
283
284    Mmsg(query, "DELETE FROM File WHERE JobId IN (%s)", jobs);
285    db_sql_query(ua->db, query.c_str(), NULL, (void *)NULL);
286    Dmsg1(050, "Delete File sql=%s\n", query.c_str());
287
288    /*
289     * Now mark Job as having files purged. This is necessary to
290     * avoid having too many Jobs to process in future prunings. If
291     * we don't do this, the number of JobId's in our in memory list
292     * could grow very large.
293     */
294    Mmsg(query, "UPDATE Job SET PurgedFiles=1 WHERE JobId IN (%s)", jobs);
295    db_sql_query(ua->db, query.c_str(), NULL, (void *)NULL);
296    Dmsg1(050, "Mark purged sql=%s\n", query.c_str());
297 }
298
299 /*
300  * Delete jobs (all records) from the catalog in groups of 1000
301  *  at a time.
302  */
303 void purge_job_list_from_catalog(UAContext *ua, del_ctx &del)
304 {
305    POOL_MEM jobids(PM_MESSAGE);
306    char ed1[50];
307
308    for (int i=0; del.num_ids; ) {
309       Dmsg1(150, "num_ids=%d\n", del.num_ids);
310       pm_strcat(jobids, "");
311       for (int j=0; j<1000 && del.num_ids>0; j++) {
312          del.num_ids--;
313          if (del.JobId[i] == 0 || ua->jcr->JobId == del.JobId[i]) {
314             Dmsg2(150, "skip JobId[%d]=%d\n", i, (int)del.JobId[i]);
315             i++;
316             continue;
317          }
318          if (*jobids.c_str() != 0) {
319             pm_strcat(jobids, ",");
320          }
321          pm_strcat(jobids, edit_int64(del.JobId[i++], ed1));
322          Dmsg1(150, "Add id=%s\n", ed1);
323          del.num_del++;
324       }
325       Dmsg1(150, "num_ids=%d\n", del.num_ids);
326       purge_jobs_from_catalog(ua, jobids.c_str());
327    }
328 }
329
330 /*
331  * Delete files from a list of jobs in groups of 1000
332  *  at a time.
333  */
334 void purge_files_from_job_list(UAContext *ua, del_ctx &del)
335 {
336    POOL_MEM jobids(PM_MESSAGE);
337    char ed1[50];
338    /*
339     * OK, now we have the list of JobId's to be pruned, send them
340     *   off to be deleted batched 1000 at a time.
341     */
342    for (int i=0; del.num_ids; ) {
343       pm_strcat(jobids, "");
344       for (int j=0; j<1000 && del.num_ids>0; j++) {
345          del.num_ids--;
346          if (del.JobId[i] == 0 || ua->jcr->JobId == del.JobId[i]) {
347             Dmsg2(150, "skip JobId[%d]=%d\n", i, (int)del.JobId[i]);
348             i++;
349             continue;
350          }
351          if (*jobids.c_str() != 0) {
352             pm_strcat(jobids, ",");
353          }
354          pm_strcat(jobids, edit_int64(del.JobId[i++], ed1));
355          Dmsg1(150, "Add id=%s\n", ed1);
356          del.num_del++;
357       }
358       purge_files_from_jobs(ua, jobids.c_str());
359    }
360 }
361
362 /*
363  * Change the type of the next copy job to backup.
364  * We need to upgrade the next copy of a normal job,
365  * and also upgrade the next copy when the normal job
366  * already have been purged.
367  *
368  *   JobId: 1   PriorJobId: 0    (original)
369  *   JobId: 2   PriorJobId: 1    (first copy)
370  *   JobId: 3   PriorJobId: 1    (second copy)
371  *
372  *   JobId: 2   PriorJobId: 1    (first copy, now regular backup)
373  *   JobId: 3   PriorJobId: 1    (second copy)
374  *
375  *  => Search through PriorJobId in jobid and
376  *                    PriorJobId in PriorJobId (jobid)
377  */
378 void upgrade_copies(UAContext *ua, char *jobs)
379 {
380    POOL_MEM query(PM_MESSAGE);
381    
382    db_lock(ua->db);
383    /* Do it in two times for mysql */
384    Mmsg(query, "CREATE TEMPORARY TABLE cpy_tmp AS "
385                   "SELECT MIN(JobId) AS JobId FROM Job "     /* Choose the oldest job */
386                    "WHERE Type='%c' "
387                      "AND ( PriorJobId IN (%s) "
388                          "OR "
389                           " PriorJobId IN ( "
390                              "SELECT PriorJobId "
391                                "FROM Job "
392                               "WHERE JobId IN (%s) "
393                                " AND Type='B' "
394                             ") "
395                          ") "
396                    "GROUP BY PriorJobId ",           /* one result per copy */
397         JT_JOB_COPY, jobs, jobs);
398    db_sql_query(ua->db, query.c_str(), NULL, (void *)NULL);
399    Dmsg1(050, "Upgrade copies Log sql=%s\n", query.c_str());
400
401    /* Now upgrade first copy to Backup */
402    Mmsg(query, "UPDATE Job SET Type='B' "           /* JT_JOB_COPY => JT_BACKUP  */
403                 "WHERE JobId IN ( SELECT JobId FROM cpy_tmp )");
404
405    db_sql_query(ua->db, query.c_str(), NULL, (void *)NULL);
406
407    Mmsg(query, "DROP TABLE cpy_tmp");
408    db_sql_query(ua->db, query.c_str(), NULL, (void *)NULL);
409
410    db_unlock(ua->db);
411 }
412
413 /*
414  * Remove all records from catalog for a list of JobIds
415  */
416 void purge_jobs_from_catalog(UAContext *ua, char *jobs)
417 {
418    POOL_MEM query(PM_MESSAGE);
419
420    /* Delete (or purge) records associated with the job */
421    purge_files_from_jobs(ua, jobs);
422
423    Mmsg(query, "DELETE FROM JobMedia WHERE JobId IN (%s)", jobs);
424    db_sql_query(ua->db, query.c_str(), NULL, (void *)NULL);
425    Dmsg1(050, "Delete JobMedia sql=%s\n", query.c_str());
426
427    Mmsg(query, "DELETE FROM Log WHERE JobId IN (%s)", jobs);
428    db_sql_query(ua->db, query.c_str(), NULL, (void *)NULL);
429    Dmsg1(050, "Delete Log sql=%s\n", query.c_str());
430
431    upgrade_copies(ua, jobs);
432
433    /* Now remove the Job record itself */
434    Mmsg(query, "DELETE FROM Job WHERE JobId IN (%s)", jobs);
435    db_sql_query(ua->db, query.c_str(), NULL, (void *)NULL);
436
437    Dmsg1(050, "Delete Job sql=%s\n", query.c_str());
438 }
439
440 void purge_files_from_volume(UAContext *ua, MEDIA_DBR *mr )
441 {} /* ***FIXME*** implement */
442
443 /*
444  * Returns: 1 if Volume purged
445  *          0 if Volume not purged
446  */
447 bool purge_jobs_from_volume(UAContext *ua, MEDIA_DBR *mr, bool force)
448 {
449    POOL_MEM query(PM_MESSAGE);
450    struct del_ctx del;
451    int i;
452    bool purged = false;
453    bool stat;
454    JOB_DBR jr;
455    char ed1[50];
456
457    stat = strcmp(mr->VolStatus, "Append") == 0 ||
458           strcmp(mr->VolStatus, "Full")   == 0 ||
459           strcmp(mr->VolStatus, "Used")   == 0 ||
460           strcmp(mr->VolStatus, "Error")  == 0;
461    if (!stat) {
462       ua->error_msg(_("\nVolume \"%s\" has VolStatus \"%s\" and cannot be purged.\n"
463                      "The VolStatus must be: Append, Full, Used, or Error to be purged.\n"),
464                      mr->VolumeName, mr->VolStatus);
465       return 0;
466    }
467
468    memset(&jr, 0, sizeof(jr));
469    memset(&del, 0, sizeof(del));
470    del.max_ids = 1000;
471    del.JobId = (JobId_t *)malloc(sizeof(JobId_t) * del.max_ids);
472
473    /*
474     * Check if he wants to purge a single jobid
475     */
476    i = find_arg_with_value(ua, "jobid");
477    if (i >= 0) {
478       del.num_ids = 1;
479       del.JobId[0] = str_to_int64(ua->argv[i]);
480    } else {
481       /*
482        * Purge ALL JobIds
483        */
484       Mmsg(query, "SELECT DISTINCT JobId FROM JobMedia WHERE MediaId=%s", 
485            edit_int64(mr->MediaId, ed1));
486       if (!db_sql_query(ua->db, query.c_str(), file_delete_handler, (void *)&del)) {
487          ua->error_msg("%s", db_strerror(ua->db));
488          Dmsg0(050, "Count failed\n");
489          goto bail_out;
490       }
491    }
492
493    purge_job_list_from_catalog(ua, del);
494
495    ua->info_msg(_("%d File%s on Volume \"%s\" purged from catalog.\n"), del.num_del,
496       del.num_del==1?"":"s", mr->VolumeName);
497
498    purged = is_volume_purged(ua, mr, force); 
499
500 bail_out:
501    if (del.JobId) {
502       free(del.JobId);
503    }
504    return purged;
505 }
506
507 /*
508  * This routine will check the JobMedia records to see if the
509  *   Volume has been purged. If so, it marks it as such and
510  *
511  * Returns: true if volume purged
512  *          false if not
513  *
514  * Note, we normally will not purge a volume that has Firstor LastWritten
515  *   zero, because it means the volume is most likely being written
516  *   however, if the user manually purges using the purge command in
517  *   the console, he has been warned, and we go ahead and purge
518  *   the volume anyway, if possible).
519  */
520 bool is_volume_purged(UAContext *ua, MEDIA_DBR *mr, bool force)
521 {
522    POOL_MEM query(PM_MESSAGE);
523    struct s_count_ctx cnt;
524    bool purged = false;
525    char ed1[50];
526
527    if (!force && (mr->FirstWritten == 0 || mr->LastWritten == 0)) {
528       goto bail_out;               /* not written cannot purge */
529    }
530
531    if (strcmp(mr->VolStatus, "Purged") == 0) {
532       purged = true;
533       goto bail_out;
534    }
535
536    /* If purged, mark it so */
537    cnt.count = 0;
538    Mmsg(query, "SELECT count(*) FROM JobMedia WHERE MediaId=%s", 
539         edit_int64(mr->MediaId, ed1));
540    if (!db_sql_query(ua->db, query.c_str(), del_count_handler, (void *)&cnt)) {
541       ua->error_msg("%s", db_strerror(ua->db));
542       Dmsg0(050, "Count failed\n");
543       goto bail_out;
544    }
545
546    if (cnt.count == 0) {
547       ua->warning_msg(_("There are no more Jobs associated with Volume \"%s\". Marking it purged.\n"),
548          mr->VolumeName);
549       if (!(purged = mark_media_purged(ua, mr))) {
550          ua->error_msg("%s", db_strerror(ua->db));
551       }
552    }
553 bail_out:
554    return purged;
555 }
556
557 /*
558  * IF volume status is Append, Full, Used, or Error, mark it Purged
559  *   Purged volumes can then be recycled (if enabled).
560  */
561 bool mark_media_purged(UAContext *ua, MEDIA_DBR *mr)
562 {
563    JCR *jcr = ua->jcr;
564    if (strcmp(mr->VolStatus, "Append") == 0 ||
565        strcmp(mr->VolStatus, "Full")   == 0 ||
566        strcmp(mr->VolStatus, "Used")   == 0 ||
567        strcmp(mr->VolStatus, "Error")  == 0) {
568       bstrncpy(mr->VolStatus, "Purged", sizeof(mr->VolStatus));
569       if (!db_update_media_record(jcr, ua->db, mr)) {
570          return false;
571       }
572       pm_strcpy(jcr->VolumeName, mr->VolumeName);
573       generate_job_event(jcr, "VolumePurged");
574       generate_plugin_event(jcr, bEventVolumePurged);
575       /*
576        * If the RecyclePool is defined, move the volume there
577        */
578       if (mr->RecyclePoolId && mr->RecyclePoolId != mr->PoolId) {
579          POOL_DBR oldpr, newpr;
580          memset(&oldpr, 0, sizeof(POOL_DBR));
581          memset(&newpr, 0, sizeof(POOL_DBR));
582          newpr.PoolId = mr->RecyclePoolId;
583          oldpr.PoolId = mr->PoolId;
584          if (   db_get_pool_record(jcr, ua->db, &oldpr) 
585              && db_get_pool_record(jcr, ua->db, &newpr)) 
586          {
587             /* check if destination pool size is ok */
588             if (newpr.MaxVols > 0 && newpr.NumVols >= newpr.MaxVols) {
589                ua->error_msg(_("Unable move recycled Volume in full " 
590                               "Pool \"%s\" MaxVols=%d\n"),
591                         newpr.Name, newpr.MaxVols);
592
593             } else {            /* move media */
594                update_vol_pool(ua, newpr.Name, mr, &oldpr);
595             }
596          } else {
597             ua->error_msg("%s", db_strerror(ua->db));
598          }
599       }
600       /* Send message to Job report, if it is a *real* job */           
601       if (jcr && jcr->JobId > 0) {
602          Jmsg(jcr, M_INFO, 0, _("All records pruned from Volume \"%s\"; marking it \"Purged\"\n"),
603             mr->VolumeName); 
604       }
605       return true;
606    } else {
607       ua->error_msg(_("Cannot purge Volume with VolStatus=%s\n"), mr->VolStatus);
608    }
609    return strcmp(mr->VolStatus, "Purged") == 0;
610 }