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_summary_values<const N: usize>(
381 summary: &mut serde_json::Map<String, serde_json::Value>,
382 values: [(&'static str, serde_json::Value); N],
383) {
384 for (key, value) in values {
385 insert_summary_value(summary, key, value);
386 }
387}
388
389fn insert_preflight_source_summary(
391 summary: &mut serde_json::Map<String, serde_json::Value>,
392 report: &BackupPreflightReport,
393) {
394 insert_summary_values(
395 summary,
396 [
397 ("status", json!(report.status)),
398 ("backup_id", json!(report.backup_id)),
399 ("backup_dir", json!(report.backup_dir)),
400 ("source_environment", json!(report.source_environment)),
401 ("source_root_canister", json!(report.source_root_canister)),
402 ("topology_hash", json!(report.topology_hash)),
403 ("mapping_path", json!(report.mapping_path)),
404 ("journal_complete", json!(report.journal_complete)),
405 (
406 "journal_operation_metrics",
407 json!(report.journal_operation_metrics),
408 ),
409 ("inspection_status", json!(report.inspection_status)),
410 ("provenance_status", json!(report.provenance_status)),
411 ("backup_id_status", json!(report.backup_id_status)),
412 (
413 "topology_receipts_status",
414 json!(report.topology_receipts_status),
415 ),
416 (
417 "topology_mismatch_count",
418 json!(report.topology_mismatch_count),
419 ),
420 ("integrity_verified", json!(report.integrity_verified)),
421 (
422 "manifest_design_v1_ready",
423 json!(report.manifest_design_v1_ready),
424 ),
425 ("manifest_members", json!(report.manifest_members)),
426 ("backup_unit_count", json!(report.backup_unit_count)),
427 ],
428 );
429}
430
431fn insert_preflight_restore_summary(
433 summary: &mut serde_json::Map<String, serde_json::Value>,
434 report: &BackupPreflightReport,
435) {
436 insert_summary_values(
437 summary,
438 [
439 ("restore_plan_members", json!(report.restore_plan_members)),
440 (
441 "restore_mapping_supplied",
442 json!(report.restore_mapping_supplied),
443 ),
444 (
445 "restore_all_sources_mapped",
446 json!(report.restore_all_sources_mapped),
447 ),
448 ],
449 );
450 insert_preflight_restore_identity_summary(summary, report);
451 insert_preflight_restore_readiness_summary(summary, report);
452 insert_preflight_restore_snapshot_summary(summary, report);
453 insert_preflight_restore_verification_summary(summary, report);
454 insert_preflight_restore_operation_summary(summary, report);
455 insert_preflight_restore_ordering_summary(summary, report);
456}
457
458fn insert_preflight_restore_identity_summary(
460 summary: &mut serde_json::Map<String, serde_json::Value>,
461 report: &BackupPreflightReport,
462) {
463 insert_summary_values(
464 summary,
465 [
466 ("restore_fixed_members", json!(report.restore_fixed_members)),
467 (
468 "restore_relocatable_members",
469 json!(report.restore_relocatable_members),
470 ),
471 (
472 "restore_in_place_members",
473 json!(report.restore_in_place_members),
474 ),
475 (
476 "restore_mapped_members",
477 json!(report.restore_mapped_members),
478 ),
479 (
480 "restore_remapped_members",
481 json!(report.restore_remapped_members),
482 ),
483 ],
484 );
485}
486
487fn insert_preflight_restore_readiness_summary(
489 summary: &mut serde_json::Map<String, serde_json::Value>,
490 report: &BackupPreflightReport,
491) {
492 insert_summary_values(
493 summary,
494 [
495 ("restore_ready", json!(report.restore_ready)),
496 (
497 "restore_readiness_reasons",
498 json!(report.restore_readiness_reasons),
499 ),
500 ],
501 );
502}
503
504fn insert_preflight_restore_snapshot_summary(
506 summary: &mut serde_json::Map<String, serde_json::Value>,
507 report: &BackupPreflightReport,
508) {
509 insert_summary_values(
510 summary,
511 [
512 (
513 "restore_all_members_have_module_hash",
514 json!(report.restore_all_members_have_module_hash),
515 ),
516 (
517 "restore_all_members_have_wasm_hash",
518 json!(report.restore_all_members_have_wasm_hash),
519 ),
520 (
521 "restore_all_members_have_code_version",
522 json!(report.restore_all_members_have_code_version),
523 ),
524 (
525 "restore_all_members_have_checksum",
526 json!(report.restore_all_members_have_checksum),
527 ),
528 (
529 "restore_members_with_module_hash",
530 json!(report.restore_members_with_module_hash),
531 ),
532 (
533 "restore_members_with_wasm_hash",
534 json!(report.restore_members_with_wasm_hash),
535 ),
536 (
537 "restore_members_with_code_version",
538 json!(report.restore_members_with_code_version),
539 ),
540 (
541 "restore_members_with_checksum",
542 json!(report.restore_members_with_checksum),
543 ),
544 ],
545 );
546}
547
548fn insert_preflight_restore_verification_summary(
550 summary: &mut serde_json::Map<String, serde_json::Value>,
551 report: &BackupPreflightReport,
552) {
553 insert_summary_values(
554 summary,
555 [
556 (
557 "restore_verification_required",
558 json!(report.restore_verification_required),
559 ),
560 (
561 "restore_all_members_have_checks",
562 json!(report.restore_all_members_have_checks),
563 ),
564 ("restore_fleet_checks", json!(report.restore_fleet_checks)),
565 (
566 "restore_member_check_groups",
567 json!(report.restore_member_check_groups),
568 ),
569 ("restore_member_checks", json!(report.restore_member_checks)),
570 (
571 "restore_members_with_checks",
572 json!(report.restore_members_with_checks),
573 ),
574 ("restore_total_checks", json!(report.restore_total_checks)),
575 ],
576 );
577}
578
579fn insert_preflight_restore_operation_summary(
581 summary: &mut serde_json::Map<String, serde_json::Value>,
582 report: &BackupPreflightReport,
583) {
584 insert_summary_values(
585 summary,
586 [
587 (
588 "restore_planned_snapshot_uploads",
589 json!(report.restore_planned_snapshot_uploads),
590 ),
591 (
592 "restore_planned_snapshot_loads",
593 json!(report.restore_planned_snapshot_loads),
594 ),
595 (
596 "restore_planned_code_reinstalls",
597 json!(report.restore_planned_code_reinstalls),
598 ),
599 (
600 "restore_planned_verification_checks",
601 json!(report.restore_planned_verification_checks),
602 ),
603 (
604 "restore_planned_operations",
605 json!(report.restore_planned_operations),
606 ),
607 (
608 "restore_planned_phases",
609 json!(report.restore_planned_phases),
610 ),
611 ],
612 );
613}
614
615fn insert_preflight_restore_ordering_summary(
617 summary: &mut serde_json::Map<String, serde_json::Value>,
618 report: &BackupPreflightReport,
619) {
620 insert_summary_values(
621 summary,
622 [
623 ("restore_phase_count", json!(report.restore_phase_count)),
624 (
625 "restore_dependency_free_members",
626 json!(report.restore_dependency_free_members),
627 ),
628 (
629 "restore_in_group_parent_edges",
630 json!(report.restore_in_group_parent_edges),
631 ),
632 (
633 "restore_cross_group_parent_edges",
634 json!(report.restore_cross_group_parent_edges),
635 ),
636 ],
637 );
638}
639
640fn insert_preflight_report_paths(
642 summary: &mut serde_json::Map<String, serde_json::Value>,
643 report: &BackupPreflightReport,
644) {
645 insert_summary_values(
646 summary,
647 [
648 (
649 "manifest_validation_path",
650 json!(report.manifest_validation_path),
651 ),
652 ("backup_status_path", json!(report.backup_status_path)),
653 (
654 "backup_inspection_path",
655 json!(report.backup_inspection_path),
656 ),
657 (
658 "backup_provenance_path",
659 json!(report.backup_provenance_path),
660 ),
661 ("backup_integrity_path", json!(report.backup_integrity_path)),
662 ("restore_plan_path", json!(report.restore_plan_path)),
663 ("restore_status_path", json!(report.restore_status_path)),
664 (
665 "preflight_summary_path",
666 json!(report.preflight_summary_path),
667 ),
668 ],
669 );
670}
671
672const fn readiness_status(ready: bool) -> &'static str {
674 if ready { "ready" } else { "not-ready" }
675}
676
677const fn consistency_status(consistent: bool) -> &'static str {
679 if consistent {
680 "consistent"
681 } else {
682 "inconsistent"
683 }
684}
685
686const fn match_status(matches: bool) -> &'static str {
688 if matches { "matched" } else { "mismatched" }
689}
690
691fn read_mapping(path: &PathBuf) -> Result<RestoreMapping, BackupPreflightError> {
693 let data = fs::read_to_string(path)?;
694 serde_json::from_str(&data).map_err(BackupPreflightError::from)
695}
696
697fn write_json_value_file(
699 path: &PathBuf,
700 value: &serde_json::Value,
701) -> Result<(), BackupPreflightError> {
702 fs::write(path, serde_json::to_vec_pretty(value)?)?;
703 Ok(())
704}