]> git.sur5r.net Git - bacula/bacula/blob - bacula/src/dird/ua_purge.c
Fix warning typo pointed out by Phil Stracchino
[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 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       "to 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          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, /*force*/true);
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, /*force*/true);
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  * Change the type of the next copy job to backup.
362  * We need to upgrade the next copy of a normal job,
363  * and also upgrade the next copy when the normal job
364  * already have been purged.
365  *
366  *   JobId: 1   PriorJobId: 0    (original)
367  *   JobId: 2   PriorJobId: 1    (first copy)
368  *   JobId: 3   PriorJobId: 1    (second copy)
369  *
370  *   JobId: 2   PriorJobId: 1    (first copy, now regular backup)
371  *   JobId: 3   PriorJobId: 1    (second copy)
372  *
373  *  => Search through PriorJobId in jobid and
374  *                    PriorJobId in PriorJobId (jobid)
375  */
376 void upgrade_copies(UAContext *ua, char *jobs)
377 {
378    POOL_MEM query(PM_MESSAGE);
379    
380    db_lock(ua->db);
381    /* Do it in two times for mysql */
382    Mmsg(query, "CREATE TEMPORARY TABLE cpy_tmp AS "
383                   "SELECT MIN(JobId) AS JobId FROM Job "     /* Choose the oldest job */
384                    "WHERE Type='%c' "
385                      "AND ( PriorJobId IN (%s) "
386                          "OR "
387                           " PriorJobId IN ( "
388                              "SELECT PriorJobId "
389                                "FROM Job "
390                               "WHERE JobId IN (%s) "
391                                " AND Type='B' "
392                             ") "
393                          ") "
394                    "GROUP BY PriorJobId ",           /* one result per copy */
395         JT_JOB_COPY, jobs, jobs);
396    db_sql_query(ua->db, query.c_str(), NULL, (void *)NULL);
397    Dmsg1(050, "Upgrade copies Log sql=%s\n", query.c_str());
398
399    /* Now upgrade first copy to Backup */
400    Mmsg(query, "UPDATE Job SET Type='B' "           /* JT_JOB_COPY => JT_BACKUP  */
401                 "WHERE JobId IN ( SELECT JobId FROM cpy_tmp )");
402
403    db_sql_query(ua->db, query.c_str(), NULL, (void *)NULL);
404
405    Mmsg(query, "DROP TABLE cpy_tmp");
406    db_sql_query(ua->db, query.c_str(), NULL, (void *)NULL);
407
408    db_unlock(ua->db);
409 }
410
411 /*
412  * Remove all records from catalog for a list of JobIds
413  */
414 void purge_jobs_from_catalog(UAContext *ua, char *jobs)
415 {
416    POOL_MEM query(PM_MESSAGE);
417
418    /* Delete (or purge) records associated with the job */
419    purge_files_from_jobs(ua, jobs);
420
421    Mmsg(query, "DELETE FROM JobMedia WHERE JobId IN (%s)", jobs);
422    db_sql_query(ua->db, query.c_str(), NULL, (void *)NULL);
423    Dmsg1(050, "Delete JobMedia sql=%s\n", query.c_str());
424
425    Mmsg(query, "DELETE FROM Log WHERE JobId IN (%s)", jobs);
426    db_sql_query(ua->db, query.c_str(), NULL, (void *)NULL);
427    Dmsg1(050, "Delete Log sql=%s\n", query.c_str());
428
429    upgrade_copies(ua, jobs);
430
431    /* Now remove the Job record itself */
432    Mmsg(query, "DELETE FROM Job WHERE JobId IN (%s)", jobs);
433    db_sql_query(ua->db, query.c_str(), NULL, (void *)NULL);
434
435    Dmsg1(050, "Delete Job sql=%s\n", query.c_str());
436 }
437
438 void purge_files_from_volume(UAContext *ua, MEDIA_DBR *mr )
439 {} /* ***FIXME*** implement */
440
441 /*
442  * Returns: 1 if Volume purged
443  *          0 if Volume not purged
444  */
445 bool purge_jobs_from_volume(UAContext *ua, MEDIA_DBR *mr, bool force)
446 {
447    POOL_MEM query(PM_MESSAGE);
448    struct del_ctx del;
449    int i;
450    bool purged = false;
451    bool stat;
452    JOB_DBR jr;
453    char ed1[50];
454
455    stat = strcmp(mr->VolStatus, "Append") == 0 ||
456           strcmp(mr->VolStatus, "Full")   == 0 ||
457           strcmp(mr->VolStatus, "Used")   == 0 ||
458           strcmp(mr->VolStatus, "Error")  == 0;
459    if (!stat) {
460       ua->error_msg(_("\nVolume \"%s\" has VolStatus \"%s\" and cannot be purged.\n"
461                      "The VolStatus must be: Append, Full, Used, or Error to be purged.\n"),
462                      mr->VolumeName, mr->VolStatus);
463       return 0;
464    }
465
466    memset(&jr, 0, sizeof(jr));
467    memset(&del, 0, sizeof(del));
468    del.max_ids = 1000;
469    del.JobId = (JobId_t *)malloc(sizeof(JobId_t) * del.max_ids);
470
471    /*
472     * Check if he wants to purge a single jobid
473     */
474    i = find_arg_with_value(ua, "jobid");
475    if (i >= 0) {
476       del.num_ids = 1;
477       del.JobId[0] = str_to_int64(ua->argv[i]);
478    } else {
479       /*
480        * Purge ALL JobIds
481        */
482       Mmsg(query, "SELECT DISTINCT JobId FROM JobMedia WHERE MediaId=%s", 
483            edit_int64(mr->MediaId, ed1));
484       if (!db_sql_query(ua->db, query.c_str(), file_delete_handler, (void *)&del)) {
485          ua->error_msg("%s", db_strerror(ua->db));
486          Dmsg0(050, "Count failed\n");
487          goto bail_out;
488       }
489    }
490
491    purge_job_list_from_catalog(ua, del);
492
493    ua->info_msg(_("%d File%s on Volume \"%s\" purged from catalog.\n"), del.num_del,
494       del.num_del==1?"":"s", mr->VolumeName);
495
496    purged = is_volume_purged(ua, mr, force); 
497
498 bail_out:
499    if (del.JobId) {
500       free(del.JobId);
501    }
502    return purged;
503 }
504
505 /*
506  * This routine will check the JobMedia records to see if the
507  *   Volume has been purged. If so, it marks it as such and
508  *
509  * Returns: true if volume purged
510  *          false if not
511  *
512  * Note, we normally will not purge a volume that has Firstor LastWritten
513  *   zero, because it means the volume is most likely being written
514  *   however, if the user manually purges using the purge command in
515  *   the console, he has been warned, and we go ahead and purge
516  *   the volume anyway, if possible).
517  */
518 bool is_volume_purged(UAContext *ua, MEDIA_DBR *mr, bool force)
519 {
520    POOL_MEM query(PM_MESSAGE);
521    struct s_count_ctx cnt;
522    bool purged = false;
523    char ed1[50];
524
525    if (!force && (mr->FirstWritten == 0 || mr->LastWritten == 0)) {
526       goto bail_out;               /* not written cannot purge */
527    }
528
529    if (strcmp(mr->VolStatus, "Purged") == 0) {
530       purged = true;
531       goto bail_out;
532    }
533
534    /* If purged, mark it so */
535    cnt.count = 0;
536    Mmsg(query, "SELECT count(*) FROM JobMedia WHERE MediaId=%s", 
537         edit_int64(mr->MediaId, ed1));
538    if (!db_sql_query(ua->db, query.c_str(), del_count_handler, (void *)&cnt)) {
539       ua->error_msg("%s", db_strerror(ua->db));
540       Dmsg0(050, "Count failed\n");
541       goto bail_out;
542    }
543
544    if (cnt.count == 0) {
545       ua->warning_msg(_("There are no more Jobs associated with Volume \"%s\". Marking it purged.\n"),
546          mr->VolumeName);
547       if (!(purged = mark_media_purged(ua, mr))) {
548          ua->error_msg("%s", db_strerror(ua->db));
549       }
550    }
551 bail_out:
552    return purged;
553 }
554
555 /*
556  * IF volume status is Append, Full, Used, or Error, mark it Purged
557  *   Purged volumes can then be recycled (if enabled).
558  */
559 bool mark_media_purged(UAContext *ua, MEDIA_DBR *mr)
560 {
561    JCR *jcr = ua->jcr;
562    if (strcmp(mr->VolStatus, "Append") == 0 ||
563        strcmp(mr->VolStatus, "Full")   == 0 ||
564        strcmp(mr->VolStatus, "Used")   == 0 ||
565        strcmp(mr->VolStatus, "Error")  == 0) {
566       bstrncpy(mr->VolStatus, "Purged", sizeof(mr->VolStatus));
567       if (!db_update_media_record(jcr, ua->db, mr)) {
568          return false;
569       }
570       pm_strcpy(jcr->VolumeName, mr->VolumeName);
571       generate_job_event(jcr, "VolumePurged");
572       generate_plugin_event(jcr, bEventVolumePurged);
573       /*
574        * If the RecyclePool is defined, move the volume there
575        */
576       if (mr->RecyclePoolId && mr->RecyclePoolId != mr->PoolId) {
577          POOL_DBR oldpr, newpr;
578          memset(&oldpr, 0, sizeof(POOL_DBR));
579          memset(&newpr, 0, sizeof(POOL_DBR));
580          newpr.PoolId = mr->RecyclePoolId;
581          oldpr.PoolId = mr->PoolId;
582          if (   db_get_pool_record(jcr, ua->db, &oldpr) 
583              && db_get_pool_record(jcr, ua->db, &newpr)) 
584          {
585             /* check if destination pool size is ok */
586             if (newpr.MaxVols > 0 && newpr.NumVols >= newpr.MaxVols) {
587                ua->error_msg(_("Unable move recycled Volume in full " 
588                               "Pool \"%s\" MaxVols=%d\n"),
589                         newpr.Name, newpr.MaxVols);
590
591             } else {            /* move media */
592                update_vol_pool(ua, newpr.Name, mr, &oldpr);
593             }
594          } else {
595             ua->error_msg("%s", db_strerror(ua->db));
596          }
597       }
598       /* Send message to Job report, if it is a *real* job */           
599       if (jcr && jcr->JobId > 0) {
600          Jmsg(jcr, M_INFO, 0, _("All records pruned from Volume \"%s\"; marking it \"Purged\"\n"),
601             mr->VolumeName); 
602       }
603       return true;
604    } else {
605       ua->error_msg(_("Cannot purge Volume with VolStatus=%s\n"), mr->VolStatus);
606    }
607    return strcmp(mr->VolStatus, "Purged") == 0;
608 }