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