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