1 /*
2    Bacula(R) - The Network Backup Solution
3 
4    Copyright (C) 2000-2020 Kern Sibbald
5 
6    The original author of Bacula is Kern Sibbald, with contributions
7    from many others, a complete list can be found in the file AUTHORS.
8 
9    You may use this file and others of this release according to the
10    license defined in the LICENSE file, which includes the Affero General
11    Public License, v3.0 ("AGPLv3") and some additional permissions and
12    terms pursuant to its AGPLv3 Section 7.
13 
14    This notice must be preserved when any source code is
15    conveyed and/or propagated.
16 
17    Bacula(R) is a registered trademark of Kern Sibbald.
18 */
19 /*
20  *
21  *   Bacula Director -- User Agent Database Purge Command
22  *
23  *      Purges Files from specific JobIds
24  * or
25  *      Purges Jobs from Volumes
26  *
27  *     Kern Sibbald, February MMII
28  *
29  */
30 
31 #include "bacula.h"
32 #include "dird.h"
33 
34 /* Forward referenced functions */
35 static int purge_files_from_client(UAContext *ua, CLIENT *client);
36 static int purge_jobs_from_client(UAContext *ua, CLIENT *client);
37 int truncate_cmd(UAContext *ua, const char *cmd);
38 
39 static const char *select_jobsfiles_from_client =
40    "SELECT JobId FROM Job "
41    "WHERE ClientId=%s "
42    "AND PurgedFiles=0";
43 
44 static const char *select_jobs_from_client =
45    "SELECT JobId, PurgedFiles FROM Job "
46    "WHERE ClientId=%s";
47 
48 /*
49  *   Purge records from database
50  *
51  *     Purge Files (from) [Job|JobId|Client|Volume]
52  *     Purge Jobs  (from) [Client|Volume]
53  *     Purge Volumes
54  *
55  *  N.B. Not all above is implemented yet.
56  */
purge_cmd(UAContext * ua,const char * cmd)57 int purge_cmd(UAContext *ua, const char *cmd)
58 {
59    int i;
60    CLIENT *client;
61    MEDIA_DBR mr;
62    JOB_DBR  jr;
63    memset(&jr, 0, sizeof(jr));
64 
65    static const char *keywords[] = {
66       NT_("files"),
67       NT_("jobs"),
68       NT_("volume"),
69       NULL};
70 
71    static const char *files_keywords[] = {
72       NT_("Job"),
73       NT_("JobId"),
74       NT_("Client"),
75       NT_("Volume"),
76       NULL};
77 
78    static const char *jobs_keywords[] = {
79       NT_("Client"),
80       NT_("Volume"),
81       NULL};
82 
83    /* Special case for the "Action On Purge", this option is working only on
84     * Purged volume, so no jobs or files will be purged.
85     * We are skipping this message if "purge volume action=xxx"
86     */
87    if (!(find_arg(ua, "volume") >= 0 && find_arg(ua, "action") >= 0)) {
88       ua->warning_msg(_(
89         "\nThis command can be DANGEROUS!!!\n\n"
90         "It purges (deletes) all Files from a Job,\n"
91         "JobId, Client or Volume; or it purges (deletes)\n"
92         "all Jobs from a Client or Volume without regard\n"
93         "to retention periods. Normally you should use the\n"
94         "PRUNE command, which respects retention periods.\n"));
95    }
96 
97    if (!open_new_client_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          /* We restrict the client list to ClientAcl, maybe something to change later */
114          client = get_client_resource(ua, JT_SYSTEM);
115          if (client) {
116             purge_files_from_client(ua, client);
117          }
118          return 1;
119       case 3:                         /* Volume */
120          if (select_media_dbr(ua, &mr)) {
121             purge_files_from_volume(ua, &mr);
122          }
123          return 1;
124       }
125    /* Jobs */
126    case 1:
127       switch(find_arg_keyword(ua, jobs_keywords)) {
128       case 0:                         /* client */
129          /* We restrict the client list to ClientAcl, maybe something to change later */
130          client = get_client_resource(ua, JT_SYSTEM);
131          if (client) {
132             purge_jobs_from_client(ua, client);
133          }
134          return 1;
135       case 1:                         /* Volume */
136          if (select_media_dbr(ua, &mr)) {
137             purge_jobs_from_volume(ua, &mr, /*force*/true);
138          }
139          return 1;
140       }
141    /* Volume */
142    case 2:
143       /* Perform ActionOnPurge (action=truncate) */
144       if (find_arg(ua, "action") >= 0) {
145          return truncate_cmd(ua, ua->cmd);
146       }
147 
148       while ((i=find_arg(ua, NT_("volume"))) >= 0) {
149          if (select_media_dbr(ua, &mr)) {
150             purge_jobs_from_volume(ua, &mr, /*force*/true);
151          }
152          *ua->argk[i] = 0;            /* zap keyword already seen */
153          ua->send_msg("\n");
154       }
155       return 1;
156    default:
157       break;
158    }
159    switch (do_keyword_prompt(ua, _("Choose item to purge"), keywords)) {
160    case 0:                            /* files */
161       /* We restrict the client list to ClientAcl, maybe something to change later */
162       client = get_client_resource(ua, JT_SYSTEM);
163       if (client) {
164          purge_files_from_client(ua, client);
165       }
166       break;
167    case 1:                            /* jobs */
168       /* We restrict the client list to ClientAcl, maybe something to change later */
169       client = get_client_resource(ua, JT_SYSTEM);
170       if (client) {
171          purge_jobs_from_client(ua, client);
172       }
173       break;
174    case 2:                            /* Volume */
175       if (select_media_dbr(ua, &mr)) {
176          purge_jobs_from_volume(ua, &mr, /*force*/true);
177       }
178       break;
179    }
180    return 1;
181 }
182 
183 /*
184  * Purge File records from the database. For any Job which
185  * is older than the retention period, we unconditionally delete
186  * all File records for that Job.  This is simple enough that no
187  * temporary tables are needed. We simply make an in memory list of
188  * the JobIds meeting the prune conditions, then delete all File records
189  * pointing to each of those JobIds.
190  */
purge_files_from_client(UAContext * ua,CLIENT * client)191 static int purge_files_from_client(UAContext *ua, CLIENT *client)
192 {
193    struct del_ctx del;
194    POOL_MEM query(PM_MESSAGE);
195    CLIENT_DBR cr;
196    char ed1[50];
197 
198    memset(&cr, 0, sizeof(cr));
199    bstrncpy(cr.Name, client->name(), sizeof(cr.Name));
200    if (!db_create_client_record(ua->jcr, ua->db, &cr)) {
201       return 0;
202    }
203 
204    memset(&del, 0, sizeof(del));
205    del.max_ids = 1000;
206    del.JobId = (JobId_t *)malloc(sizeof(JobId_t) * del.max_ids);
207 
208    /* Keep track of this important event */
209    ua->send_events("DC0001", EVENTS_TYPE_COMMAND, "purge files client=%s", cr.Name);
210    ua->info_msg(_("Begin purging files for Client \"%s\"\n"), cr.Name);
211 
212    Mmsg(query, select_jobsfiles_from_client, edit_int64(cr.ClientId, ed1));
213    Dmsg1(050, "select sql=%s\n", query.c_str());
214    db_sql_query(ua->db, query.c_str(), file_delete_handler, (void *)&del);
215 
216    purge_files_from_job_list(ua, del);
217 
218    if (del.num_del == 0) {
219       ua->warning_msg(_("No Files found for client %s to purge from %s catalog.\n"),
220          client->name(), client->catalog->name());
221    } else {
222       ua->info_msg(_("Files for %d Jobs for client \"%s\" purged from %s catalog.\n"), del.num_del,
223          client->name(), client->catalog->name());
224    }
225 
226    if (del.JobId) {
227       free(del.JobId);
228    }
229    return 1;
230 }
231 
232 
233 
234 /*
235  * Purge Job records from the database. For any Job which
236  * is older than the retention period, we unconditionally delete
237  * it and all File records for that Job.  This is simple enough that no
238  * temporary tables are needed. We simply make an in memory list of
239  * the JobIds then delete the Job, Files, and JobMedia records in that list.
240  */
purge_jobs_from_client(UAContext * ua,CLIENT * client)241 static int purge_jobs_from_client(UAContext *ua, CLIENT *client)
242 {
243    struct del_ctx del;
244    POOL_MEM query(PM_MESSAGE);
245    CLIENT_DBR cr;
246    char ed1[50];
247 
248    memset(&cr, 0, sizeof(cr));
249 
250    bstrncpy(cr.Name, client->name(), sizeof(cr.Name));
251    if (!db_create_client_record(ua->jcr, ua->db, &cr)) {
252       return 0;
253    }
254 
255    memset(&del, 0, sizeof(del));
256    del.max_ids = 1000;
257    del.JobId = (JobId_t *)malloc(sizeof(JobId_t) * del.max_ids);
258    del.PurgedFiles = (char *)malloc(del.max_ids);
259 
260    ua->info_msg(_("Begin purging jobs from Client \"%s\"\n"), cr.Name);
261 
262    Mmsg(query, select_jobs_from_client, edit_int64(cr.ClientId, ed1));
263    Dmsg1(150, "select sql=%s\n", query.c_str());
264    db_sql_query(ua->db, query.c_str(), job_delete_handler, (void *)&del);
265 
266    purge_job_list_from_catalog(ua, del);
267 
268    if (del.num_del == 0) {
269       ua->warning_msg(_("No Jobs found for client %s to purge from %s catalog.\n"),
270          client->name(), client->catalog->name());
271    } else {
272       ua->info_msg(_("%d Jobs for client %s purged from %s catalog.\n"), del.num_del,
273          client->name(), client->catalog->name());
274    }
275 
276    if (del.JobId) {
277       free(del.JobId);
278    }
279    if (del.PurgedFiles) {
280       free(del.PurgedFiles);
281    }
282    return 1;
283 }
284 
285 
286 /*
287  * Remove File records from a list of JobIds
288  */
purge_files_from_jobs(UAContext * ua,char * jobs)289 void purge_files_from_jobs(UAContext *ua, char *jobs)
290 {
291    POOL_MEM query(PM_MESSAGE);
292 
293    Mmsg(query, "DELETE FROM File WHERE JobId IN (%s)", jobs);
294    db_sql_query(ua->db, query.c_str(), NULL, (void *)NULL);
295    Dmsg1(050, "Delete File sql=%s\n", query.c_str());
296 
297    Mmsg(query, "DELETE FROM FileMedia WHERE JobId IN (%s)", jobs);
298    db_sql_query(ua->db, query.c_str(), NULL, (void *)NULL);
299    Dmsg1(050, "Delete FileMedia sql=%s\n", query.c_str());
300 
301    Mmsg(query, "DELETE FROM BaseFiles WHERE JobId IN (%s)", jobs);
302    db_sql_query(ua->db, query.c_str(), NULL, (void *)NULL);
303    Dmsg1(050, "Delete BaseFiles sql=%s\n", query.c_str());
304 
305    Mmsg(query, "DELETE FROM PathVisibility WHERE JobId IN (%s)", jobs);
306    db_sql_query(ua->db, query.c_str(), NULL, (void *)NULL);
307    Dmsg1(050, "Delete PathVisibility sql=%s\n", query.c_str());
308 
309    /*
310     * Now mark Job as having files purged. This is necessary to
311     * avoid having too many Jobs to process in future prunings. If
312     * we don't do this, the number of JobId's in our in memory list
313     * could grow very large.
314     */
315    Mmsg(query, "UPDATE Job SET PurgedFiles=1 WHERE JobId IN (%s)", jobs);
316    db_sql_query(ua->db, query.c_str(), NULL, (void *)NULL);
317    Dmsg1(050, "Mark purged sql=%s\n", query.c_str());
318 }
319 
320 /*
321  * Delete jobs (all records) from the catalog in groups of 1000
322  *  at a time.
323  */
purge_job_list_from_catalog(UAContext * ua,del_ctx & del)324 void purge_job_list_from_catalog(UAContext *ua, del_ctx &del)
325 {
326    POOL_MEM jobids(PM_MESSAGE);
327    char ed1[50];
328 
329    exclude_running_jobs_from_list(&del);
330 
331    for (int i=0; del.num_ids; ) {
332       Dmsg1(150, "num_ids=%d\n", del.num_ids);
333       pm_strcat(jobids, "");
334       for (int j=0; j<1000 && del.num_ids>0; j++) {
335          del.num_ids--;
336          if (del.JobId[i] == 0 || ua->jcr->JobId == del.JobId[i]) {
337             Dmsg2(150, "skip JobId[%d]=%d\n", i, (int)del.JobId[i]);
338             i++;
339             continue;
340          }
341          if (*jobids.c_str() != 0) {
342             pm_strcat(jobids, ",");
343          }
344          pm_strcat(jobids, edit_int64(del.JobId[i++], ed1));
345          Dmsg1(150, "Add id=%s\n", ed1);
346          del.num_del++;
347       }
348       Dmsg1(150, "num_ids=%d\n", del.num_ids);
349       purge_jobs_from_catalog(ua, jobids.c_str());
350    }
351 }
352 
353 /*
354  * Delete files from a list of jobs in groups of 1000
355  *  at a time.
356  */
purge_files_from_job_list(UAContext * ua,del_ctx & del)357 void purge_files_from_job_list(UAContext *ua, del_ctx &del)
358 {
359    POOL_MEM jobids(PM_MESSAGE);
360    char ed1[50];
361    /*
362     * OK, now we have the list of JobId's to be pruned, send them
363     *   off to be deleted batched 1000 at a time.
364     */
365    for (int i=0; del.num_ids; ) {
366       pm_strcat(jobids, "");
367       for (int j=0; j<1000 && del.num_ids>0; j++) {
368          del.num_ids--;
369          if (del.JobId[i] == 0 || ua->jcr->JobId == del.JobId[i]) {
370             Dmsg2(150, "skip JobId[%d]=%d\n", i, (int)del.JobId[i]);
371             i++;
372             continue;
373          }
374          if (*jobids.c_str() != 0) {
375             pm_strcat(jobids, ",");
376          }
377          pm_strcat(jobids, edit_int64(del.JobId[i++], ed1));
378          Dmsg1(150, "Add id=%s\n", ed1);
379          del.num_del++;
380       }
381       purge_files_from_jobs(ua, jobids.c_str());
382    }
383 }
384 
385 /*
386  * Change the type of the next copy job to backup.
387  * We need to upgrade the next copy of a normal job,
388  * and also upgrade the next copy when the normal job
389  * already have been purged.
390  *
391  *   JobId: 1   PriorJobId: 0    (original)
392  *   JobId: 2   PriorJobId: 1    (first copy)
393  *   JobId: 3   PriorJobId: 1    (second copy)
394  *
395  *   JobId: 2   PriorJobId: 1    (first copy, now regular backup)
396  *   JobId: 3   PriorJobId: 1    (second copy)
397  *
398  *  => Search through PriorJobId in jobid and
399  *                    PriorJobId in PriorJobId (jobid)
400  */
upgrade_copies(UAContext * ua,char * jobs)401 void upgrade_copies(UAContext *ua, char *jobs)
402 {
403    POOL_MEM query(PM_MESSAGE);
404    int dbtype = ua->db->bdb_get_type_index();
405 
406    db_lock(ua->db);
407 
408    Mmsg(query, uap_upgrade_copies_oldest_job[dbtype], JT_JOB_COPY, jobs, jobs);
409    db_sql_query(ua->db, query.c_str(), NULL, (void *)NULL);
410    Dmsg1(050, "Upgrade copies Log sql=%s\n", query.c_str());
411 
412    /* Now upgrade first copy to Backup */
413    Mmsg(query, "UPDATE Job SET Type='B' "      /* JT_JOB_COPY => JT_BACKUP  */
414                 "WHERE JobId IN ( SELECT JobId FROM cpy_tmp )");
415 
416    db_sql_query(ua->db, query.c_str(), NULL, (void *)NULL);
417 
418    Mmsg(query, "DROP TABLE cpy_tmp");
419    db_sql_query(ua->db, query.c_str(), NULL, (void *)NULL);
420 
421    db_unlock(ua->db);
422 }
423 
424 /*
425  * Remove all records from catalog for a list of JobIds
426  */
purge_jobs_from_catalog(UAContext * ua,char * jobs)427 void purge_jobs_from_catalog(UAContext *ua, char *jobs)
428 {
429    POOL_MEM query(PM_MESSAGE);
430    /* Keep track of this important event */
431    ua->send_events("DC0002", EVENTS_TYPE_COMMAND, "purge jobid=%s", jobs);
432 
433    /* Delete (or purge) records associated with the job */
434    purge_files_from_jobs(ua, jobs);
435 
436    Mmsg(query, "DELETE FROM JobMedia WHERE JobId IN (%s)", jobs);
437    db_sql_query(ua->db, query.c_str(), NULL, (void *)NULL);
438    Dmsg1(050, "Delete JobMedia sql=%s\n", query.c_str());
439 
440    Mmsg(query, "DELETE FROM FileMedia WHERE JobId IN (%s)", jobs);
441    db_sql_query(ua->db, query.c_str(), NULL, (void *)NULL);
442    Dmsg1(050, "Delete JobMedia sql=%s\n", query.c_str());
443 
444    Mmsg(query, "DELETE FROM Log WHERE JobId IN (%s)", jobs);
445    db_sql_query(ua->db, query.c_str(), NULL, (void *)NULL);
446    Dmsg1(050, "Delete Log sql=%s\n", query.c_str());
447 
448    Mmsg(query, "DELETE FROM RestoreObject WHERE JobId IN (%s)", jobs);
449    db_sql_query(ua->db, query.c_str(), NULL, (void *)NULL);
450    Dmsg1(050, "Delete RestoreObject sql=%s\n", query.c_str());
451 
452    /* The JobId of the Snapshot record is no longer usable
453     * TODO: Migth want to use a copy for the jobid?
454     */
455    Mmsg(query, "UPDATE Snapshot SET JobId=0 WHERE JobId IN (%s)", jobs);
456    db_sql_query(ua->db, query.c_str(), NULL, (void *)NULL);
457 
458    upgrade_copies(ua, jobs);
459    /* Now remove the Job record itself */
460    Mmsg(query, "DELETE FROM Job WHERE JobId IN (%s)", jobs);
461    db_sql_query(ua->db, query.c_str(), NULL, (void *)NULL);
462 
463    Dmsg1(050, "Delete Job sql=%s\n", query.c_str());
464 }
465 
purge_files_from_volume(UAContext * ua,MEDIA_DBR * mr)466 void purge_files_from_volume(UAContext *ua, MEDIA_DBR *mr )
467 {} /* ***FIXME*** implement */
468 
469 /*
470  * Returns: 1 if Volume purged
471  *          0 if Volume not purged
472  */
purge_jobs_from_volume(UAContext * ua,MEDIA_DBR * mr,bool force)473 bool purge_jobs_from_volume(UAContext *ua, MEDIA_DBR *mr, bool force)
474 {
475    POOL_MEM query(PM_MESSAGE);
476    db_list_ctx lst_all, lst;
477    char *jobids=NULL;
478    int i;
479    bool purged = false;
480    bool stat;
481 
482    stat = strcmp(mr->VolStatus, "Append") == 0 ||
483           strcmp(mr->VolStatus, "Full")   == 0 ||
484           strcmp(mr->VolStatus, "Used")   == 0 ||
485           strcmp(mr->VolStatus, "Error")  == 0;
486    if (!stat) {
487       ua->error_msg(_("\nVolume \"%s\" has VolStatus \"%s\" and cannot be purged.\n"
488                      "The VolStatus must be: Append, Full, Used, or Error to be purged.\n"),
489                      mr->VolumeName, mr->VolStatus);
490       return 0;
491    }
492 
493    /*
494     * Check if he wants to purge a single jobid
495     */
496    i = find_arg_with_value(ua, "jobid");
497    if (i >= 0 && is_a_number_list(ua->argv[i])) {
498       jobids = ua->argv[i];
499 
500    } else {
501       POOL_MEM query;
502       /*
503        * Purge ALL JobIds
504        */
505       if (!db_get_volume_jobids(ua->jcr, ua->db, mr, &lst_all)) {
506          ua->error_msg("%s", db_strerror(ua->db));
507          Dmsg0(050, "Count failed\n");
508          goto bail_out;
509       }
510 
511       if (lst_all.count > 0) {
512          Mmsg(query, "SELECT JobId FROM Job WHERE JobId IN (%s) AND JobStatus NOT IN ('R', 'C')",
513               lst_all.list);
514          if (!db_sql_query(ua->db, query.c_str(), db_list_handler, &lst)) {
515             ua->error_msg("%s", db_strerror(ua->db));
516             goto bail_out;
517          }
518       }
519       jobids = lst.list;
520    }
521 
522    if (*jobids) {
523       /* Keep track of this important event */
524       ua->send_events("DC0003", EVENTS_TYPE_COMMAND, "purge volume=%s", mr->VolumeName);
525 
526       purge_jobs_from_catalog(ua, jobids);
527       ua->info_msg(_("%d Job%s on Volume \"%s\" purged from catalog.\n"),
528                    lst.count, lst.count<=1?"":"s", mr->VolumeName);
529    }
530    purged = is_volume_purged(ua, mr, force);
531 
532 bail_out:
533    return purged;
534 }
535 
536 /*
537  * This routine will check the JobMedia records to see if the
538  *   Volume has been purged. If so, it marks it as such and
539  *
540  * Returns: true if volume purged
541  *          false if not
542  *
543  * Note, we normally will not purge a volume that has Firstor LastWritten
544  *   zero, because it means the volume is most likely being written
545  *   however, if the user manually purges using the purge command in
546  *   the console, he has been warned, and we go ahead and purge
547  *   the volume anyway, if possible).
548  */
is_volume_purged(UAContext * ua,MEDIA_DBR * mr,bool force)549 bool is_volume_purged(UAContext *ua, MEDIA_DBR *mr, bool force)
550 {
551    POOL_MEM query(PM_MESSAGE);
552    struct s_count_ctx cnt;
553    bool purged = false;
554    char ed1[50];
555 
556    if (!force && (mr->FirstWritten == 0 || mr->LastWritten == 0)) {
557       goto bail_out;               /* not written cannot purge */
558    }
559 
560    if (strcmp(mr->VolStatus, "Purged") == 0) {
561       Dmsg1(100, "Volume=%s already purged.\n", mr->VolumeName);
562       purged = true;
563       goto bail_out;
564    }
565 
566    /* If purged, mark it so */
567    cnt.count = 0;
568    Mmsg(query, "SELECT 1 FROM JobMedia WHERE MediaId=%s LIMIT 1",
569         edit_int64(mr->MediaId, ed1));
570    if (!db_sql_query(ua->db, query.c_str(), del_count_handler, (void *)&cnt)) {
571       ua->error_msg("%s", db_strerror(ua->db));
572       Dmsg0(050, "Count failed\n");
573       goto bail_out;
574    }
575 
576    if (cnt.count == 0) {
577       ua->warning_msg(_("There are no more Jobs associated with Volume \"%s\". Marking it purged.\n"),
578          mr->VolumeName);
579       Dmsg1(100, "There are no more Jobs associated with Volume \"%s\". Marking it purged.\n",
580          mr->VolumeName);
581       if (!(purged = mark_media_purged(ua, mr))) {
582          ua->error_msg("%s", db_strerror(ua->db));
583       }
584    }
585 bail_out:
586    return purged;
587 }
588 
589 /*
590  * Called here to send the appropriate commands to the SD
591  *  to do truncate on purge.
592  */
truncate_volume(UAContext * ua,MEDIA_DBR * mr,char * pool,char * storage,int drive,BSOCK * sd)593 static void truncate_volume(UAContext *ua, MEDIA_DBR *mr,
594                             char *pool, char *storage,
595                             int drive, BSOCK *sd)
596 {
597    bool ok = false;
598    uint64_t VolBytes = 0;
599    uint64_t VolABytes = 0;
600    uint32_t VolType = 0;
601 
602    if (!mr->Recycle) {
603       return;
604    }
605 
606    /* Do it only if action on purge = truncate is set */
607    if (!(mr->ActionOnPurge & ON_PURGE_TRUNCATE)) {
608       ua->error_msg(_("\nThe option \"Action On Purge = Truncate\" was not defined in the Pool resource.\n"
609                       "Truncate not allowed on Volume \"%s\"\n"), mr->VolumeName);
610       return;
611    }
612 
613    /*
614     * Send the command to truncate the volume after purge. If this feature
615     * is disabled for the specific device, this will be a no-op.
616     */
617 
618    /* Protect us from spaces */
619    bash_spaces(mr->VolumeName);
620    bash_spaces(mr->MediaType);
621    bash_spaces(pool);
622    bash_spaces(storage);
623 
624    /* Do it by relabeling the Volume, which truncates it */
625    sd->fsend("relabel %s OldName=%s NewName=%s PoolName=%s "
626              "MediaType=%s Slot=%d drive=%d\n",
627              storage,
628              mr->VolumeName, mr->VolumeName,
629              pool, mr->MediaType, mr->Slot, drive);
630 
631    unbash_spaces(mr->VolumeName);
632    unbash_spaces(mr->MediaType);
633    unbash_spaces(pool);
634    unbash_spaces(storage);
635 
636    /* Check for valid response. With cloud volumes, the upload of the part.1 can
637     * generate a dir_update_volume_info() message that is handled by bget_dirmsg()
638     */
639    while (bget_dirmsg(sd) >= 0) {
640       ua->send_msg("%s", sd->msg);
641       if (sscanf(sd->msg, "3000 OK label. VolBytes=%llu VolABytes=%lld VolType=%d ",
642                  &VolBytes, &VolABytes, &VolType) == 3) {
643 
644          ok = true;
645          /* Clean up a few things in the media record */
646          mr->VolBytes = VolBytes;
647          mr->VolABytes = VolABytes;
648          mr->VolType = VolType;
649          mr->VolFiles = 0;
650          mr->VolParts = 1;
651          mr->VolCloudParts = 0;
652          mr->LastPartBytes = VolBytes;
653          mr->VolJobs = 0;
654          mr->VolBlocks = 1;
655          mr->VolHoleBytes = 0;
656          mr->VolHoles = 0;
657          mr->EndBlock = 1;
658 
659          set_storageid_in_mr(NULL, mr);
660          if (!db_update_media_record(ua->jcr, ua->db, mr)) {
661             ua->error_msg(_("Can't update volume size in the catalog for Volume \"%s\"\n"),
662                mr->VolumeName);
663             ok = false;
664          }
665          /* Keep track of this important event */
666          ua->send_events("DC0004", EVENTS_TYPE_COMMAND, "truncate volume=%s", mr->VolumeName);
667          ua->send_msg(_("The volume \"%s\" has been truncated\n"), mr->VolumeName);
668       }
669    }
670    if (!ok) {
671       ua->warning_msg(_("Error truncating Volume \"%s\"\n"), mr->VolumeName);
672    }
673 }
674 
675 /*
676  * Implement Bacula bconsole command  purge action
677  *     purge action=truncate pool= volume= storage= mediatype=
678  * or
679  *     truncate [cache] pool= volume= storage= mediatype=
680  *
681  * If the keyword "cache:  is present, then we use the truncate
682  *   command rather than relabel so that the driver can decide
683  *   whether or not it wants to truncate.  Note: only the
684  *   Cloud driver permits truncating the cache.
685  *
686  * Note, later we might want to rename this action_on_purge_cmd() as
687  *  was the original, but only if we add additional actions such as
688  *  erase, ... For the moment, we only do a truncate.
689  *
690  */
truncate_cmd(UAContext * ua,const char * cmd)691 int truncate_cmd(UAContext *ua, const char *cmd)
692 {
693    int drive = -1;
694    int nb = 0;
695    uint32_t *results = NULL;
696    const char *action = "truncate";
697    MEDIA_DBR mr;
698    POOL_DBR pr;
699    BSOCK *sd;
700    char storage[MAX_NAME_LENGTH];
701 
702    if (find_arg(ua, "cache") > 0) {
703       return cloud_volumes_cmd(ua, cmd, "truncate cache");
704    }
705 
706    bmemset(&pr, 0, sizeof(pr));
707 
708    /*
709     * Look for all Purged volumes that can be recycled, are enabled and
710     *  have more than 1,000 bytes (i.e. actually have data).
711     */
712    mr.Recycle = 1;
713    mr.Enabled = 1;
714    mr.VolBytes = 1000;
715    bstrncpy(mr.VolStatus, "Purged", sizeof(mr.VolStatus));
716    /* Get list of volumes to truncate */
717    if (!scan_storage_cmd(ua, cmd, true, /* allfrompool */
718                          &drive, &mr, &pr, &action, storage, &nb, &results)) {
719       goto bail_out;
720    }
721 
722    if ((sd=open_sd_bsock(ua)) == NULL) {
723       Dmsg0(100, "Can't open connection to sd\n");
724       goto bail_out;
725    }
726 
727    /*
728     * Loop over the candidate Volumes and actually truncate them
729     */
730    for (int i=0; i < nb; i++) {
731       mr.clear();
732       mr.MediaId = results[i];
733       if (db_get_media_record(ua->jcr, ua->db, &mr)) {
734          if (strcasecmp(mr.VolStatus, "Purged") != 0) {
735             ua->send_msg(_("Truncate Volume \"%s\" skipped. Status is \"%s\", but must be \"Purged\".\n"),
736                mr.VolumeName, mr.VolStatus);
737             continue;
738          }
739          if (drive < 0) {
740             STORE *store = (STORE*)GetResWithName(R_STORAGE, storage);
741             drive = get_storage_drive(ua, store);
742          }
743 
744          /* Must select Pool if not already done */
745          if (pr.PoolId == 0) {
746             pr.PoolId = mr.PoolId;
747             if (!db_get_pool_record(ua->jcr, ua->db, &pr)) {
748                goto bail_out;   /* free allocated memory */
749             }
750          }
751          if (strcasecmp("truncate", action) == 0) {
752             truncate_volume(ua, &mr, pr.Name, storage,
753                             drive, sd);
754          }
755       } else {
756          Dmsg1(0, "Can't find MediaId=%lu\n", mr.MediaId);
757       }
758    }
759 
760 bail_out:
761    close_db(ua);
762    close_sd_bsock(ua);
763    ua->jcr->wstore = NULL;
764    if (results) {
765       free(results);
766    }
767 
768    return 1;
769 }
770 
771 /*
772  * IF volume status is Append, Full, Used, or Error, mark it Purged
773  *   Purged volumes can then be recycled (if enabled).
774  */
mark_media_purged(UAContext * ua,MEDIA_DBR * mr)775 bool mark_media_purged(UAContext *ua, MEDIA_DBR *mr)
776 {
777    JCR *jcr = ua->jcr;
778    if (strcmp(mr->VolStatus, "Append") == 0 ||
779        strcmp(mr->VolStatus, "Full")   == 0 ||
780        strcmp(mr->VolStatus, "Used")   == 0 ||
781        strcmp(mr->VolStatus, "Error")  == 0) {
782       bstrncpy(mr->VolStatus, "Purged", sizeof(mr->VolStatus));
783       set_storageid_in_mr(NULL, mr);
784       if (!db_update_media_record(jcr, ua->db, mr)) {
785          return false;
786       }
787       pm_strcpy(jcr->VolumeName, mr->VolumeName);
788       generate_plugin_event(jcr, bDirEventVolumePurged);
789       /*
790        * If the RecyclePool is defined, move the volume there
791        */
792       if (mr->RecyclePoolId && mr->RecyclePoolId != mr->PoolId) {
793          POOL_DBR oldpr, newpr;
794          bmemset(&oldpr, 0, sizeof(POOL_DBR));
795          bmemset(&newpr, 0, sizeof(POOL_DBR));
796          newpr.PoolId = mr->RecyclePoolId;
797          oldpr.PoolId = mr->PoolId;
798          if (   db_get_pool_numvols(jcr, ua->db, &oldpr)
799              && db_get_pool_numvols(jcr, ua->db, &newpr)) {
800             /* check if destination pool size is ok */
801             if (newpr.MaxVols > 0 && newpr.NumVols >= newpr.MaxVols) {
802                ua->error_msg(_("Unable move recycled Volume in full "
803                               "Pool \"%s\" MaxVols=%d\n"),
804                         newpr.Name, newpr.MaxVols);
805 
806             } else {            /* move media */
807                update_vol_pool(ua, newpr.Name, mr, &oldpr);
808             }
809          } else {
810             ua->error_msg("%s", db_strerror(ua->db));
811          }
812       }
813 
814       /* Send message to Job report, if it is a *real* job */
815       if (jcr && jcr->JobId > 0) {
816          Jmsg(jcr, M_INFO, 0, _("All records pruned from Volume \"%s\"; marking it \"Purged\"\n"),
817             mr->VolumeName);
818       }
819       return true;
820    } else {
821       ua->error_msg(_("Cannot purge Volume with VolStatus=%s\n"), mr->VolStatus);
822    }
823    return strcmp(mr->VolStatus, "Purged") == 0;
824 }
825