]> git.sur5r.net Git - bacula/bacula/blob - bacula/src/dird/ua_purge.c
kes Since the user has been warned, allow console purge command
[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  * Remove all records from catalog for a list of JobIds
364  */
365 void purge_jobs_from_catalog(UAContext *ua, char *jobs)
366 {
367    POOL_MEM query(PM_MESSAGE);
368
369    /* Delete (or purge) records associated with the job */
370    purge_files_from_jobs(ua, jobs);
371
372    Mmsg(query, "DELETE FROM JobMedia WHERE JobId IN (%s)", jobs);
373    db_sql_query(ua->db, query.c_str(), NULL, (void *)NULL);
374    Dmsg1(050, "Delete JobMedia sql=%s\n", query.c_str());
375
376    Mmsg(query, "DELETE FROM Log WHERE JobId IN (%s)", jobs);
377    db_sql_query(ua->db, query.c_str(), NULL, (void *)NULL);
378    Dmsg1(050, "Delete Log sql=%s\n", query.c_str());
379
380    /* Now remove the Job record itself */
381    Mmsg(query, "DELETE FROM Job WHERE JobId IN (%s)", jobs);
382    db_sql_query(ua->db, query.c_str(), NULL, (void *)NULL);
383    Dmsg1(050, "Delete Job sql=%s\n", query.c_str());
384 }
385
386
387 void purge_files_from_volume(UAContext *ua, MEDIA_DBR *mr )
388 {} /* ***FIXME*** implement */
389
390 /*
391  * Returns: 1 if Volume purged
392  *          0 if Volume not purged
393  */
394 bool purge_jobs_from_volume(UAContext *ua, MEDIA_DBR *mr, bool force)
395 {
396    POOL_MEM query(PM_MESSAGE);
397    struct del_ctx del;
398    int i;
399    bool purged = false;
400    bool stat;
401    JOB_DBR jr;
402    char ed1[50];
403
404    stat = strcmp(mr->VolStatus, "Append") == 0 ||
405           strcmp(mr->VolStatus, "Full")   == 0 ||
406           strcmp(mr->VolStatus, "Used")   == 0 ||
407           strcmp(mr->VolStatus, "Error")  == 0;
408    if (!stat) {
409       ua->error_msg(_("\nVolume \"%s\" has VolStatus \"%s\" and cannot be purged.\n"
410                      "The VolStatus must be: Append, Full, Used, or Error to be purged.\n"),
411                      mr->VolumeName, mr->VolStatus);
412       return 0;
413    }
414
415    memset(&jr, 0, sizeof(jr));
416    memset(&del, 0, sizeof(del));
417    del.max_ids = 1000;
418    del.JobId = (JobId_t *)malloc(sizeof(JobId_t) * del.max_ids);
419
420    /*
421     * Check if he wants to purge a single jobid
422     */
423    i = find_arg_with_value(ua, "jobid");
424    if (i >= 0) {
425       del.num_ids = 1;
426       del.JobId[0] = str_to_int64(ua->argv[i]);
427    } else {
428       /*
429        * Purge ALL JobIds
430        */
431       Mmsg(query, "SELECT DISTINCT JobId FROM JobMedia WHERE MediaId=%s", 
432            edit_int64(mr->MediaId, ed1));
433       if (!db_sql_query(ua->db, query.c_str(), file_delete_handler, (void *)&del)) {
434          ua->error_msg("%s", db_strerror(ua->db));
435          Dmsg0(050, "Count failed\n");
436          goto bail_out;
437       }
438    }
439
440    purge_job_list_from_catalog(ua, del);
441
442    ua->info_msg(_("%d File%s on Volume \"%s\" purged from catalog.\n"), del.num_del,
443       del.num_del==1?"":"s", mr->VolumeName);
444
445    purged = is_volume_purged(ua, mr, force); 
446
447 bail_out:
448    if (del.JobId) {
449       free(del.JobId);
450    }
451    return purged;
452 }
453
454 /*
455  * This routine will check the JobMedia records to see if the
456  *   Volume has been purged. If so, it marks it as such and
457  *
458  * Returns: true if volume purged
459  *          false if not
460  *
461  * Note, we normally will not purge a volume that has Firstor LastWritten
462  *   zero, because it means the volume is most likely being written
463  *   however, if the user manually purges using the purge command in
464  *   the console, he has been warned, and we go ahead and purge
465  *   the volume anyway, if possible).
466  */
467 bool is_volume_purged(UAContext *ua, MEDIA_DBR *mr, bool force)
468 {
469    POOL_MEM query(PM_MESSAGE);
470    struct s_count_ctx cnt;
471    bool purged = false;
472    char ed1[50];
473
474    if (!force && (mr->FirstWritten == 0 || mr->LastWritten == 0)) {
475       goto bail_out;               /* not written cannot purge */
476    }
477
478    if (strcmp(mr->VolStatus, "Purged") == 0) {
479       purged = true;
480       goto bail_out;
481    }
482
483    /* If purged, mark it so */
484    cnt.count = 0;
485    Mmsg(query, "SELECT count(*) FROM JobMedia WHERE MediaId=%s", 
486         edit_int64(mr->MediaId, ed1));
487    if (!db_sql_query(ua->db, query.c_str(), del_count_handler, (void *)&cnt)) {
488       ua->error_msg("%s", db_strerror(ua->db));
489       Dmsg0(050, "Count failed\n");
490       goto bail_out;
491    }
492
493    if (cnt.count == 0) {
494       ua->warning_msg(_("There are no more Jobs associated with Volume \"%s\". Marking it purged.\n"),
495          mr->VolumeName);
496       if (!(purged = mark_media_purged(ua, mr))) {
497          ua->error_msg("%s", db_strerror(ua->db));
498       }
499    }
500 bail_out:
501    return purged;
502 }
503
504 /*
505  * IF volume status is Append, Full, Used, or Error, mark it Purged
506  *   Purged volumes can then be recycled (if enabled).
507  */
508 bool mark_media_purged(UAContext *ua, MEDIA_DBR *mr)
509 {
510    JCR *jcr = ua->jcr;
511    if (strcmp(mr->VolStatus, "Append") == 0 ||
512        strcmp(mr->VolStatus, "Full")   == 0 ||
513        strcmp(mr->VolStatus, "Used")   == 0 ||
514        strcmp(mr->VolStatus, "Error")  == 0) {
515       bstrncpy(mr->VolStatus, "Purged", sizeof(mr->VolStatus));
516       if (!db_update_media_record(jcr, ua->db, mr)) {
517          return false;
518       }
519       pm_strcpy(jcr->VolumeName, mr->VolumeName);
520       generate_job_event(jcr, "VolumePurged");
521       generate_plugin_event(jcr, bEventVolumePurged);
522       /*
523        * If the RecyclePool is defined, move the volume there
524        */
525       if (mr->RecyclePoolId && mr->RecyclePoolId != mr->PoolId) {
526          POOL_DBR oldpr, newpr;
527          memset(&oldpr, 0, sizeof(POOL_DBR));
528          memset(&newpr, 0, sizeof(POOL_DBR));
529          newpr.PoolId = mr->RecyclePoolId;
530          oldpr.PoolId = mr->PoolId;
531          if (   db_get_pool_record(jcr, ua->db, &oldpr) 
532              && db_get_pool_record(jcr, ua->db, &newpr)) 
533          {
534             /* check if destination pool size is ok */
535             if (newpr.MaxVols > 0 && newpr.NumVols >= newpr.MaxVols) {
536                ua->error_msg(_("Unable move recycled Volume in full " 
537                               "Pool \"%s\" MaxVols=%d\n"),
538                         newpr.Name, newpr.MaxVols);
539
540             } else {            /* move media */
541                update_vol_pool(ua, newpr.Name, mr, &oldpr);
542             }
543          } else {
544             ua->error_msg("%s", db_strerror(ua->db));
545          }
546       }
547       /* Send message to Job report, if it is a *real* job */           
548       if (jcr && jcr->JobId > 0) {
549          Jmsg(jcr, M_INFO, 0, _("All records pruned from Volume \"%s\"; marking it \"Purged\"\n"),
550             mr->VolumeName); 
551       }
552       return true;
553    } else {
554       ua->error_msg(_("Cannot purge Volume with VolStatus=%s\n"), mr->VolStatus);
555    }
556    return strcmp(mr->VolStatus, "Purged") == 0;
557 }