1use crate::{
2 journal::{DownloadOperationMetrics, JournalResumeReport},
3 manifest::{FleetBackupManifest, manifest_validation_summary},
4 persistence::{
5 BackupInspectionReport, BackupIntegrityReport, BackupLayout, BackupProvenanceReport,
6 PersistenceError,
7 },
8 restore::{RestoreMapping, RestorePlan, RestorePlanError, RestorePlanner, RestoreStatus},
9};
10use serde_json::json;
11use std::{
12 fs,
13 path::{Path, PathBuf},
14};
15use thiserror::Error as ThisError;
16
17#[derive(Clone, Debug, Eq, PartialEq)]
22pub struct BackupPreflightConfig {
23 pub backup_dir: PathBuf,
24 pub out_dir: PathBuf,
25 pub mapping: Option<PathBuf>,
26}
27
28#[derive(Clone, Debug, Eq, PartialEq)]
33#[expect(
34 clippy::struct_excessive_bools,
35 reason = "preflight reports intentionally mirror machine-readable JSON status flags"
36)]
37pub struct BackupPreflightReport {
38 pub status: String,
39 pub backup_id: String,
40 pub backup_dir: String,
41 pub source_environment: String,
42 pub source_root_canister: String,
43 pub topology_hash: String,
44 pub mapping_path: Option<String>,
45 pub journal_complete: bool,
46 pub journal_operation_metrics: DownloadOperationMetrics,
47 pub inspection_status: String,
48 pub provenance_status: String,
49 pub backup_id_status: String,
50 pub topology_receipts_status: String,
51 pub topology_mismatch_count: usize,
52 pub integrity_verified: bool,
53 pub manifest_design_v1_ready: bool,
54 pub manifest_members: usize,
55 pub backup_unit_count: usize,
56 pub restore_plan_members: usize,
57 pub restore_mapping_supplied: bool,
58 pub restore_all_sources_mapped: bool,
59 pub restore_fixed_members: usize,
60 pub restore_relocatable_members: usize,
61 pub restore_in_place_members: usize,
62 pub restore_mapped_members: usize,
63 pub restore_remapped_members: usize,
64 pub restore_ready: bool,
65 pub restore_readiness_reasons: Vec<String>,
66 pub restore_all_members_have_module_hash: bool,
67 pub restore_all_members_have_wasm_hash: bool,
68 pub restore_all_members_have_code_version: bool,
69 pub restore_all_members_have_checksum: bool,
70 pub restore_members_with_module_hash: usize,
71 pub restore_members_with_wasm_hash: usize,
72 pub restore_members_with_code_version: usize,
73 pub restore_members_with_checksum: usize,
74 pub restore_verification_required: bool,
75 pub restore_all_members_have_checks: bool,
76 pub restore_fleet_checks: usize,
77 pub restore_member_check_groups: usize,
78 pub restore_member_checks: usize,
79 pub restore_members_with_checks: usize,
80 pub restore_total_checks: usize,
81 pub restore_planned_snapshot_uploads: usize,
82 pub restore_planned_snapshot_loads: usize,
83 pub restore_planned_code_reinstalls: usize,
84 pub restore_planned_verification_checks: usize,
85 pub restore_planned_operations: usize,
86 pub restore_planned_phases: usize,
87 pub restore_phase_count: usize,
88 pub restore_dependency_free_members: usize,
89 pub restore_in_group_parent_edges: usize,
90 pub restore_cross_group_parent_edges: usize,
91 pub manifest_validation_path: String,
92 pub backup_status_path: String,
93 pub backup_inspection_path: String,
94 pub backup_provenance_path: String,
95 pub backup_integrity_path: String,
96 pub restore_plan_path: String,
97 pub restore_status_path: String,
98 pub preflight_summary_path: String,
99}
100
101#[derive(Debug, ThisError)]
106pub enum BackupPreflightError {
107 #[error(
108 "backup journal {backup_id} is incomplete: {pending_artifacts}/{total_artifacts} artifacts still require resume work"
109 )]
110 IncompleteJournal {
111 backup_id: String,
112 total_artifacts: usize,
113 pending_artifacts: usize,
114 },
115
116 #[error(transparent)]
117 Io(#[from] std::io::Error),
118
119 #[error(transparent)]
120 Json(#[from] serde_json::Error),
121
122 #[error(transparent)]
123 Persistence(#[from] PersistenceError),
124
125 #[error(transparent)]
126 RestorePlan(#[from] RestorePlanError),
127}
128
129struct PreflightArtifactPaths {
134 manifest_validation: PathBuf,
135 backup_status: PathBuf,
136 backup_inspection: PathBuf,
137 backup_provenance: PathBuf,
138 backup_integrity: PathBuf,
139 restore_plan: PathBuf,
140 restore_status: PathBuf,
141 preflight_summary: PathBuf,
142}
143
144struct PreflightReportInput<'a> {
149 config: &'a BackupPreflightConfig,
150 manifest: &'a FleetBackupManifest,
151 status: &'a JournalResumeReport,
152 inspection: &'a BackupInspectionReport,
153 provenance: &'a BackupProvenanceReport,
154 integrity: &'a BackupIntegrityReport,
155 restore_plan: &'a RestorePlan,
156 paths: &'a PreflightArtifactPaths,
157}
158
159struct PreflightArtifactInput<'a> {
164 paths: &'a PreflightArtifactPaths,
165 manifest: &'a FleetBackupManifest,
166 status: &'a JournalResumeReport,
167 inspection: &'a BackupInspectionReport,
168 provenance: &'a BackupProvenanceReport,
169 integrity: &'a BackupIntegrityReport,
170 restore_plan: &'a RestorePlan,
171 restore_status: &'a RestoreStatus,
172}
173
174pub fn run_backup_preflight(
176 config: &BackupPreflightConfig,
177) -> Result<BackupPreflightReport, BackupPreflightError> {
178 fs::create_dir_all(&config.out_dir)?;
179
180 let layout = BackupLayout::new(config.backup_dir.clone());
181 let manifest = layout.read_manifest()?;
182 let status = layout.read_journal()?.resume_report();
183 ensure_complete_status(&status)?;
184 let inspection = layout.inspect()?;
185 let provenance = layout.provenance()?;
186 let integrity = layout.verify_integrity()?;
187 let mapping = config.mapping.as_ref().map(read_mapping).transpose()?;
188 let restore_plan = RestorePlanner::plan(&manifest, mapping.as_ref())?;
189 let restore_status = RestoreStatus::from_plan(&restore_plan);
190 let paths = preflight_artifact_paths(&config.out_dir);
191
192 write_preflight_artifacts(PreflightArtifactInput {
193 paths: &paths,
194 manifest: &manifest,
195 status: &status,
196 inspection: &inspection,
197 provenance: &provenance,
198 integrity: &integrity,
199 restore_plan: &restore_plan,
200 restore_status: &restore_status,
201 })?;
202 let report = build_preflight_report(PreflightReportInput {
203 config,
204 manifest: &manifest,
205 status: &status,
206 inspection: &inspection,
207 provenance: &provenance,
208 integrity: &integrity,
209 restore_plan: &restore_plan,
210 paths: &paths,
211 });
212 write_json_value_file(&paths.preflight_summary, &preflight_summary_value(&report))?;
213 Ok(report)
214}
215
216fn ensure_complete_status(report: &JournalResumeReport) -> Result<(), BackupPreflightError> {
218 if report.is_complete {
219 return Ok(());
220 }
221
222 Err(BackupPreflightError::IncompleteJournal {
223 backup_id: report.backup_id.clone(),
224 total_artifacts: report.total_artifacts,
225 pending_artifacts: report.pending_artifacts,
226 })
227}
228
229fn preflight_artifact_paths(out_dir: &Path) -> PreflightArtifactPaths {
231 PreflightArtifactPaths {
232 manifest_validation: out_dir.join("manifest-validation.json"),
233 backup_status: out_dir.join("backup-status.json"),
234 backup_inspection: out_dir.join("backup-inspection.json"),
235 backup_provenance: out_dir.join("backup-provenance.json"),
236 backup_integrity: out_dir.join("backup-integrity.json"),
237 restore_plan: out_dir.join("restore-plan.json"),
238 restore_status: out_dir.join("restore-status.json"),
239 preflight_summary: out_dir.join("preflight-summary.json"),
240 }
241}
242
243fn write_preflight_artifacts(
245 input: PreflightArtifactInput<'_>,
246) -> Result<(), BackupPreflightError> {
247 write_json_value_file(
248 &input.paths.manifest_validation,
249 &manifest_validation_summary(input.manifest),
250 )?;
251 fs::write(
252 &input.paths.backup_status,
253 serde_json::to_vec_pretty(&input.status)?,
254 )?;
255 fs::write(
256 &input.paths.backup_inspection,
257 serde_json::to_vec_pretty(&input.inspection)?,
258 )?;
259 fs::write(
260 &input.paths.backup_provenance,
261 serde_json::to_vec_pretty(&input.provenance)?,
262 )?;
263 fs::write(
264 &input.paths.backup_integrity,
265 serde_json::to_vec_pretty(&input.integrity)?,
266 )?;
267 fs::write(
268 &input.paths.restore_plan,
269 serde_json::to_vec_pretty(&input.restore_plan)?,
270 )?;
271 fs::write(
272 &input.paths.restore_status,
273 serde_json::to_vec_pretty(&input.restore_status)?,
274 )?;
275 Ok(())
276}
277
278fn build_preflight_report(input: PreflightReportInput<'_>) -> BackupPreflightReport {
280 let identity = &input.restore_plan.identity_summary;
281 let snapshot = &input.restore_plan.snapshot_summary;
282 let verification = &input.restore_plan.verification_summary;
283 let operation = &input.restore_plan.operation_summary;
284 let ordering = &input.restore_plan.ordering_summary;
285
286 BackupPreflightReport {
287 status: "ready".to_string(),
288 backup_id: input.manifest.backup_id.clone(),
289 backup_dir: input.config.backup_dir.display().to_string(),
290 source_environment: input.manifest.source.environment.clone(),
291 source_root_canister: input.manifest.source.root_canister.clone(),
292 topology_hash: input.manifest.fleet.topology_hash.clone(),
293 mapping_path: input
294 .config
295 .mapping
296 .as_ref()
297 .map(|path| path.display().to_string()),
298 journal_complete: input.status.is_complete,
299 journal_operation_metrics: input.status.operation_metrics.clone(),
300 inspection_status: readiness_status(input.inspection.ready_for_verify).to_string(),
301 provenance_status: consistency_status(
302 input.provenance.backup_id_matches && input.provenance.topology_receipts_match,
303 )
304 .to_string(),
305 backup_id_status: match_status(input.provenance.backup_id_matches).to_string(),
306 topology_receipts_status: match_status(input.provenance.topology_receipts_match)
307 .to_string(),
308 topology_mismatch_count: input.provenance.topology_receipt_mismatches.len(),
309 integrity_verified: input.integrity.verified,
310 manifest_design_v1_ready: input.manifest.design_conformance_report().design_v1_ready,
311 manifest_members: input.manifest.fleet.members.len(),
312 backup_unit_count: input.provenance.backup_unit_count,
313 restore_plan_members: input.restore_plan.member_count,
314 restore_mapping_supplied: identity.mapping_supplied,
315 restore_all_sources_mapped: identity.all_sources_mapped,
316 restore_fixed_members: identity.fixed_members,
317 restore_relocatable_members: identity.relocatable_members,
318 restore_in_place_members: identity.in_place_members,
319 restore_mapped_members: identity.mapped_members,
320 restore_remapped_members: identity.remapped_members,
321 restore_ready: input.restore_plan.readiness_summary.ready,
322 restore_readiness_reasons: input.restore_plan.readiness_summary.reasons.clone(),
323 restore_all_members_have_module_hash: snapshot.all_members_have_module_hash,
324 restore_all_members_have_wasm_hash: snapshot.all_members_have_wasm_hash,
325 restore_all_members_have_code_version: snapshot.all_members_have_code_version,
326 restore_all_members_have_checksum: snapshot.all_members_have_checksum,
327 restore_members_with_module_hash: snapshot.members_with_module_hash,
328 restore_members_with_wasm_hash: snapshot.members_with_wasm_hash,
329 restore_members_with_code_version: snapshot.members_with_code_version,
330 restore_members_with_checksum: snapshot.members_with_checksum,
331 restore_verification_required: verification.verification_required,
332 restore_all_members_have_checks: verification.all_members_have_checks,
333 restore_fleet_checks: verification.fleet_checks,
334 restore_member_check_groups: verification.member_check_groups,
335 restore_member_checks: verification.member_checks,
336 restore_members_with_checks: verification.members_with_checks,
337 restore_total_checks: verification.total_checks,
338 restore_planned_snapshot_uploads: operation
339 .effective_planned_snapshot_uploads(input.restore_plan.member_count),
340 restore_planned_snapshot_loads: operation.planned_snapshot_loads,
341 restore_planned_code_reinstalls: operation.planned_code_reinstalls,
342 restore_planned_verification_checks: operation.planned_verification_checks,
343 restore_planned_operations: operation
344 .effective_planned_operations(input.restore_plan.member_count),
345 restore_planned_phases: operation.planned_phases,
346 restore_phase_count: ordering.phase_count,
347 restore_dependency_free_members: ordering.dependency_free_members,
348 restore_in_group_parent_edges: ordering.in_group_parent_edges,
349 restore_cross_group_parent_edges: ordering.cross_group_parent_edges,
350 manifest_validation_path: input.paths.manifest_validation.display().to_string(),
351 backup_status_path: input.paths.backup_status.display().to_string(),
352 backup_inspection_path: input.paths.backup_inspection.display().to_string(),
353 backup_provenance_path: input.paths.backup_provenance.display().to_string(),
354 backup_integrity_path: input.paths.backup_integrity.display().to_string(),
355 restore_plan_path: input.paths.restore_plan.display().to_string(),
356 restore_status_path: input.paths.restore_status.display().to_string(),
357 preflight_summary_path: input.paths.preflight_summary.display().to_string(),
358 }
359}
360
361fn preflight_summary_value(report: &BackupPreflightReport) -> serde_json::Value {
363 let mut summary = serde_json::Map::new();
364 insert_preflight_source_summary(&mut summary, report);
365 insert_preflight_restore_summary(&mut summary, report);
366 insert_preflight_report_paths(&mut summary, report);
367 serde_json::Value::Object(summary)
368}
369
370fn insert_summary_value(
372 summary: &mut serde_json::Map<String, serde_json::Value>,
373 key: &'static str,
374 value: serde_json::Value,
375) {
376 summary.insert(key.to_string(), value);
377}
378
379fn insert_preflight_source_summary(
381 summary: &mut serde_json::Map<String, serde_json::Value>,
382 report: &BackupPreflightReport,
383) {
384 insert_summary_value(summary, "status", json!(report.status));
385 insert_summary_value(summary, "backup_id", json!(report.backup_id));
386 insert_summary_value(summary, "backup_dir", json!(report.backup_dir));
387 insert_summary_value(
388 summary,
389 "source_environment",
390 json!(report.source_environment),
391 );
392 insert_summary_value(
393 summary,
394 "source_root_canister",
395 json!(report.source_root_canister),
396 );
397 insert_summary_value(summary, "topology_hash", json!(report.topology_hash));
398 insert_summary_value(summary, "mapping_path", json!(report.mapping_path));
399 insert_summary_value(summary, "journal_complete", json!(report.journal_complete));
400 insert_summary_value(
401 summary,
402 "journal_operation_metrics",
403 json!(report.journal_operation_metrics),
404 );
405 insert_summary_value(
406 summary,
407 "inspection_status",
408 json!(report.inspection_status),
409 );
410 insert_summary_value(
411 summary,
412 "provenance_status",
413 json!(report.provenance_status),
414 );
415 insert_summary_value(summary, "backup_id_status", json!(report.backup_id_status));
416 insert_summary_value(
417 summary,
418 "topology_receipts_status",
419 json!(report.topology_receipts_status),
420 );
421 insert_summary_value(
422 summary,
423 "topology_mismatch_count",
424 json!(report.topology_mismatch_count),
425 );
426 insert_summary_value(
427 summary,
428 "integrity_verified",
429 json!(report.integrity_verified),
430 );
431 insert_summary_value(
432 summary,
433 "manifest_design_v1_ready",
434 json!(report.manifest_design_v1_ready),
435 );
436 insert_summary_value(summary, "manifest_members", json!(report.manifest_members));
437 insert_summary_value(
438 summary,
439 "backup_unit_count",
440 json!(report.backup_unit_count),
441 );
442}
443
444fn insert_preflight_restore_summary(
446 summary: &mut serde_json::Map<String, serde_json::Value>,
447 report: &BackupPreflightReport,
448) {
449 insert_summary_value(
450 summary,
451 "restore_plan_members",
452 json!(report.restore_plan_members),
453 );
454 insert_summary_value(
455 summary,
456 "restore_mapping_supplied",
457 json!(report.restore_mapping_supplied),
458 );
459 insert_summary_value(
460 summary,
461 "restore_all_sources_mapped",
462 json!(report.restore_all_sources_mapped),
463 );
464 insert_preflight_restore_identity_summary(summary, report);
465 insert_preflight_restore_readiness_summary(summary, report);
466 insert_preflight_restore_snapshot_summary(summary, report);
467 insert_preflight_restore_verification_summary(summary, report);
468 insert_preflight_restore_operation_summary(summary, report);
469 insert_preflight_restore_ordering_summary(summary, report);
470}
471
472fn insert_preflight_restore_identity_summary(
474 summary: &mut serde_json::Map<String, serde_json::Value>,
475 report: &BackupPreflightReport,
476) {
477 insert_summary_value(
478 summary,
479 "restore_fixed_members",
480 json!(report.restore_fixed_members),
481 );
482 insert_summary_value(
483 summary,
484 "restore_relocatable_members",
485 json!(report.restore_relocatable_members),
486 );
487 insert_summary_value(
488 summary,
489 "restore_in_place_members",
490 json!(report.restore_in_place_members),
491 );
492 insert_summary_value(
493 summary,
494 "restore_mapped_members",
495 json!(report.restore_mapped_members),
496 );
497 insert_summary_value(
498 summary,
499 "restore_remapped_members",
500 json!(report.restore_remapped_members),
501 );
502}
503
504fn insert_preflight_restore_readiness_summary(
506 summary: &mut serde_json::Map<String, serde_json::Value>,
507 report: &BackupPreflightReport,
508) {
509 insert_summary_value(summary, "restore_ready", json!(report.restore_ready));
510 insert_summary_value(
511 summary,
512 "restore_readiness_reasons",
513 json!(report.restore_readiness_reasons),
514 );
515}
516
517fn insert_preflight_restore_snapshot_summary(
519 summary: &mut serde_json::Map<String, serde_json::Value>,
520 report: &BackupPreflightReport,
521) {
522 insert_summary_value(
523 summary,
524 "restore_all_members_have_module_hash",
525 json!(report.restore_all_members_have_module_hash),
526 );
527 insert_summary_value(
528 summary,
529 "restore_all_members_have_wasm_hash",
530 json!(report.restore_all_members_have_wasm_hash),
531 );
532 insert_summary_value(
533 summary,
534 "restore_all_members_have_code_version",
535 json!(report.restore_all_members_have_code_version),
536 );
537 insert_summary_value(
538 summary,
539 "restore_all_members_have_checksum",
540 json!(report.restore_all_members_have_checksum),
541 );
542 insert_summary_value(
543 summary,
544 "restore_members_with_module_hash",
545 json!(report.restore_members_with_module_hash),
546 );
547 insert_summary_value(
548 summary,
549 "restore_members_with_wasm_hash",
550 json!(report.restore_members_with_wasm_hash),
551 );
552 insert_summary_value(
553 summary,
554 "restore_members_with_code_version",
555 json!(report.restore_members_with_code_version),
556 );
557 insert_summary_value(
558 summary,
559 "restore_members_with_checksum",
560 json!(report.restore_members_with_checksum),
561 );
562}
563
564fn insert_preflight_restore_verification_summary(
566 summary: &mut serde_json::Map<String, serde_json::Value>,
567 report: &BackupPreflightReport,
568) {
569 insert_summary_value(
570 summary,
571 "restore_verification_required",
572 json!(report.restore_verification_required),
573 );
574 insert_summary_value(
575 summary,
576 "restore_all_members_have_checks",
577 json!(report.restore_all_members_have_checks),
578 );
579 insert_summary_value(
580 summary,
581 "restore_fleet_checks",
582 json!(report.restore_fleet_checks),
583 );
584 insert_summary_value(
585 summary,
586 "restore_member_check_groups",
587 json!(report.restore_member_check_groups),
588 );
589 insert_summary_value(
590 summary,
591 "restore_member_checks",
592 json!(report.restore_member_checks),
593 );
594 insert_summary_value(
595 summary,
596 "restore_members_with_checks",
597 json!(report.restore_members_with_checks),
598 );
599 insert_summary_value(
600 summary,
601 "restore_total_checks",
602 json!(report.restore_total_checks),
603 );
604}
605
606fn insert_preflight_restore_operation_summary(
608 summary: &mut serde_json::Map<String, serde_json::Value>,
609 report: &BackupPreflightReport,
610) {
611 insert_summary_value(
612 summary,
613 "restore_planned_snapshot_uploads",
614 json!(report.restore_planned_snapshot_uploads),
615 );
616 insert_summary_value(
617 summary,
618 "restore_planned_snapshot_loads",
619 json!(report.restore_planned_snapshot_loads),
620 );
621 insert_summary_value(
622 summary,
623 "restore_planned_code_reinstalls",
624 json!(report.restore_planned_code_reinstalls),
625 );
626 insert_summary_value(
627 summary,
628 "restore_planned_verification_checks",
629 json!(report.restore_planned_verification_checks),
630 );
631 insert_summary_value(
632 summary,
633 "restore_planned_operations",
634 json!(report.restore_planned_operations),
635 );
636 insert_summary_value(
637 summary,
638 "restore_planned_phases",
639 json!(report.restore_planned_phases),
640 );
641}
642
643fn insert_preflight_restore_ordering_summary(
645 summary: &mut serde_json::Map<String, serde_json::Value>,
646 report: &BackupPreflightReport,
647) {
648 insert_summary_value(
649 summary,
650 "restore_phase_count",
651 json!(report.restore_phase_count),
652 );
653 insert_summary_value(
654 summary,
655 "restore_dependency_free_members",
656 json!(report.restore_dependency_free_members),
657 );
658 insert_summary_value(
659 summary,
660 "restore_in_group_parent_edges",
661 json!(report.restore_in_group_parent_edges),
662 );
663 insert_summary_value(
664 summary,
665 "restore_cross_group_parent_edges",
666 json!(report.restore_cross_group_parent_edges),
667 );
668}
669
670fn insert_preflight_report_paths(
672 summary: &mut serde_json::Map<String, serde_json::Value>,
673 report: &BackupPreflightReport,
674) {
675 insert_summary_value(
676 summary,
677 "manifest_validation_path",
678 json!(report.manifest_validation_path),
679 );
680 insert_summary_value(
681 summary,
682 "backup_status_path",
683 json!(report.backup_status_path),
684 );
685 insert_summary_value(
686 summary,
687 "backup_inspection_path",
688 json!(report.backup_inspection_path),
689 );
690 insert_summary_value(
691 summary,
692 "backup_provenance_path",
693 json!(report.backup_provenance_path),
694 );
695 insert_summary_value(
696 summary,
697 "backup_integrity_path",
698 json!(report.backup_integrity_path),
699 );
700 insert_summary_value(
701 summary,
702 "restore_plan_path",
703 json!(report.restore_plan_path),
704 );
705 insert_summary_value(
706 summary,
707 "restore_status_path",
708 json!(report.restore_status_path),
709 );
710 insert_summary_value(
711 summary,
712 "preflight_summary_path",
713 json!(report.preflight_summary_path),
714 );
715}
716
717const fn readiness_status(ready: bool) -> &'static str {
719 if ready { "ready" } else { "not-ready" }
720}
721
722const fn consistency_status(consistent: bool) -> &'static str {
724 if consistent {
725 "consistent"
726 } else {
727 "inconsistent"
728 }
729}
730
731const fn match_status(matches: bool) -> &'static str {
733 if matches { "matched" } else { "mismatched" }
734}
735
736fn read_mapping(path: &PathBuf) -> Result<RestoreMapping, BackupPreflightError> {
738 let data = fs::read_to_string(path)?;
739 serde_json::from_str(&data).map_err(BackupPreflightError::from)
740}
741
742fn write_json_value_file(
744 path: &PathBuf,
745 value: &serde_json::Value,
746) -> Result<(), BackupPreflightError> {
747 fs::write(path, serde_json::to_vec_pretty(value)?)?;
748 Ok(())
749}