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    ua->info_msg(_("Begin purging files for Client \"%s\"\n"), cr.Name);
209 
210    Mmsg(query, select_jobsfiles_from_client, edit_int64(cr.ClientId, ed1));
211    Dmsg1(050, "select sql=%s\n", query.c_str());
212    db_sql_query(ua->db, query.c_str(), file_delete_handler, (void *)&del);
213 
214    purge_files_from_job_list(ua, del);
215 
216    if (del.num_del == 0) {
217       ua->warning_msg(_("No Files found for client %s to purge from %s catalog.\n"),
218          client->name(), client->catalog->name());
219    } else {
220       ua->info_msg(_("Files for %d Jobs for client \"%s\" purged from %s catalog.\n"), del.num_del,
221          client->name(), client->catalog->name());
222    }
223 
224    if (del.JobId) {
225       free(del.JobId);
226    }
227    return 1;
228 }
229 
230 
231 
232 /*
233  * Purge Job records from the database. For any Job which
234  * is older than the retention period, we unconditionally delete
235  * it and all File records for that Job.  This is simple enough that no
236  * temporary tables are needed. We simply make an in memory list of
237  * the JobIds then delete the Job, Files, and JobMedia records in that list.
238  */
purge_jobs_from_client(UAContext * ua,CLIENT * client)239 static int purge_jobs_from_client(UAContext *ua, CLIENT *client)
240 {
241    struct del_ctx del;
242    POOL_MEM query(PM_MESSAGE);
243    CLIENT_DBR cr;
244    char ed1[50];
245 
246    memset(&cr, 0, sizeof(cr));
247 
248    bstrncpy(cr.Name, client->name(), sizeof(cr.Name));
249    if (!db_create_client_record(ua->jcr, ua->db, &cr)) {
250       return 0;
251    }
252 
253    memset(&del, 0, sizeof(del));
254    del.max_ids = 1000;
255    del.JobId = (JobId_t *)malloc(sizeof(JobId_t) * del.max_ids);
256    del.PurgedFiles = (char *)malloc(del.max_ids);
257 
258    ua->info_msg(_("Begin purging jobs from Client \"%s\"\n"), cr.Name);
259 
260    Mmsg(query, select_jobs_from_client, edit_int64(cr.ClientId, ed1));
261    Dmsg1(150, "select sql=%s\n", query.c_str());
262    db_sql_query(ua->db, query.c_str(), job_delete_handler, (void *)&del);
263 
264    purge_job_list_from_catalog(ua, del);
265 
266    if (del.num_del == 0) {
267       ua->warning_msg(_("No Jobs found for client %s to purge from %s catalog.\n"),
268          client->name(), client->catalog->name());
269    } else {
270       ua->info_msg(_("%d Jobs for client %s purged from %s catalog.\n"), del.num_del,
271          client->name(), client->catalog->name());
272    }
273 
274    if (del.JobId) {
275       free(del.JobId);
276    }
277    if (del.PurgedFiles) {
278       free(del.PurgedFiles);
279    }
280    return 1;
281 }
282 
283 
284 /*
285  * Remove File records from a list of JobIds
286  */
purge_files_from_jobs(UAContext * ua,char * jobs)287 void purge_files_from_jobs(UAContext *ua, char *jobs)
288 {
289    POOL_MEM query(PM_MESSAGE);
290 
291    Mmsg(query, "DELETE FROM File WHERE JobId IN (%s)", jobs);
292    db_sql_query(ua->db, query.c_str(), NULL, (void *)NULL);
293    Dmsg1(050, "Delete File sql=%s\n", query.c_str());
294 
295    Mmsg(query, "DELETE FROM BaseFiles WHERE JobId IN (%s)", jobs);
296    db_sql_query(ua->db, query.c_str(), NULL, (void *)NULL);
297    Dmsg1(050, "Delete BaseFiles sql=%s\n", query.c_str());
298 
299    Mmsg(query, "DELETE FROM PathVisibility WHERE JobId IN (%s)", jobs);
300    db_sql_query(ua->db, query.c_str(), NULL, (void *)NULL);
301    Dmsg1(050, "Delete PathVisibility sql=%s\n", query.c_str());
302 
303    /*
304     * Now mark Job as having files purged. This is necessary to
305     * avoid having too many Jobs to process in future prunings. If
306     * we don't do this, the number of JobId's in our in memory list
307     * could grow very large.
308     */
309    Mmsg(query, "UPDATE Job SET PurgedFiles=1 WHERE JobId IN (%s)", jobs);
310    db_sql_query(ua->db, query.c_str(), NULL, (void *)NULL);
311    Dmsg1(050, "Mark purged sql=%s\n", query.c_str());
312 }
313 
314 /*
315  * Delete jobs (all records) from the catalog in groups of 1000
316  *  at a time.
317  */
purge_job_list_from_catalog(UAContext * ua,del_ctx & del)318 void purge_job_list_from_catalog(UAContext *ua, del_ctx &del)
319 {
320    POOL_MEM jobids(PM_MESSAGE);
321    char ed1[50];
322 
323    for (int i=0; del.num_ids; ) {
324       Dmsg1(150, "num_ids=%d\n", del.num_ids);
325       pm_strcat(jobids, "");
326       for (int j=0; j<1000 && del.num_ids>0; j++) {
327          del.num_ids--;
328          if (del.JobId[i] == 0 || ua->jcr->JobId == del.JobId[i]) {
329             Dmsg2(150, "skip JobId[%d]=%d\n", i, (int)del.JobId[i]);
330             i++;
331             continue;
332          }
333          if (*jobids.c_str() != 0) {
334             pm_strcat(jobids, ",");
335          }
336          pm_strcat(jobids, edit_int64(del.JobId[i++], ed1));
337          Dmsg1(150, "Add id=%s\n", ed1);
338          del.num_del++;
339       }
340       Dmsg1(150, "num_ids=%d\n", del.num_ids);
341       purge_jobs_from_catalog(ua, jobids.c_str());
342    }
343 }
344 
345 /*
346  * Delete files from a list of jobs in groups of 1000
347  *  at a time.
348  */
purge_files_from_job_list(UAContext * ua,del_ctx & del)349 void purge_files_from_job_list(UAContext *ua, del_ctx &del)
350 {
351    POOL_MEM jobids(PM_MESSAGE);
352    char ed1[50];
353    /*
354     * OK, now we have the list of JobId's to be pruned, send them
355     *   off to be deleted batched 1000 at a time.
356     */
357    for (int i=0; del.num_ids; ) {
358       pm_strcat(jobids, "");
359       for (int j=0; j<1000 && del.num_ids>0; j++) {
360          del.num_ids--;
361          if (del.JobId[i] == 0 || ua->jcr->JobId == del.JobId[i]) {
362             Dmsg2(150, "skip JobId[%d]=%d\n", i, (int)del.JobId[i]);
363             i++;
364             continue;
365          }
366          if (*jobids.c_str() != 0) {
367             pm_strcat(jobids, ",");
368          }
369          pm_strcat(jobids, edit_int64(del.JobId[i++], ed1));
370          Dmsg1(150, "Add id=%s\n", ed1);
371          del.num_del++;
372       }
373       purge_files_from_jobs(ua, jobids.c_str());
374    }
375 }
376 
377 /*
378  * Change the type of the next copy job to backup.
379  * We need to upgrade the next copy of a normal job,
380  * and also upgrade the next copy when the normal job
381  * already have been purged.
382  *
383  *   JobId: 1   PriorJobId: 0    (original)
384  *   JobId: 2   PriorJobId: 1    (first copy)
385  *   JobId: 3   PriorJobId: 1    (second copy)
386  *
387  *   JobId: 2   PriorJobId: 1    (first copy, now regular backup)
388  *   JobId: 3   PriorJobId: 1    (second copy)
389  *
390  *  => Search through PriorJobId in jobid and
391  *                    PriorJobId in PriorJobId (jobid)
392  */
upgrade_copies(UAContext * ua,char * jobs)393 void upgrade_copies(UAContext *ua, char *jobs)
394 {
395    POOL_MEM query(PM_MESSAGE);
396    int dbtype = ua->db->bdb_get_type_index();
397 
398    db_lock(ua->db);
399 
400    Mmsg(query, uap_upgrade_copies_oldest_job[dbtype], JT_JOB_COPY, jobs, jobs);
401    db_sql_query(ua->db, query.c_str(), NULL, (void *)NULL);
402    Dmsg1(050, "Upgrade copies Log sql=%s\n", query.c_str());
403 
404    /* Now upgrade first copy to Backup */
405    Mmsg(query, "UPDATE Job SET Type='B' "      /* JT_JOB_COPY => JT_BACKUP  */
406                 "WHERE JobId IN ( SELECT JobId FROM cpy_tmp )");
407 
408    db_sql_query(ua->db, query.c_str(), NULL, (void *)NULL);
409 
410    Mmsg(query, "DROP TABLE cpy_tmp");
411    db_sql_query(ua->db, query.c_str(), NULL, (void *)NULL);
412 
413    db_unlock(ua->db);
414 }
415 
416 /*
417  * Remove all records from catalog for a list of JobIds
418  */
purge_jobs_from_catalog(UAContext * ua,char * jobs)419 void purge_jobs_from_catalog(UAContext *ua, char *jobs)
420 {
421    POOL_MEM query(PM_MESSAGE);
422 
423    /* Delete (or purge) records associated with the job */
424    purge_files_from_jobs(ua, jobs);
425 
426    Mmsg(query, "DELETE FROM JobMedia WHERE JobId IN (%s)", jobs);
427    db_sql_query(ua->db, query.c_str(), NULL, (void *)NULL);
428    Dmsg1(050, "Delete JobMedia sql=%s\n", query.c_str());
429 
430    Mmsg(query, "DELETE FROM Log WHERE JobId IN (%s)", jobs);
431    db_sql_query(ua->db, query.c_str(), NULL, (void *)NULL);
432    Dmsg1(050, "Delete Log sql=%s\n", query.c_str());
433 
434    Mmsg(query, "DELETE FROM RestoreObject WHERE JobId IN (%s)", jobs);
435    db_sql_query(ua->db, query.c_str(), NULL, (void *)NULL);
436    Dmsg1(050, "Delete RestoreObject sql=%s\n", query.c_str());
437 
438    /* The JobId of the Snapshot record is no longer usable
439     * TODO: Migth want to use a copy for the jobid?
440     */
441    Mmsg(query, "UPDATE Snapshot SET JobId=0 WHERE JobId IN (%s)", jobs);
442    db_sql_query(ua->db, query.c_str(), NULL, (void *)NULL);
443 
444    upgrade_copies(ua, jobs);
445 
446    /* Now remove the Job record itself */
447    Mmsg(query, "DELETE FROM Job WHERE JobId IN (%s)", jobs);
448    db_sql_query(ua->db, query.c_str(), NULL, (void *)NULL);
449 
450    Dmsg1(050, "Delete Job sql=%s\n", query.c_str());
451 }
452 
purge_files_from_volume(UAContext * ua,MEDIA_DBR * mr)453 void purge_files_from_volume(UAContext *ua, MEDIA_DBR *mr )
454 {} /* ***FIXME*** implement */
455 
456 /*
457  * Returns: 1 if Volume purged
458  *          0 if Volume not purged
459  */
purge_jobs_from_volume(UAContext * ua,MEDIA_DBR * mr,bool force)460 bool purge_jobs_from_volume(UAContext *ua, MEDIA_DBR *mr, bool force)
461 {
462    POOL_MEM query(PM_MESSAGE);
463    db_list_ctx lst_all, lst;
464    char *jobids=NULL;
465    int i;
466    bool purged = false;
467    bool stat;
468 
469    stat = strcmp(mr->VolStatus, "Append") == 0 ||
470           strcmp(mr->VolStatus, "Full")   == 0 ||
471           strcmp(mr->VolStatus, "Used")   == 0 ||
472           strcmp(mr->VolStatus, "Error")  == 0;
473    if (!stat) {
474       ua->error_msg(_("\nVolume \"%s\" has VolStatus \"%s\" and cannot be purged.\n"
475                      "The VolStatus must be: Append, Full, Used, or Error to be purged.\n"),
476                      mr->VolumeName, mr->VolStatus);
477       return 0;
478    }
479 
480    /*
481     * Check if he wants to purge a single jobid
482     */
483    i = find_arg_with_value(ua, "jobid");
484    if (i >= 0 && is_a_number_list(ua->argv[i])) {
485       jobids = ua->argv[i];
486 
487    } else {
488       POOL_MEM query;
489       /*
490        * Purge ALL JobIds
491        */
492       if (!db_get_volume_jobids(ua->jcr, ua->db, mr, &lst_all)) {
493          ua->error_msg("%s", db_strerror(ua->db));
494          Dmsg0(050, "Count failed\n");
495          goto bail_out;
496       }
497 
498       if (lst_all.count > 0) {
499          Mmsg(query, "SELECT JobId FROM Job WHERE JobId IN (%s) AND JobStatus NOT IN ('R', 'C')",
500               lst_all.list);
501          if (!db_sql_query(ua->db, query.c_str(), db_list_handler, &lst)) {
502             ua->error_msg("%s", db_strerror(ua->db));
503             goto bail_out;
504          }
505       }
506       jobids = lst.list;
507    }
508 
509    if (*jobids) {
510       purge_jobs_from_catalog(ua, jobids);
511       ua->info_msg(_("%d Job%s on Volume \"%s\" purged from catalog.\n"),
512                    lst.count, lst.count<=1?"":"s", mr->VolumeName);
513    }
514    purged = is_volume_purged(ua, mr, force);
515 
516 bail_out:
517    return purged;
518 }
519 
520 /*
521  * This routine will check the JobMedia records to see if the
522  *   Volume has been purged. If so, it marks it as such and
523  *
524  * Returns: true if volume purged
525  *          false if not
526  *
527  * Note, we normally will not purge a volume that has Firstor LastWritten
528  *   zero, because it means the volume is most likely being written
529  *   however, if the user manually purges using the purge command in
530  *   the console, he has been warned, and we go ahead and purge
531  *   the volume anyway, if possible).
532  */
is_volume_purged(UAContext * ua,MEDIA_DBR * mr,bool force)533 bool is_volume_purged(UAContext *ua, MEDIA_DBR *mr, bool force)
534 {
535    POOL_MEM query(PM_MESSAGE);
536    struct s_count_ctx cnt;
537    bool purged = false;
538    char ed1[50];
539 
540    if (!force && (mr->FirstWritten == 0 || mr->LastWritten == 0)) {
541       goto bail_out;               /* not written cannot purge */
542    }
543 
544    if (strcmp(mr->VolStatus, "Purged") == 0) {
545       Dmsg1(100, "Volume=%s already purged.\n", mr->VolumeName);
546       purged = true;
547       goto bail_out;
548    }
549 
550    /* If purged, mark it so */
551    cnt.count = 0;
552    Mmsg(query, "SELECT 1 FROM JobMedia WHERE MediaId=%s LIMIT 1",
553         edit_int64(mr->MediaId, ed1));
554    if (!db_sql_query(ua->db, query.c_str(), del_count_handler, (void *)&cnt)) {
555       ua->error_msg("%s", db_strerror(ua->db));
556       Dmsg0(050, "Count failed\n");
557       goto bail_out;
558    }
559 
560    if (cnt.count == 0) {
561       ua->warning_msg(_("There are no more Jobs associated with Volume \"%s\". Marking it purged.\n"),
562          mr->VolumeName);
563       Dmsg1(100, "There are no more Jobs associated with Volume \"%s\". Marking it purged.\n",
564          mr->VolumeName);
565       if (!(purged = mark_media_purged(ua, mr))) {
566          ua->error_msg("%s", db_strerror(ua->db));
567       }
568    }
569 bail_out:
570    return purged;
571 }
572 
573 /*
574  * Called here to send the appropriate commands to the SD
575  *  to do truncate on purge.
576  */
truncate_volume(UAContext * ua,MEDIA_DBR * mr,char * pool,char * storage,int drive,BSOCK * sd)577 static void truncate_volume(UAContext *ua, MEDIA_DBR *mr,
578                             char *pool, char *storage,
579                             int drive, BSOCK *sd)
580 {
581    bool ok = false;
582    uint64_t VolBytes = 0;
583    uint64_t VolABytes = 0;
584    uint32_t VolType = 0;
585 
586    if (!mr->Recycle) {
587       return;
588    }
589 
590    /* Do it only if action on purge = truncate is set */
591    if (!(mr->ActionOnPurge & ON_PURGE_TRUNCATE)) {
592       ua->error_msg(_("\nThe option \"Action On Purge = Truncate\" was not defined in the Pool resource.\n"
593                       "Truncate not allowed on Volume \"%s\"\n"), mr->VolumeName);
594       return;
595    }
596 
597    /*
598     * Send the command to truncate the volume after purge. If this feature
599     * is disabled for the specific device, this will be a no-op.
600     */
601 
602    /* Protect us from spaces */
603    bash_spaces(mr->VolumeName);
604    bash_spaces(mr->MediaType);
605    bash_spaces(pool);
606    bash_spaces(storage);
607 
608    /* Do it by relabeling the Volume, which truncates it */
609    sd->fsend("relabel %s OldName=%s NewName=%s PoolName=%s "
610              "MediaType=%s Slot=%d drive=%d\n",
611              storage,
612              mr->VolumeName, mr->VolumeName,
613              pool, mr->MediaType, mr->Slot, drive);
614 
615    unbash_spaces(mr->VolumeName);
616    unbash_spaces(mr->MediaType);
617    unbash_spaces(pool);
618    unbash_spaces(storage);
619 
620    /* Check for valid response. With cloud volumes, the upload of the part.1 can
621     * generate a dir_update_volume_info() message that is handled by bget_dirmsg()
622     */
623    while (bget_dirmsg(sd) >= 0) {
624       ua->send_msg("%s", sd->msg);
625       if (sscanf(sd->msg, "3000 OK label. VolBytes=%llu VolABytes=%lld VolType=%d ",
626                  &VolBytes, &VolABytes, &VolType) == 3) {
627 
628          ok = true;
629          /* Clean up a few things in the media record */
630          mr->VolBytes = VolBytes;
631          mr->VolABytes = VolABytes;
632          mr->VolType = VolType;
633          mr->VolFiles = 0;
634          mr->VolParts = 1;
635          mr->VolCloudParts = 0;
636          mr->LastPartBytes = VolBytes;
637          mr->VolJobs = 0;
638          mr->VolBlocks = 1;
639          mr->VolHoleBytes = 0;
640          mr->VolHoles = 0;
641          mr->EndBlock = 1;
642 
643          set_storageid_in_mr(NULL, mr);
644          if (!db_update_media_record(ua->jcr, ua->db, mr)) {
645             ua->error_msg(_("Can't update volume size in the catalog for Volume \"%s\"\n"),
646                mr->VolumeName);
647             ok = false;
648          }
649          ua->send_msg(_("The volume \"%s\" has been truncated\n"), mr->VolumeName);
650       }
651    }
652    if (!ok) {
653       ua->warning_msg(_("Error truncating Volume \"%s\"\n"), mr->VolumeName);
654    }
655 }
656 
657 /*
658  * Implement Bacula bconsole command  purge action
659  *     purge action=truncate pool= volume= storage= mediatype=
660  * or
661  *     truncate [cache] pool= volume= storage= mediatype=
662  *
663  * If the keyword "cache:  is present, then we use the truncate
664  *   command rather than relabel so that the driver can decide
665  *   whether or not it wants to truncate.  Note: only the
666  *   Cloud driver permits truncating the cache.
667  *
668  * Note, later we might want to rename this action_on_purge_cmd() as
669  *  was the original, but only if we add additional actions such as
670  *  erase, ... For the moment, we only do a truncate.
671  *
672  */
truncate_cmd(UAContext * ua,const char * cmd)673 int truncate_cmd(UAContext *ua, const char *cmd)
674 {
675    int drive = -1;
676    int nb = 0;
677    uint32_t *results = NULL;
678    const char *action = "truncate";
679    MEDIA_DBR mr;
680    POOL_DBR pr;
681    BSOCK *sd;
682    char storage[MAX_NAME_LENGTH];
683 
684    if (find_arg(ua, "cache") > 0) {
685       return cloud_volumes_cmd(ua, cmd, "truncate cache");
686    }
687 
688    memset(&pr, 0, sizeof(pr));
689 
690    /*
691     * Look for all Purged volumes that can be recycled, are enabled and
692     *  have more than 1,000 bytes (i.e. actually have data).
693     */
694    mr.Recycle = 1;
695    mr.Enabled = 1;
696    mr.VolBytes = 1000;
697    bstrncpy(mr.VolStatus, "Purged", sizeof(mr.VolStatus));
698    /* Get list of volumes to truncate */
699    if (!scan_storage_cmd(ua, cmd, true, /* allfrompool */
700                          &drive, &mr, &pr, &action, storage, &nb, &results)) {
701       goto bail_out;
702    }
703 
704    if ((sd=open_sd_bsock(ua)) == NULL) {
705       Dmsg0(100, "Can't open connection to sd\n");
706       goto bail_out;
707    }
708 
709    /*
710     * Loop over the candidate Volumes and actually truncate them
711     */
712    for (int i=0; i < nb; i++) {
713       mr.clear();
714       mr.MediaId = results[i];
715       if (db_get_media_record(ua->jcr, ua->db, &mr)) {
716          if (strcasecmp(mr.VolStatus, "Purged") != 0) {
717             ua->send_msg(_("Truncate Volume \"%s\" skipped. Status is \"%s\", but must be \"Purged\".\n"),
718                mr.VolumeName, mr.VolStatus);
719             continue;
720          }
721          if (drive < 0) {
722             STORE *store = (STORE*)GetResWithName(R_STORAGE, storage);
723             drive = get_storage_drive(ua, store);
724          }
725 
726          /* Must select Pool if not already done */
727          if (pr.PoolId == 0) {
728             pr.PoolId = mr.PoolId;
729             if (!db_get_pool_record(ua->jcr, ua->db, &pr)) {
730                goto bail_out;   /* free allocated memory */
731             }
732          }
733          if (strcasecmp("truncate", action) == 0) {
734             truncate_volume(ua, &mr, pr.Name, storage,
735                             drive, sd);
736          }
737       } else {
738          Dmsg1(0, "Can't find MediaId=%lu\n", mr.MediaId);
739       }
740    }
741 
742 bail_out:
743    close_db(ua);
744    close_sd_bsock(ua);
745    ua->jcr->wstore = NULL;
746    if (results) {
747       free(results);
748    }
749 
750    return 1;
751 }
752 
753 /*
754  * IF volume status is Append, Full, Used, or Error, mark it Purged
755  *   Purged volumes can then be recycled (if enabled).
756  */
mark_media_purged(UAContext * ua,MEDIA_DBR * mr)757 bool mark_media_purged(UAContext *ua, MEDIA_DBR *mr)
758 {
759    JCR *jcr = ua->jcr;
760    if (strcmp(mr->VolStatus, "Append") == 0 ||
761        strcmp(mr->VolStatus, "Full")   == 0 ||
762        strcmp(mr->VolStatus, "Used")   == 0 ||
763        strcmp(mr->VolStatus, "Error")  == 0) {
764       bstrncpy(mr->VolStatus, "Purged", sizeof(mr->VolStatus));
765       set_storageid_in_mr(NULL, mr);
766       if (!db_update_media_record(jcr, ua->db, mr)) {
767          return false;
768       }
769       pm_strcpy(jcr->VolumeName, mr->VolumeName);
770       generate_plugin_event(jcr, bDirEventVolumePurged);
771       /*
772        * If the RecyclePool is defined, move the volume there
773        */
774       if (mr->RecyclePoolId && mr->RecyclePoolId != mr->PoolId) {
775          POOL_DBR oldpr, newpr;
776          memset(&oldpr, 0, sizeof(POOL_DBR));
777          memset(&newpr, 0, sizeof(POOL_DBR));
778          newpr.PoolId = mr->RecyclePoolId;
779          oldpr.PoolId = mr->PoolId;
780          if (   db_get_pool_numvols(jcr, ua->db, &oldpr)
781              && db_get_pool_numvols(jcr, ua->db, &newpr)) {
782             /* check if destination pool size is ok */
783             if (newpr.MaxVols > 0 && newpr.NumVols >= newpr.MaxVols) {
784                ua->error_msg(_("Unable move recycled Volume in full "
785                               "Pool \"%s\" MaxVols=%d\n"),
786                         newpr.Name, newpr.MaxVols);
787 
788             } else {            /* move media */
789                update_vol_pool(ua, newpr.Name, mr, &oldpr);
790             }
791          } else {
792             ua->error_msg("%s", db_strerror(ua->db));
793          }
794       }
795 
796       /* Send message to Job report, if it is a *real* job */
797       if (jcr && jcr->JobId > 0) {
798          Jmsg(jcr, M_INFO, 0, _("All records pruned from Volume \"%s\"; marking it \"Purged\"\n"),
799             mr->VolumeName);
800       }
801       return true;
802    } else {
803       ua->error_msg(_("Cannot purge Volume with VolStatus=%s\n"), mr->VolStatus);
804    }
805    return strcmp(mr->VolStatus, "Purged") == 0;
806 }
807