1use canic_backup::{
2 journal::{DownloadOperationMetrics, JournalResumeReport},
3 manifest::{BackupUnitKind, ConsistencyMode, FleetBackupManifest},
4 persistence::{
5 BackupInspectionReport, BackupIntegrityReport, BackupLayout, BackupProvenanceReport,
6 PersistenceError,
7 },
8 restore::{RestoreMapping, RestorePlan, RestorePlanError, RestorePlanner},
9};
10use serde_json::json;
11use std::{
12 ffi::OsString,
13 fs,
14 io::{self, Write},
15 path::{Path, PathBuf},
16};
17use thiserror::Error as ThisError;
18
19#[derive(Debug, ThisError)]
24pub enum BackupCommandError {
25 #[error("{0}")]
26 Usage(&'static str),
27
28 #[error("missing required option {0}")]
29 MissingOption(&'static str),
30
31 #[error("unknown option {0}")]
32 UnknownOption(String),
33
34 #[error("option {0} requires a value")]
35 MissingValue(&'static str),
36
37 #[error(
38 "backup journal {backup_id} is incomplete: {pending_artifacts}/{total_artifacts} artifacts still require resume work"
39 )]
40 IncompleteJournal {
41 backup_id: String,
42 total_artifacts: usize,
43 pending_artifacts: usize,
44 },
45
46 #[error(
47 "backup inspection {backup_id} is not ready for verification: backup_id_matches={backup_id_matches}, topology_receipts_match={topology_receipts_match}, journal_complete={journal_complete}, topology_mismatches={topology_mismatches}, missing={missing_artifacts}, unexpected={unexpected_artifacts}, path_mismatches={path_mismatches}, checksum_mismatches={checksum_mismatches}"
48 )]
49 InspectionNotReady {
50 backup_id: String,
51 backup_id_matches: bool,
52 topology_receipts_match: bool,
53 journal_complete: bool,
54 topology_mismatches: usize,
55 missing_artifacts: usize,
56 unexpected_artifacts: usize,
57 path_mismatches: usize,
58 checksum_mismatches: usize,
59 },
60
61 #[error(
62 "backup provenance {backup_id} is not consistent: backup_id_matches={backup_id_matches}, topology_receipts_match={topology_receipts_match}, topology_mismatches={topology_mismatches}"
63 )]
64 ProvenanceNotConsistent {
65 backup_id: String,
66 backup_id_matches: bool,
67 topology_receipts_match: bool,
68 topology_mismatches: usize,
69 },
70
71 #[error(transparent)]
72 Io(#[from] std::io::Error),
73
74 #[error(transparent)]
75 Json(#[from] serde_json::Error),
76
77 #[error(transparent)]
78 Persistence(#[from] PersistenceError),
79
80 #[error(transparent)]
81 RestorePlan(#[from] RestorePlanError),
82}
83
84#[derive(Clone, Debug, Eq, PartialEq)]
89pub struct BackupPreflightOptions {
90 pub dir: PathBuf,
91 pub out_dir: PathBuf,
92 pub mapping: Option<PathBuf>,
93}
94
95impl BackupPreflightOptions {
96 pub fn parse<I>(args: I) -> Result<Self, BackupCommandError>
98 where
99 I: IntoIterator<Item = OsString>,
100 {
101 let mut dir = None;
102 let mut out_dir = None;
103 let mut mapping = None;
104
105 let mut args = args.into_iter();
106 while let Some(arg) = args.next() {
107 let arg = arg
108 .into_string()
109 .map_err(|_| BackupCommandError::Usage(usage()))?;
110 match arg.as_str() {
111 "--dir" => dir = Some(PathBuf::from(next_value(&mut args, "--dir")?)),
112 "--out-dir" => out_dir = Some(PathBuf::from(next_value(&mut args, "--out-dir")?)),
113 "--mapping" => mapping = Some(PathBuf::from(next_value(&mut args, "--mapping")?)),
114 "--help" | "-h" => return Err(BackupCommandError::Usage(usage())),
115 _ => return Err(BackupCommandError::UnknownOption(arg)),
116 }
117 }
118
119 Ok(Self {
120 dir: dir.ok_or(BackupCommandError::MissingOption("--dir"))?,
121 out_dir: out_dir.ok_or(BackupCommandError::MissingOption("--out-dir"))?,
122 mapping,
123 })
124 }
125}
126
127#[derive(Clone, Debug, Eq, PartialEq)]
132#[expect(
133 clippy::struct_excessive_bools,
134 reason = "preflight reports intentionally mirror machine-readable JSON status flags"
135)]
136pub struct BackupPreflightReport {
137 pub status: String,
138 pub backup_id: String,
139 pub backup_dir: String,
140 pub source_environment: String,
141 pub source_root_canister: String,
142 pub topology_hash: String,
143 pub mapping_path: Option<String>,
144 pub journal_complete: bool,
145 pub journal_operation_metrics: DownloadOperationMetrics,
146 pub inspection_status: String,
147 pub provenance_status: String,
148 pub backup_id_status: String,
149 pub topology_receipts_status: String,
150 pub topology_mismatch_count: usize,
151 pub integrity_verified: bool,
152 pub manifest_members: usize,
153 pub backup_unit_count: usize,
154 pub restore_plan_members: usize,
155 pub restore_mapping_supplied: bool,
156 pub restore_all_sources_mapped: bool,
157 pub restore_fixed_members: usize,
158 pub restore_relocatable_members: usize,
159 pub restore_in_place_members: usize,
160 pub restore_mapped_members: usize,
161 pub restore_remapped_members: usize,
162 pub restore_ready: bool,
163 pub restore_readiness_reasons: Vec<String>,
164 pub restore_all_members_have_module_hash: bool,
165 pub restore_all_members_have_wasm_hash: bool,
166 pub restore_all_members_have_code_version: bool,
167 pub restore_all_members_have_checksum: bool,
168 pub restore_members_with_module_hash: usize,
169 pub restore_members_with_wasm_hash: usize,
170 pub restore_members_with_code_version: usize,
171 pub restore_members_with_checksum: usize,
172 pub restore_verification_required: bool,
173 pub restore_all_members_have_checks: bool,
174 pub restore_fleet_checks: usize,
175 pub restore_member_check_groups: usize,
176 pub restore_member_checks: usize,
177 pub restore_members_with_checks: usize,
178 pub restore_total_checks: usize,
179 pub restore_planned_snapshot_loads: usize,
180 pub restore_planned_code_reinstalls: usize,
181 pub restore_planned_verification_checks: usize,
182 pub restore_planned_phases: usize,
183 pub restore_phase_count: usize,
184 pub restore_dependency_free_members: usize,
185 pub restore_in_group_parent_edges: usize,
186 pub restore_cross_group_parent_edges: usize,
187 pub manifest_validation_path: String,
188 pub backup_status_path: String,
189 pub backup_inspection_path: String,
190 pub backup_provenance_path: String,
191 pub backup_integrity_path: String,
192 pub restore_plan_path: String,
193 pub preflight_summary_path: String,
194}
195
196struct PreflightArtifactPaths {
201 manifest_validation: PathBuf,
202 backup_status: PathBuf,
203 backup_inspection: PathBuf,
204 backup_provenance: PathBuf,
205 backup_integrity: PathBuf,
206 restore_plan: PathBuf,
207 preflight_summary: PathBuf,
208}
209
210struct PreflightReportInput<'a> {
215 options: &'a BackupPreflightOptions,
216 manifest: &'a FleetBackupManifest,
217 status: &'a JournalResumeReport,
218 inspection: &'a BackupInspectionReport,
219 provenance: &'a BackupProvenanceReport,
220 integrity: &'a BackupIntegrityReport,
221 restore_plan: &'a RestorePlan,
222 paths: &'a PreflightArtifactPaths,
223}
224
225#[derive(Clone, Debug, Eq, PartialEq)]
230pub struct BackupInspectOptions {
231 pub dir: PathBuf,
232 pub out: Option<PathBuf>,
233 pub require_ready: bool,
234}
235
236impl BackupInspectOptions {
237 pub fn parse<I>(args: I) -> Result<Self, BackupCommandError>
239 where
240 I: IntoIterator<Item = OsString>,
241 {
242 let mut dir = None;
243 let mut out = None;
244 let mut require_ready = false;
245
246 let mut args = args.into_iter();
247 while let Some(arg) = args.next() {
248 let arg = arg
249 .into_string()
250 .map_err(|_| BackupCommandError::Usage(usage()))?;
251 match arg.as_str() {
252 "--dir" => dir = Some(PathBuf::from(next_value(&mut args, "--dir")?)),
253 "--out" => out = Some(PathBuf::from(next_value(&mut args, "--out")?)),
254 "--require-ready" => require_ready = true,
255 "--help" | "-h" => return Err(BackupCommandError::Usage(usage())),
256 _ => return Err(BackupCommandError::UnknownOption(arg)),
257 }
258 }
259
260 Ok(Self {
261 dir: dir.ok_or(BackupCommandError::MissingOption("--dir"))?,
262 out,
263 require_ready,
264 })
265 }
266}
267
268#[derive(Clone, Debug, Eq, PartialEq)]
273pub struct BackupProvenanceOptions {
274 pub dir: PathBuf,
275 pub out: Option<PathBuf>,
276 pub require_consistent: bool,
277}
278
279impl BackupProvenanceOptions {
280 pub fn parse<I>(args: I) -> Result<Self, BackupCommandError>
282 where
283 I: IntoIterator<Item = OsString>,
284 {
285 let mut dir = None;
286 let mut out = None;
287 let mut require_consistent = false;
288
289 let mut args = args.into_iter();
290 while let Some(arg) = args.next() {
291 let arg = arg
292 .into_string()
293 .map_err(|_| BackupCommandError::Usage(usage()))?;
294 match arg.as_str() {
295 "--dir" => dir = Some(PathBuf::from(next_value(&mut args, "--dir")?)),
296 "--out" => out = Some(PathBuf::from(next_value(&mut args, "--out")?)),
297 "--require-consistent" => require_consistent = true,
298 "--help" | "-h" => return Err(BackupCommandError::Usage(usage())),
299 _ => return Err(BackupCommandError::UnknownOption(arg)),
300 }
301 }
302
303 Ok(Self {
304 dir: dir.ok_or(BackupCommandError::MissingOption("--dir"))?,
305 out,
306 require_consistent,
307 })
308 }
309}
310
311#[derive(Clone, Debug, Eq, PartialEq)]
316pub struct BackupVerifyOptions {
317 pub dir: PathBuf,
318 pub out: Option<PathBuf>,
319}
320
321impl BackupVerifyOptions {
322 pub fn parse<I>(args: I) -> Result<Self, BackupCommandError>
324 where
325 I: IntoIterator<Item = OsString>,
326 {
327 let mut dir = None;
328 let mut out = None;
329
330 let mut args = args.into_iter();
331 while let Some(arg) = args.next() {
332 let arg = arg
333 .into_string()
334 .map_err(|_| BackupCommandError::Usage(usage()))?;
335 match arg.as_str() {
336 "--dir" => dir = Some(PathBuf::from(next_value(&mut args, "--dir")?)),
337 "--out" => out = Some(PathBuf::from(next_value(&mut args, "--out")?)),
338 "--help" | "-h" => return Err(BackupCommandError::Usage(usage())),
339 _ => return Err(BackupCommandError::UnknownOption(arg)),
340 }
341 }
342
343 Ok(Self {
344 dir: dir.ok_or(BackupCommandError::MissingOption("--dir"))?,
345 out,
346 })
347 }
348}
349
350#[derive(Clone, Debug, Eq, PartialEq)]
355pub struct BackupStatusOptions {
356 pub dir: PathBuf,
357 pub out: Option<PathBuf>,
358 pub require_complete: bool,
359}
360
361impl BackupStatusOptions {
362 pub fn parse<I>(args: I) -> Result<Self, BackupCommandError>
364 where
365 I: IntoIterator<Item = OsString>,
366 {
367 let mut dir = None;
368 let mut out = None;
369 let mut require_complete = false;
370
371 let mut args = args.into_iter();
372 while let Some(arg) = args.next() {
373 let arg = arg
374 .into_string()
375 .map_err(|_| BackupCommandError::Usage(usage()))?;
376 match arg.as_str() {
377 "--dir" => dir = Some(PathBuf::from(next_value(&mut args, "--dir")?)),
378 "--out" => out = Some(PathBuf::from(next_value(&mut args, "--out")?)),
379 "--require-complete" => require_complete = true,
380 "--help" | "-h" => return Err(BackupCommandError::Usage(usage())),
381 _ => return Err(BackupCommandError::UnknownOption(arg)),
382 }
383 }
384
385 Ok(Self {
386 dir: dir.ok_or(BackupCommandError::MissingOption("--dir"))?,
387 out,
388 require_complete,
389 })
390 }
391}
392
393pub fn run<I>(args: I) -> Result<(), BackupCommandError>
395where
396 I: IntoIterator<Item = OsString>,
397{
398 let mut args = args.into_iter();
399 let Some(command) = args.next().and_then(|arg| arg.into_string().ok()) else {
400 return Err(BackupCommandError::Usage(usage()));
401 };
402
403 match command.as_str() {
404 "preflight" => {
405 let options = BackupPreflightOptions::parse(args)?;
406 backup_preflight(&options)?;
407 Ok(())
408 }
409 "inspect" => {
410 let options = BackupInspectOptions::parse(args)?;
411 let report = inspect_backup(&options)?;
412 write_inspect_report(&options, &report)?;
413 enforce_inspection_requirements(&options, &report)?;
414 Ok(())
415 }
416 "provenance" => {
417 let options = BackupProvenanceOptions::parse(args)?;
418 let report = backup_provenance(&options)?;
419 write_provenance_report(&options, &report)?;
420 enforce_provenance_requirements(&options, &report)?;
421 Ok(())
422 }
423 "status" => {
424 let options = BackupStatusOptions::parse(args)?;
425 let report = backup_status(&options)?;
426 write_status_report(&options, &report)?;
427 enforce_status_requirements(&options, &report)?;
428 Ok(())
429 }
430 "verify" => {
431 let options = BackupVerifyOptions::parse(args)?;
432 let report = verify_backup(&options)?;
433 write_report(&options, &report)?;
434 Ok(())
435 }
436 "help" | "--help" | "-h" => Err(BackupCommandError::Usage(usage())),
437 _ => Err(BackupCommandError::UnknownOption(command)),
438 }
439}
440
441pub fn backup_preflight(
443 options: &BackupPreflightOptions,
444) -> Result<BackupPreflightReport, BackupCommandError> {
445 fs::create_dir_all(&options.out_dir)?;
446
447 let layout = BackupLayout::new(options.dir.clone());
448 let manifest = layout.read_manifest()?;
449 let status = layout.read_journal()?.resume_report();
450 ensure_complete_status(&status)?;
451 let inspection = layout.inspect()?;
452 let provenance = layout.provenance()?;
453 let integrity = layout.verify_integrity()?;
454 let mapping = options.mapping.as_ref().map(read_mapping).transpose()?;
455 let restore_plan = RestorePlanner::plan(&manifest, mapping.as_ref())?;
456 let paths = preflight_artifact_paths(&options.out_dir);
457
458 write_preflight_artifacts(
459 &paths,
460 &manifest,
461 &status,
462 &inspection,
463 &provenance,
464 &integrity,
465 &restore_plan,
466 )?;
467 let report = build_preflight_report(PreflightReportInput {
468 options,
469 manifest: &manifest,
470 status: &status,
471 inspection: &inspection,
472 provenance: &provenance,
473 integrity: &integrity,
474 restore_plan: &restore_plan,
475 paths: &paths,
476 });
477 write_json_value_file(&paths.preflight_summary, &preflight_summary_value(&report))?;
478 Ok(report)
479}
480
481fn preflight_artifact_paths(out_dir: &Path) -> PreflightArtifactPaths {
483 PreflightArtifactPaths {
484 manifest_validation: out_dir.join("manifest-validation.json"),
485 backup_status: out_dir.join("backup-status.json"),
486 backup_inspection: out_dir.join("backup-inspection.json"),
487 backup_provenance: out_dir.join("backup-provenance.json"),
488 backup_integrity: out_dir.join("backup-integrity.json"),
489 restore_plan: out_dir.join("restore-plan.json"),
490 preflight_summary: out_dir.join("preflight-summary.json"),
491 }
492}
493
494fn write_preflight_artifacts(
496 paths: &PreflightArtifactPaths,
497 manifest: &FleetBackupManifest,
498 status: &JournalResumeReport,
499 inspection: &BackupInspectionReport,
500 provenance: &BackupProvenanceReport,
501 integrity: &BackupIntegrityReport,
502 restore_plan: &RestorePlan,
503) -> Result<(), BackupCommandError> {
504 write_json_value_file(
505 &paths.manifest_validation,
506 &manifest_validation_summary(manifest),
507 )?;
508 fs::write(&paths.backup_status, serde_json::to_vec_pretty(&status)?)?;
509 fs::write(
510 &paths.backup_inspection,
511 serde_json::to_vec_pretty(&inspection)?,
512 )?;
513 fs::write(
514 &paths.backup_provenance,
515 serde_json::to_vec_pretty(&provenance)?,
516 )?;
517 fs::write(
518 &paths.backup_integrity,
519 serde_json::to_vec_pretty(&integrity)?,
520 )?;
521 fs::write(
522 &paths.restore_plan,
523 serde_json::to_vec_pretty(&restore_plan)?,
524 )?;
525 Ok(())
526}
527
528fn build_preflight_report(input: PreflightReportInput<'_>) -> BackupPreflightReport {
530 let identity = &input.restore_plan.identity_summary;
531 let snapshot = &input.restore_plan.snapshot_summary;
532 let verification = &input.restore_plan.verification_summary;
533 let operation = &input.restore_plan.operation_summary;
534 let ordering = &input.restore_plan.ordering_summary;
535
536 BackupPreflightReport {
537 status: "ready".to_string(),
538 backup_id: input.manifest.backup_id.clone(),
539 backup_dir: input.options.dir.display().to_string(),
540 source_environment: input.manifest.source.environment.clone(),
541 source_root_canister: input.manifest.source.root_canister.clone(),
542 topology_hash: input.manifest.fleet.topology_hash.clone(),
543 mapping_path: input
544 .options
545 .mapping
546 .as_ref()
547 .map(|path| path.display().to_string()),
548 journal_complete: input.status.is_complete,
549 journal_operation_metrics: input.status.operation_metrics.clone(),
550 inspection_status: readiness_status(input.inspection.ready_for_verify).to_string(),
551 provenance_status: consistency_status(
552 input.provenance.backup_id_matches && input.provenance.topology_receipts_match,
553 )
554 .to_string(),
555 backup_id_status: match_status(input.provenance.backup_id_matches).to_string(),
556 topology_receipts_status: match_status(input.provenance.topology_receipts_match)
557 .to_string(),
558 topology_mismatch_count: input.provenance.topology_receipt_mismatches.len(),
559 integrity_verified: input.integrity.verified,
560 manifest_members: input.manifest.fleet.members.len(),
561 backup_unit_count: input.provenance.backup_unit_count,
562 restore_plan_members: input.restore_plan.member_count,
563 restore_mapping_supplied: identity.mapping_supplied,
564 restore_all_sources_mapped: identity.all_sources_mapped,
565 restore_fixed_members: identity.fixed_members,
566 restore_relocatable_members: identity.relocatable_members,
567 restore_in_place_members: identity.in_place_members,
568 restore_mapped_members: identity.mapped_members,
569 restore_remapped_members: identity.remapped_members,
570 restore_ready: input.restore_plan.readiness_summary.ready,
571 restore_readiness_reasons: input.restore_plan.readiness_summary.reasons.clone(),
572 restore_all_members_have_module_hash: snapshot.all_members_have_module_hash,
573 restore_all_members_have_wasm_hash: snapshot.all_members_have_wasm_hash,
574 restore_all_members_have_code_version: snapshot.all_members_have_code_version,
575 restore_all_members_have_checksum: snapshot.all_members_have_checksum,
576 restore_members_with_module_hash: snapshot.members_with_module_hash,
577 restore_members_with_wasm_hash: snapshot.members_with_wasm_hash,
578 restore_members_with_code_version: snapshot.members_with_code_version,
579 restore_members_with_checksum: snapshot.members_with_checksum,
580 restore_verification_required: verification.verification_required,
581 restore_all_members_have_checks: verification.all_members_have_checks,
582 restore_fleet_checks: verification.fleet_checks,
583 restore_member_check_groups: verification.member_check_groups,
584 restore_member_checks: verification.member_checks,
585 restore_members_with_checks: verification.members_with_checks,
586 restore_total_checks: verification.total_checks,
587 restore_planned_snapshot_loads: operation.planned_snapshot_loads,
588 restore_planned_code_reinstalls: operation.planned_code_reinstalls,
589 restore_planned_verification_checks: operation.planned_verification_checks,
590 restore_planned_phases: operation.planned_phases,
591 restore_phase_count: ordering.phase_count,
592 restore_dependency_free_members: ordering.dependency_free_members,
593 restore_in_group_parent_edges: ordering.in_group_parent_edges,
594 restore_cross_group_parent_edges: ordering.cross_group_parent_edges,
595 manifest_validation_path: input.paths.manifest_validation.display().to_string(),
596 backup_status_path: input.paths.backup_status.display().to_string(),
597 backup_inspection_path: input.paths.backup_inspection.display().to_string(),
598 backup_provenance_path: input.paths.backup_provenance.display().to_string(),
599 backup_integrity_path: input.paths.backup_integrity.display().to_string(),
600 restore_plan_path: input.paths.restore_plan.display().to_string(),
601 preflight_summary_path: input.paths.preflight_summary.display().to_string(),
602 }
603}
604
605pub fn inspect_backup(
607 options: &BackupInspectOptions,
608) -> Result<BackupInspectionReport, BackupCommandError> {
609 let layout = BackupLayout::new(options.dir.clone());
610 layout.inspect().map_err(BackupCommandError::from)
611}
612
613pub fn backup_provenance(
615 options: &BackupProvenanceOptions,
616) -> Result<BackupProvenanceReport, BackupCommandError> {
617 let layout = BackupLayout::new(options.dir.clone());
618 layout.provenance().map_err(BackupCommandError::from)
619}
620
621fn enforce_provenance_requirements(
623 options: &BackupProvenanceOptions,
624 report: &BackupProvenanceReport,
625) -> Result<(), BackupCommandError> {
626 if !options.require_consistent || (report.backup_id_matches && report.topology_receipts_match) {
627 return Ok(());
628 }
629
630 Err(BackupCommandError::ProvenanceNotConsistent {
631 backup_id: report.backup_id.clone(),
632 backup_id_matches: report.backup_id_matches,
633 topology_receipts_match: report.topology_receipts_match,
634 topology_mismatches: report.topology_receipt_mismatches.len(),
635 })
636}
637
638fn enforce_inspection_requirements(
640 options: &BackupInspectOptions,
641 report: &BackupInspectionReport,
642) -> Result<(), BackupCommandError> {
643 if !options.require_ready || report.ready_for_verify {
644 return Ok(());
645 }
646
647 Err(BackupCommandError::InspectionNotReady {
648 backup_id: report.backup_id.clone(),
649 backup_id_matches: report.backup_id_matches,
650 topology_receipts_match: report.topology_receipt_mismatches.is_empty(),
651 journal_complete: report.journal_complete,
652 topology_mismatches: report.topology_receipt_mismatches.len(),
653 missing_artifacts: report.missing_journal_artifacts.len(),
654 unexpected_artifacts: report.unexpected_journal_artifacts.len(),
655 path_mismatches: report.path_mismatches.len(),
656 checksum_mismatches: report.checksum_mismatches.len(),
657 })
658}
659
660pub fn backup_status(
662 options: &BackupStatusOptions,
663) -> Result<JournalResumeReport, BackupCommandError> {
664 let layout = BackupLayout::new(options.dir.clone());
665 let journal = layout.read_journal()?;
666 Ok(journal.resume_report())
667}
668
669fn ensure_complete_status(report: &JournalResumeReport) -> Result<(), BackupCommandError> {
671 if report.is_complete {
672 return Ok(());
673 }
674
675 Err(BackupCommandError::IncompleteJournal {
676 backup_id: report.backup_id.clone(),
677 total_artifacts: report.total_artifacts,
678 pending_artifacts: report.pending_artifacts,
679 })
680}
681
682fn enforce_status_requirements(
684 options: &BackupStatusOptions,
685 report: &JournalResumeReport,
686) -> Result<(), BackupCommandError> {
687 if !options.require_complete {
688 return Ok(());
689 }
690
691 ensure_complete_status(report)
692}
693
694pub fn verify_backup(
696 options: &BackupVerifyOptions,
697) -> Result<BackupIntegrityReport, BackupCommandError> {
698 let layout = BackupLayout::new(options.dir.clone());
699 layout.verify_integrity().map_err(BackupCommandError::from)
700}
701
702fn write_status_report(
704 options: &BackupStatusOptions,
705 report: &JournalResumeReport,
706) -> Result<(), BackupCommandError> {
707 if let Some(path) = &options.out {
708 let data = serde_json::to_vec_pretty(report)?;
709 fs::write(path, data)?;
710 return Ok(());
711 }
712
713 let stdout = io::stdout();
714 let mut handle = stdout.lock();
715 serde_json::to_writer_pretty(&mut handle, report)?;
716 writeln!(handle)?;
717 Ok(())
718}
719
720fn write_inspect_report(
722 options: &BackupInspectOptions,
723 report: &BackupInspectionReport,
724) -> Result<(), BackupCommandError> {
725 if let Some(path) = &options.out {
726 let data = serde_json::to_vec_pretty(report)?;
727 fs::write(path, data)?;
728 return Ok(());
729 }
730
731 let stdout = io::stdout();
732 let mut handle = stdout.lock();
733 serde_json::to_writer_pretty(&mut handle, report)?;
734 writeln!(handle)?;
735 Ok(())
736}
737
738fn write_provenance_report(
740 options: &BackupProvenanceOptions,
741 report: &BackupProvenanceReport,
742) -> Result<(), BackupCommandError> {
743 if let Some(path) = &options.out {
744 let data = serde_json::to_vec_pretty(report)?;
745 fs::write(path, data)?;
746 return Ok(());
747 }
748
749 let stdout = io::stdout();
750 let mut handle = stdout.lock();
751 serde_json::to_writer_pretty(&mut handle, report)?;
752 writeln!(handle)?;
753 Ok(())
754}
755
756fn write_report(
758 options: &BackupVerifyOptions,
759 report: &BackupIntegrityReport,
760) -> Result<(), BackupCommandError> {
761 if let Some(path) = &options.out {
762 let data = serde_json::to_vec_pretty(report)?;
763 fs::write(path, data)?;
764 return Ok(());
765 }
766
767 let stdout = io::stdout();
768 let mut handle = stdout.lock();
769 serde_json::to_writer_pretty(&mut handle, report)?;
770 writeln!(handle)?;
771 Ok(())
772}
773
774fn write_json_value_file(
776 path: &PathBuf,
777 value: &serde_json::Value,
778) -> Result<(), BackupCommandError> {
779 if let Some(parent) = path.parent() {
780 fs::create_dir_all(parent)?;
781 }
782
783 let data = serde_json::to_vec_pretty(value)?;
784 fs::write(path, data)?;
785 Ok(())
786}
787
788fn preflight_summary_value(report: &BackupPreflightReport) -> serde_json::Value {
790 let mut summary = serde_json::Map::new();
791 insert_preflight_source_summary(&mut summary, report);
792 insert_preflight_restore_summary(&mut summary, report);
793 insert_preflight_report_paths(&mut summary, report);
794 serde_json::Value::Object(summary)
795}
796
797fn insert_summary_value(
799 summary: &mut serde_json::Map<String, serde_json::Value>,
800 key: &'static str,
801 value: serde_json::Value,
802) {
803 summary.insert(key.to_string(), value);
804}
805
806fn insert_preflight_source_summary(
808 summary: &mut serde_json::Map<String, serde_json::Value>,
809 report: &BackupPreflightReport,
810) {
811 insert_summary_value(summary, "status", json!(report.status));
812 insert_summary_value(summary, "backup_id", json!(report.backup_id));
813 insert_summary_value(summary, "backup_dir", json!(report.backup_dir));
814 insert_summary_value(
815 summary,
816 "source_environment",
817 json!(report.source_environment),
818 );
819 insert_summary_value(
820 summary,
821 "source_root_canister",
822 json!(report.source_root_canister),
823 );
824 insert_summary_value(summary, "topology_hash", json!(report.topology_hash));
825 insert_summary_value(summary, "mapping_path", json!(report.mapping_path));
826 insert_summary_value(summary, "journal_complete", json!(report.journal_complete));
827 insert_summary_value(
828 summary,
829 "journal_operation_metrics",
830 json!(report.journal_operation_metrics),
831 );
832 insert_summary_value(
833 summary,
834 "inspection_status",
835 json!(report.inspection_status),
836 );
837 insert_summary_value(
838 summary,
839 "provenance_status",
840 json!(report.provenance_status),
841 );
842 insert_summary_value(summary, "backup_id_status", json!(report.backup_id_status));
843 insert_summary_value(
844 summary,
845 "topology_receipts_status",
846 json!(report.topology_receipts_status),
847 );
848 insert_summary_value(
849 summary,
850 "topology_mismatch_count",
851 json!(report.topology_mismatch_count),
852 );
853 insert_summary_value(
854 summary,
855 "integrity_verified",
856 json!(report.integrity_verified),
857 );
858 insert_summary_value(summary, "manifest_members", json!(report.manifest_members));
859 insert_summary_value(
860 summary,
861 "backup_unit_count",
862 json!(report.backup_unit_count),
863 );
864}
865
866fn insert_preflight_restore_summary(
868 summary: &mut serde_json::Map<String, serde_json::Value>,
869 report: &BackupPreflightReport,
870) {
871 insert_summary_value(
872 summary,
873 "restore_plan_members",
874 json!(report.restore_plan_members),
875 );
876 insert_summary_value(
877 summary,
878 "restore_mapping_supplied",
879 json!(report.restore_mapping_supplied),
880 );
881 insert_summary_value(
882 summary,
883 "restore_all_sources_mapped",
884 json!(report.restore_all_sources_mapped),
885 );
886 insert_preflight_restore_identity_summary(summary, report);
887 insert_preflight_restore_readiness_summary(summary, report);
888 insert_preflight_restore_snapshot_summary(summary, report);
889 insert_preflight_restore_verification_summary(summary, report);
890 insert_preflight_restore_operation_summary(summary, report);
891 insert_preflight_restore_ordering_summary(summary, report);
892}
893
894fn insert_preflight_restore_identity_summary(
896 summary: &mut serde_json::Map<String, serde_json::Value>,
897 report: &BackupPreflightReport,
898) {
899 insert_summary_value(
900 summary,
901 "restore_fixed_members",
902 json!(report.restore_fixed_members),
903 );
904 insert_summary_value(
905 summary,
906 "restore_relocatable_members",
907 json!(report.restore_relocatable_members),
908 );
909 insert_summary_value(
910 summary,
911 "restore_in_place_members",
912 json!(report.restore_in_place_members),
913 );
914 insert_summary_value(
915 summary,
916 "restore_mapped_members",
917 json!(report.restore_mapped_members),
918 );
919 insert_summary_value(
920 summary,
921 "restore_remapped_members",
922 json!(report.restore_remapped_members),
923 );
924}
925
926fn insert_preflight_restore_readiness_summary(
928 summary: &mut serde_json::Map<String, serde_json::Value>,
929 report: &BackupPreflightReport,
930) {
931 insert_summary_value(summary, "restore_ready", json!(report.restore_ready));
932 insert_summary_value(
933 summary,
934 "restore_readiness_reasons",
935 json!(report.restore_readiness_reasons),
936 );
937}
938
939fn insert_preflight_restore_snapshot_summary(
941 summary: &mut serde_json::Map<String, serde_json::Value>,
942 report: &BackupPreflightReport,
943) {
944 insert_summary_value(
945 summary,
946 "restore_all_members_have_module_hash",
947 json!(report.restore_all_members_have_module_hash),
948 );
949 insert_summary_value(
950 summary,
951 "restore_all_members_have_wasm_hash",
952 json!(report.restore_all_members_have_wasm_hash),
953 );
954 insert_summary_value(
955 summary,
956 "restore_all_members_have_code_version",
957 json!(report.restore_all_members_have_code_version),
958 );
959 insert_summary_value(
960 summary,
961 "restore_all_members_have_checksum",
962 json!(report.restore_all_members_have_checksum),
963 );
964 insert_summary_value(
965 summary,
966 "restore_members_with_module_hash",
967 json!(report.restore_members_with_module_hash),
968 );
969 insert_summary_value(
970 summary,
971 "restore_members_with_wasm_hash",
972 json!(report.restore_members_with_wasm_hash),
973 );
974 insert_summary_value(
975 summary,
976 "restore_members_with_code_version",
977 json!(report.restore_members_with_code_version),
978 );
979 insert_summary_value(
980 summary,
981 "restore_members_with_checksum",
982 json!(report.restore_members_with_checksum),
983 );
984}
985
986fn insert_preflight_restore_verification_summary(
988 summary: &mut serde_json::Map<String, serde_json::Value>,
989 report: &BackupPreflightReport,
990) {
991 insert_summary_value(
992 summary,
993 "restore_verification_required",
994 json!(report.restore_verification_required),
995 );
996 insert_summary_value(
997 summary,
998 "restore_all_members_have_checks",
999 json!(report.restore_all_members_have_checks),
1000 );
1001 insert_summary_value(
1002 summary,
1003 "restore_fleet_checks",
1004 json!(report.restore_fleet_checks),
1005 );
1006 insert_summary_value(
1007 summary,
1008 "restore_member_check_groups",
1009 json!(report.restore_member_check_groups),
1010 );
1011 insert_summary_value(
1012 summary,
1013 "restore_member_checks",
1014 json!(report.restore_member_checks),
1015 );
1016 insert_summary_value(
1017 summary,
1018 "restore_members_with_checks",
1019 json!(report.restore_members_with_checks),
1020 );
1021 insert_summary_value(
1022 summary,
1023 "restore_total_checks",
1024 json!(report.restore_total_checks),
1025 );
1026}
1027
1028fn insert_preflight_restore_operation_summary(
1030 summary: &mut serde_json::Map<String, serde_json::Value>,
1031 report: &BackupPreflightReport,
1032) {
1033 insert_summary_value(
1034 summary,
1035 "restore_planned_snapshot_loads",
1036 json!(report.restore_planned_snapshot_loads),
1037 );
1038 insert_summary_value(
1039 summary,
1040 "restore_planned_code_reinstalls",
1041 json!(report.restore_planned_code_reinstalls),
1042 );
1043 insert_summary_value(
1044 summary,
1045 "restore_planned_verification_checks",
1046 json!(report.restore_planned_verification_checks),
1047 );
1048 insert_summary_value(
1049 summary,
1050 "restore_planned_phases",
1051 json!(report.restore_planned_phases),
1052 );
1053}
1054
1055fn insert_preflight_restore_ordering_summary(
1057 summary: &mut serde_json::Map<String, serde_json::Value>,
1058 report: &BackupPreflightReport,
1059) {
1060 insert_summary_value(
1061 summary,
1062 "restore_phase_count",
1063 json!(report.restore_phase_count),
1064 );
1065 insert_summary_value(
1066 summary,
1067 "restore_dependency_free_members",
1068 json!(report.restore_dependency_free_members),
1069 );
1070 insert_summary_value(
1071 summary,
1072 "restore_in_group_parent_edges",
1073 json!(report.restore_in_group_parent_edges),
1074 );
1075 insert_summary_value(
1076 summary,
1077 "restore_cross_group_parent_edges",
1078 json!(report.restore_cross_group_parent_edges),
1079 );
1080}
1081
1082fn insert_preflight_report_paths(
1084 summary: &mut serde_json::Map<String, serde_json::Value>,
1085 report: &BackupPreflightReport,
1086) {
1087 insert_summary_value(
1088 summary,
1089 "manifest_validation_path",
1090 json!(report.manifest_validation_path),
1091 );
1092 insert_summary_value(
1093 summary,
1094 "backup_status_path",
1095 json!(report.backup_status_path),
1096 );
1097 insert_summary_value(
1098 summary,
1099 "backup_inspection_path",
1100 json!(report.backup_inspection_path),
1101 );
1102 insert_summary_value(
1103 summary,
1104 "backup_provenance_path",
1105 json!(report.backup_provenance_path),
1106 );
1107 insert_summary_value(
1108 summary,
1109 "backup_integrity_path",
1110 json!(report.backup_integrity_path),
1111 );
1112 insert_summary_value(
1113 summary,
1114 "restore_plan_path",
1115 json!(report.restore_plan_path),
1116 );
1117 insert_summary_value(
1118 summary,
1119 "preflight_summary_path",
1120 json!(report.preflight_summary_path),
1121 );
1122}
1123
1124fn manifest_validation_summary(manifest: &FleetBackupManifest) -> serde_json::Value {
1126 json!({
1127 "status": "valid",
1128 "backup_id": manifest.backup_id,
1129 "members": manifest.fleet.members.len(),
1130 "backup_unit_count": manifest.consistency.backup_units.len(),
1131 "consistency_mode": consistency_mode_name(&manifest.consistency.mode),
1132 "topology_hash": manifest.fleet.topology_hash,
1133 "topology_hash_algorithm": manifest.fleet.topology_hash_algorithm,
1134 "topology_hash_input": manifest.fleet.topology_hash_input,
1135 "topology_validation_status": "validated",
1136 "backup_unit_kinds": backup_unit_kind_counts(manifest),
1137 "backup_units": manifest
1138 .consistency
1139 .backup_units
1140 .iter()
1141 .map(|unit| json!({
1142 "unit_id": unit.unit_id,
1143 "kind": backup_unit_kind_name(&unit.kind),
1144 "role_count": unit.roles.len(),
1145 "dependency_count": unit.dependency_closure.len(),
1146 "topology_validation": unit.topology_validation,
1147 }))
1148 .collect::<Vec<_>>(),
1149 })
1150}
1151
1152fn backup_unit_kind_counts(manifest: &FleetBackupManifest) -> serde_json::Value {
1154 let mut whole_fleet = 0;
1155 let mut control_plane_subset = 0;
1156 let mut subtree_rooted = 0;
1157 let mut flat = 0;
1158 for unit in &manifest.consistency.backup_units {
1159 match &unit.kind {
1160 BackupUnitKind::WholeFleet => whole_fleet += 1,
1161 BackupUnitKind::ControlPlaneSubset => control_plane_subset += 1,
1162 BackupUnitKind::SubtreeRooted => subtree_rooted += 1,
1163 BackupUnitKind::Flat => flat += 1,
1164 }
1165 }
1166
1167 json!({
1168 "whole_fleet": whole_fleet,
1169 "control_plane_subset": control_plane_subset,
1170 "subtree_rooted": subtree_rooted,
1171 "flat": flat,
1172 })
1173}
1174
1175const fn consistency_mode_name(mode: &ConsistencyMode) -> &'static str {
1177 match mode {
1178 ConsistencyMode::CrashConsistent => "crash-consistent",
1179 ConsistencyMode::QuiescedUnit => "quiesced-unit",
1180 }
1181}
1182
1183const fn backup_unit_kind_name(kind: &BackupUnitKind) -> &'static str {
1185 match kind {
1186 BackupUnitKind::WholeFleet => "whole-fleet",
1187 BackupUnitKind::ControlPlaneSubset => "control-plane-subset",
1188 BackupUnitKind::SubtreeRooted => "subtree-rooted",
1189 BackupUnitKind::Flat => "flat",
1190 }
1191}
1192
1193const fn readiness_status(ready: bool) -> &'static str {
1195 if ready { "ready" } else { "not-ready" }
1196}
1197
1198const fn consistency_status(consistent: bool) -> &'static str {
1200 if consistent {
1201 "consistent"
1202 } else {
1203 "inconsistent"
1204 }
1205}
1206
1207const fn match_status(matches: bool) -> &'static str {
1209 if matches { "matched" } else { "mismatched" }
1210}
1211
1212fn read_mapping(path: &PathBuf) -> Result<RestoreMapping, BackupCommandError> {
1214 let data = fs::read_to_string(path)?;
1215 serde_json::from_str(&data).map_err(BackupCommandError::from)
1216}
1217
1218fn next_value<I>(args: &mut I, option: &'static str) -> Result<String, BackupCommandError>
1220where
1221 I: Iterator<Item = OsString>,
1222{
1223 args.next()
1224 .and_then(|value| value.into_string().ok())
1225 .ok_or(BackupCommandError::MissingValue(option))
1226}
1227
1228const fn usage() -> &'static str {
1230 "usage: canic backup preflight --dir <backup-dir> --out-dir <dir> [--mapping <file>]\n canic backup inspect --dir <backup-dir> [--out <file>] [--require-ready]\n canic backup provenance --dir <backup-dir> [--out <file>] [--require-consistent]\n canic backup status --dir <backup-dir> [--out <file>] [--require-complete]\n canic backup verify --dir <backup-dir> [--out <file>]"
1231}
1232
1233#[cfg(test)]
1234mod tests {
1235 use super::*;
1236 use canic_backup::{
1237 artifacts::ArtifactChecksum,
1238 journal::{ArtifactJournalEntry, ArtifactState, DownloadJournal},
1239 manifest::{
1240 BackupUnit, BackupUnitKind, ConsistencyMode, ConsistencySection, FleetBackupManifest,
1241 FleetMember, FleetSection, IdentityMode, SourceMetadata, SourceSnapshot, ToolMetadata,
1242 VerificationCheck, VerificationPlan,
1243 },
1244 };
1245 use std::{
1246 fs,
1247 path::Path,
1248 time::{SystemTime, UNIX_EPOCH},
1249 };
1250
1251 const ROOT: &str = "aaaaa-aa";
1252 const HASH: &str = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef";
1253
1254 #[test]
1256 fn parses_backup_preflight_options() {
1257 let options = BackupPreflightOptions::parse([
1258 OsString::from("--dir"),
1259 OsString::from("backups/run"),
1260 OsString::from("--out-dir"),
1261 OsString::from("reports/run"),
1262 OsString::from("--mapping"),
1263 OsString::from("mapping.json"),
1264 ])
1265 .expect("parse options");
1266
1267 assert_eq!(options.dir, PathBuf::from("backups/run"));
1268 assert_eq!(options.out_dir, PathBuf::from("reports/run"));
1269 assert_eq!(options.mapping, Some(PathBuf::from("mapping.json")));
1270 }
1271
1272 #[test]
1274 fn backup_preflight_writes_standard_reports() {
1275 let root = temp_dir("canic-cli-backup-preflight");
1276 let out_dir = root.join("reports");
1277 let backup_dir = root.join("backup");
1278 let layout = BackupLayout::new(backup_dir.clone());
1279 let checksum = write_artifact(&backup_dir, b"root artifact");
1280
1281 layout
1282 .write_manifest(&valid_manifest())
1283 .expect("write manifest");
1284 layout
1285 .write_journal(&journal_with_checksum(checksum.hash))
1286 .expect("write journal");
1287
1288 let options = BackupPreflightOptions {
1289 dir: backup_dir,
1290 out_dir: out_dir.clone(),
1291 mapping: None,
1292 };
1293 let report = backup_preflight(&options).expect("run preflight");
1294
1295 assert_eq!(report.status, "ready");
1296 assert_eq!(report.backup_id, "backup-test");
1297 assert_eq!(report.source_environment, "local");
1298 assert_eq!(report.source_root_canister, ROOT);
1299 assert_eq!(report.topology_hash, HASH);
1300 assert_eq!(report.mapping_path, None);
1301 assert!(report.journal_complete);
1302 assert_eq!(
1303 report.journal_operation_metrics,
1304 DownloadOperationMetrics::default()
1305 );
1306 assert_eq!(report.inspection_status, "ready");
1307 assert_eq!(report.provenance_status, "consistent");
1308 assert_eq!(report.backup_id_status, "matched");
1309 assert_eq!(report.topology_receipts_status, "matched");
1310 assert_eq!(report.topology_mismatch_count, 0);
1311 assert!(report.integrity_verified);
1312 assert_eq!(report.manifest_members, 1);
1313 assert_eq!(report.backup_unit_count, 1);
1314 assert_eq!(report.restore_plan_members, 1);
1315 assert!(!report.restore_mapping_supplied);
1316 assert!(!report.restore_all_sources_mapped);
1317 assert_preflight_report_restore_counts(&report);
1318 assert!(out_dir.join("manifest-validation.json").exists());
1319 assert!(out_dir.join("backup-status.json").exists());
1320 assert!(out_dir.join("backup-inspection.json").exists());
1321 assert!(out_dir.join("backup-provenance.json").exists());
1322 assert!(out_dir.join("backup-integrity.json").exists());
1323 assert!(out_dir.join("restore-plan.json").exists());
1324 assert!(out_dir.join("preflight-summary.json").exists());
1325
1326 let summary: serde_json::Value = serde_json::from_slice(
1327 &fs::read(out_dir.join("preflight-summary.json")).expect("read summary"),
1328 )
1329 .expect("decode summary");
1330 let manifest_validation: serde_json::Value = serde_json::from_slice(
1331 &fs::read(out_dir.join("manifest-validation.json")).expect("read manifest summary"),
1332 )
1333 .expect("decode manifest summary");
1334
1335 fs::remove_dir_all(root).expect("remove temp root");
1336 assert_preflight_summary_matches_report(&summary, &report);
1337 assert_eq!(manifest_validation["backup_unit_count"], 1);
1338 assert_eq!(manifest_validation["consistency_mode"], "crash-consistent");
1339 assert_eq!(
1340 manifest_validation["topology_validation_status"],
1341 "validated"
1342 );
1343 assert_eq!(
1344 manifest_validation["backup_unit_kinds"]["subtree_rooted"],
1345 1
1346 );
1347 assert_eq!(
1348 manifest_validation["backup_units"][0]["kind"],
1349 "subtree-rooted"
1350 );
1351 }
1352
1353 fn assert_preflight_report_restore_counts(report: &BackupPreflightReport) {
1355 assert_eq!(report.restore_fixed_members, 1);
1356 assert_eq!(report.restore_relocatable_members, 0);
1357 assert_eq!(report.restore_in_place_members, 1);
1358 assert_eq!(report.restore_mapped_members, 0);
1359 assert_eq!(report.restore_remapped_members, 0);
1360 assert!(!report.restore_ready);
1361 assert_eq!(
1362 report.restore_readiness_reasons,
1363 [
1364 "missing-module-hash",
1365 "missing-wasm-hash",
1366 "missing-snapshot-checksum"
1367 ]
1368 );
1369 assert!(!report.restore_all_members_have_module_hash);
1370 assert!(!report.restore_all_members_have_wasm_hash);
1371 assert!(report.restore_all_members_have_code_version);
1372 assert!(!report.restore_all_members_have_checksum);
1373 assert_eq!(report.restore_members_with_module_hash, 0);
1374 assert_eq!(report.restore_members_with_wasm_hash, 0);
1375 assert_eq!(report.restore_members_with_code_version, 1);
1376 assert_eq!(report.restore_members_with_checksum, 0);
1377 assert!(report.restore_verification_required);
1378 assert!(report.restore_all_members_have_checks);
1379 assert_eq!(report.restore_fleet_checks, 0);
1380 assert_eq!(report.restore_member_check_groups, 0);
1381 assert_eq!(report.restore_member_checks, 1);
1382 assert_eq!(report.restore_members_with_checks, 1);
1383 assert_eq!(report.restore_total_checks, 1);
1384 assert_eq!(report.restore_planned_snapshot_loads, 1);
1385 assert_eq!(report.restore_planned_code_reinstalls, 1);
1386 assert_eq!(report.restore_planned_verification_checks, 1);
1387 assert_eq!(report.restore_planned_phases, 1);
1388 assert_eq!(report.restore_phase_count, 1);
1389 assert_eq!(report.restore_dependency_free_members, 1);
1390 assert_eq!(report.restore_in_group_parent_edges, 0);
1391 assert_eq!(report.restore_cross_group_parent_edges, 0);
1392 }
1393
1394 fn assert_preflight_summary_matches_report(
1396 summary: &serde_json::Value,
1397 report: &BackupPreflightReport,
1398 ) {
1399 assert_preflight_source_summary_matches_report(summary, report);
1400 assert_preflight_restore_identity_summary_matches_report(summary, report);
1401 assert_preflight_restore_readiness_summary_matches_report(summary, report);
1402 assert_preflight_restore_snapshot_summary_matches_report(summary, report);
1403 assert_preflight_restore_verification_summary_matches_report(summary, report);
1404 assert_preflight_restore_operation_summary_matches_report(summary, report);
1405 assert_preflight_restore_ordering_summary_matches_report(summary, report);
1406 assert_preflight_path_summary_matches_report(summary, report);
1407 }
1408
1409 fn assert_preflight_source_summary_matches_report(
1411 summary: &serde_json::Value,
1412 report: &BackupPreflightReport,
1413 ) {
1414 assert_eq!(summary["status"], report.status);
1415 assert_eq!(summary["backup_id"], report.backup_id);
1416 assert_eq!(summary["source_environment"], report.source_environment);
1417 assert_eq!(summary["source_root_canister"], report.source_root_canister);
1418 assert_eq!(summary["topology_hash"], report.topology_hash);
1419 assert_eq!(summary["journal_complete"], report.journal_complete);
1420 assert_eq!(
1421 summary["journal_operation_metrics"],
1422 json!(report.journal_operation_metrics)
1423 );
1424 assert_eq!(summary["inspection_status"], report.inspection_status);
1425 assert_eq!(summary["provenance_status"], report.provenance_status);
1426 assert_eq!(summary["backup_id_status"], report.backup_id_status);
1427 assert_eq!(
1428 summary["topology_receipts_status"],
1429 report.topology_receipts_status
1430 );
1431 assert_eq!(
1432 summary["topology_mismatch_count"],
1433 report.topology_mismatch_count
1434 );
1435 assert_eq!(summary["integrity_verified"], report.integrity_verified);
1436 assert_eq!(summary["manifest_members"], report.manifest_members);
1437 assert_eq!(summary["backup_unit_count"], report.backup_unit_count);
1438 assert_eq!(summary["restore_plan_members"], report.restore_plan_members);
1439 assert_eq!(
1440 summary["restore_mapping_supplied"],
1441 report.restore_mapping_supplied
1442 );
1443 assert_eq!(
1444 summary["restore_all_sources_mapped"],
1445 report.restore_all_sources_mapped
1446 );
1447 }
1448
1449 fn assert_preflight_restore_identity_summary_matches_report(
1451 summary: &serde_json::Value,
1452 report: &BackupPreflightReport,
1453 ) {
1454 assert_eq!(
1455 summary["restore_fixed_members"],
1456 report.restore_fixed_members
1457 );
1458 assert_eq!(
1459 summary["restore_relocatable_members"],
1460 report.restore_relocatable_members
1461 );
1462 assert_eq!(
1463 summary["restore_in_place_members"],
1464 report.restore_in_place_members
1465 );
1466 assert_eq!(
1467 summary["restore_mapped_members"],
1468 report.restore_mapped_members
1469 );
1470 assert_eq!(
1471 summary["restore_remapped_members"],
1472 report.restore_remapped_members
1473 );
1474 }
1475
1476 fn assert_preflight_restore_readiness_summary_matches_report(
1478 summary: &serde_json::Value,
1479 report: &BackupPreflightReport,
1480 ) {
1481 assert_eq!(summary["restore_ready"], report.restore_ready);
1482 assert_eq!(
1483 summary["restore_readiness_reasons"],
1484 json!(report.restore_readiness_reasons)
1485 );
1486 }
1487
1488 fn assert_preflight_restore_snapshot_summary_matches_report(
1490 summary: &serde_json::Value,
1491 report: &BackupPreflightReport,
1492 ) {
1493 assert_eq!(
1494 summary["restore_all_members_have_module_hash"],
1495 report.restore_all_members_have_module_hash
1496 );
1497 assert_eq!(
1498 summary["restore_all_members_have_wasm_hash"],
1499 report.restore_all_members_have_wasm_hash
1500 );
1501 assert_eq!(
1502 summary["restore_all_members_have_code_version"],
1503 report.restore_all_members_have_code_version
1504 );
1505 assert_eq!(
1506 summary["restore_all_members_have_checksum"],
1507 report.restore_all_members_have_checksum
1508 );
1509 assert_eq!(
1510 summary["restore_members_with_module_hash"],
1511 report.restore_members_with_module_hash
1512 );
1513 assert_eq!(
1514 summary["restore_members_with_wasm_hash"],
1515 report.restore_members_with_wasm_hash
1516 );
1517 assert_eq!(
1518 summary["restore_members_with_code_version"],
1519 report.restore_members_with_code_version
1520 );
1521 assert_eq!(
1522 summary["restore_members_with_checksum"],
1523 report.restore_members_with_checksum
1524 );
1525 }
1526
1527 fn assert_preflight_restore_verification_summary_matches_report(
1529 summary: &serde_json::Value,
1530 report: &BackupPreflightReport,
1531 ) {
1532 assert_eq!(
1533 summary["restore_verification_required"],
1534 report.restore_verification_required
1535 );
1536 assert_eq!(
1537 summary["restore_all_members_have_checks"],
1538 report.restore_all_members_have_checks
1539 );
1540 assert_eq!(summary["restore_fleet_checks"], report.restore_fleet_checks);
1541 assert_eq!(
1542 summary["restore_member_check_groups"],
1543 report.restore_member_check_groups
1544 );
1545 assert_eq!(
1546 summary["restore_member_checks"],
1547 report.restore_member_checks
1548 );
1549 assert_eq!(
1550 summary["restore_members_with_checks"],
1551 report.restore_members_with_checks
1552 );
1553 assert_eq!(summary["restore_total_checks"], report.restore_total_checks);
1554 }
1555
1556 fn assert_preflight_restore_operation_summary_matches_report(
1558 summary: &serde_json::Value,
1559 report: &BackupPreflightReport,
1560 ) {
1561 assert_eq!(
1562 summary["restore_planned_snapshot_loads"],
1563 report.restore_planned_snapshot_loads
1564 );
1565 assert_eq!(
1566 summary["restore_planned_code_reinstalls"],
1567 report.restore_planned_code_reinstalls
1568 );
1569 assert_eq!(
1570 summary["restore_planned_verification_checks"],
1571 report.restore_planned_verification_checks
1572 );
1573 assert_eq!(
1574 summary["restore_planned_phases"],
1575 report.restore_planned_phases
1576 );
1577 }
1578
1579 fn assert_preflight_restore_ordering_summary_matches_report(
1581 summary: &serde_json::Value,
1582 report: &BackupPreflightReport,
1583 ) {
1584 assert_eq!(summary["restore_phase_count"], report.restore_phase_count);
1585 assert_eq!(
1586 summary["restore_dependency_free_members"],
1587 report.restore_dependency_free_members
1588 );
1589 assert_eq!(
1590 summary["restore_in_group_parent_edges"],
1591 report.restore_in_group_parent_edges
1592 );
1593 assert_eq!(
1594 summary["restore_cross_group_parent_edges"],
1595 report.restore_cross_group_parent_edges
1596 );
1597 }
1598
1599 fn assert_preflight_path_summary_matches_report(
1601 summary: &serde_json::Value,
1602 report: &BackupPreflightReport,
1603 ) {
1604 assert_eq!(
1605 summary["manifest_validation_path"],
1606 report.manifest_validation_path
1607 );
1608 assert_eq!(summary["backup_status_path"], report.backup_status_path);
1609 assert_eq!(
1610 summary["backup_inspection_path"],
1611 report.backup_inspection_path
1612 );
1613 assert_eq!(
1614 summary["backup_provenance_path"],
1615 report.backup_provenance_path
1616 );
1617 assert_eq!(
1618 summary["backup_integrity_path"],
1619 report.backup_integrity_path
1620 );
1621 assert_eq!(summary["restore_plan_path"], report.restore_plan_path);
1622 assert_eq!(
1623 summary["preflight_summary_path"],
1624 report.preflight_summary_path
1625 );
1626 }
1627
1628 #[test]
1630 fn backup_preflight_rejects_incomplete_journal() {
1631 let root = temp_dir("canic-cli-backup-preflight-incomplete");
1632 let out_dir = root.join("reports");
1633 let backup_dir = root.join("backup");
1634 let layout = BackupLayout::new(backup_dir.clone());
1635
1636 layout
1637 .write_manifest(&valid_manifest())
1638 .expect("write manifest");
1639 layout
1640 .write_journal(&created_journal())
1641 .expect("write journal");
1642
1643 let options = BackupPreflightOptions {
1644 dir: backup_dir,
1645 out_dir,
1646 mapping: None,
1647 };
1648
1649 let err = backup_preflight(&options).expect_err("incomplete journal should fail");
1650
1651 fs::remove_dir_all(root).expect("remove temp root");
1652 assert!(matches!(
1653 err,
1654 BackupCommandError::IncompleteJournal {
1655 pending_artifacts: 1,
1656 total_artifacts: 1,
1657 ..
1658 }
1659 ));
1660 }
1661
1662 #[test]
1664 fn parses_backup_verify_options() {
1665 let options = BackupVerifyOptions::parse([
1666 OsString::from("--dir"),
1667 OsString::from("backups/run"),
1668 OsString::from("--out"),
1669 OsString::from("report.json"),
1670 ])
1671 .expect("parse options");
1672
1673 assert_eq!(options.dir, PathBuf::from("backups/run"));
1674 assert_eq!(options.out, Some(PathBuf::from("report.json")));
1675 }
1676
1677 #[test]
1679 fn parses_backup_inspect_options() {
1680 let options = BackupInspectOptions::parse([
1681 OsString::from("--dir"),
1682 OsString::from("backups/run"),
1683 OsString::from("--out"),
1684 OsString::from("inspect.json"),
1685 OsString::from("--require-ready"),
1686 ])
1687 .expect("parse options");
1688
1689 assert_eq!(options.dir, PathBuf::from("backups/run"));
1690 assert_eq!(options.out, Some(PathBuf::from("inspect.json")));
1691 assert!(options.require_ready);
1692 }
1693
1694 #[test]
1696 fn parses_backup_provenance_options() {
1697 let options = BackupProvenanceOptions::parse([
1698 OsString::from("--dir"),
1699 OsString::from("backups/run"),
1700 OsString::from("--out"),
1701 OsString::from("provenance.json"),
1702 OsString::from("--require-consistent"),
1703 ])
1704 .expect("parse options");
1705
1706 assert_eq!(options.dir, PathBuf::from("backups/run"));
1707 assert_eq!(options.out, Some(PathBuf::from("provenance.json")));
1708 assert!(options.require_consistent);
1709 }
1710
1711 #[test]
1713 fn parses_backup_status_options() {
1714 let options = BackupStatusOptions::parse([
1715 OsString::from("--dir"),
1716 OsString::from("backups/run"),
1717 OsString::from("--out"),
1718 OsString::from("status.json"),
1719 OsString::from("--require-complete"),
1720 ])
1721 .expect("parse options");
1722
1723 assert_eq!(options.dir, PathBuf::from("backups/run"));
1724 assert_eq!(options.out, Some(PathBuf::from("status.json")));
1725 assert!(options.require_complete);
1726 }
1727
1728 #[test]
1730 fn backup_status_reads_journal_resume_report() {
1731 let root = temp_dir("canic-cli-backup-status");
1732 let layout = BackupLayout::new(root.clone());
1733 layout
1734 .write_journal(&journal_with_checksum(HASH.to_string()))
1735 .expect("write journal");
1736
1737 let options = BackupStatusOptions {
1738 dir: root.clone(),
1739 out: None,
1740 require_complete: false,
1741 };
1742 let report = backup_status(&options).expect("read backup status");
1743
1744 fs::remove_dir_all(root).expect("remove temp root");
1745 assert_eq!(report.backup_id, "backup-test");
1746 assert_eq!(report.total_artifacts, 1);
1747 assert!(report.is_complete);
1748 assert_eq!(report.pending_artifacts, 0);
1749 assert_eq!(report.counts.skip, 1);
1750 }
1751
1752 #[test]
1754 fn inspect_backup_reads_layout_metadata() {
1755 let root = temp_dir("canic-cli-backup-inspect");
1756 let layout = BackupLayout::new(root.clone());
1757
1758 layout
1759 .write_manifest(&valid_manifest())
1760 .expect("write manifest");
1761 layout
1762 .write_journal(&journal_with_checksum(HASH.to_string()))
1763 .expect("write journal");
1764
1765 let options = BackupInspectOptions {
1766 dir: root.clone(),
1767 out: None,
1768 require_ready: false,
1769 };
1770 let report = inspect_backup(&options).expect("inspect backup");
1771
1772 fs::remove_dir_all(root).expect("remove temp root");
1773 assert_eq!(report.backup_id, "backup-test");
1774 assert!(report.backup_id_matches);
1775 assert!(report.journal_complete);
1776 assert!(report.ready_for_verify);
1777 assert!(report.topology_receipt_mismatches.is_empty());
1778 assert_eq!(report.matched_artifacts, 1);
1779 }
1780
1781 #[test]
1783 fn backup_provenance_reads_layout_metadata() {
1784 let root = temp_dir("canic-cli-backup-provenance");
1785 let layout = BackupLayout::new(root.clone());
1786
1787 layout
1788 .write_manifest(&valid_manifest())
1789 .expect("write manifest");
1790 layout
1791 .write_journal(&journal_with_checksum(HASH.to_string()))
1792 .expect("write journal");
1793
1794 let options = BackupProvenanceOptions {
1795 dir: root.clone(),
1796 out: None,
1797 require_consistent: false,
1798 };
1799 let report = backup_provenance(&options).expect("read provenance");
1800
1801 fs::remove_dir_all(root).expect("remove temp root");
1802 assert_eq!(report.backup_id, "backup-test");
1803 assert!(report.backup_id_matches);
1804 assert_eq!(report.source_environment, "local");
1805 assert_eq!(report.discovery_topology_hash, HASH);
1806 assert!(report.topology_receipts_match);
1807 assert!(report.topology_receipt_mismatches.is_empty());
1808 assert_eq!(report.backup_unit_count, 1);
1809 assert_eq!(report.member_count, 1);
1810 assert_eq!(report.backup_units[0].kind, "subtree-rooted");
1811 assert_eq!(report.members[0].canister_id, ROOT);
1812 assert_eq!(report.members[0].snapshot_id, "root-snapshot");
1813 assert_eq!(report.members[0].journal_state, Some("Durable".to_string()));
1814 }
1815
1816 #[test]
1818 fn require_consistent_accepts_matching_provenance() {
1819 let options = BackupProvenanceOptions {
1820 dir: PathBuf::from("unused"),
1821 out: None,
1822 require_consistent: true,
1823 };
1824 let report = ready_provenance_report();
1825
1826 enforce_provenance_requirements(&options, &report)
1827 .expect("matching provenance should pass");
1828 }
1829
1830 #[test]
1832 fn require_consistent_rejects_provenance_drift() {
1833 let options = BackupProvenanceOptions {
1834 dir: PathBuf::from("unused"),
1835 out: None,
1836 require_consistent: true,
1837 };
1838 let mut report = ready_provenance_report();
1839 report.backup_id_matches = false;
1840 report.journal_backup_id = "other-backup".to_string();
1841 report.topology_receipts_match = false;
1842 report.topology_receipt_mismatches.push(
1843 canic_backup::persistence::TopologyReceiptMismatch {
1844 field: "pre_snapshot_topology_hash".to_string(),
1845 manifest: HASH.to_string(),
1846 journal: None,
1847 },
1848 );
1849
1850 let err = enforce_provenance_requirements(&options, &report)
1851 .expect_err("provenance drift should fail");
1852
1853 assert!(matches!(
1854 err,
1855 BackupCommandError::ProvenanceNotConsistent {
1856 backup_id_matches: false,
1857 topology_receipts_match: false,
1858 topology_mismatches: 1,
1859 ..
1860 }
1861 ));
1862 }
1863
1864 #[test]
1866 fn require_ready_accepts_ready_inspection() {
1867 let options = BackupInspectOptions {
1868 dir: PathBuf::from("unused"),
1869 out: None,
1870 require_ready: true,
1871 };
1872 let report = ready_inspection_report();
1873
1874 enforce_inspection_requirements(&options, &report).expect("ready inspection should pass");
1875 }
1876
1877 #[test]
1879 fn require_ready_rejects_unready_inspection() {
1880 let options = BackupInspectOptions {
1881 dir: PathBuf::from("unused"),
1882 out: None,
1883 require_ready: true,
1884 };
1885 let mut report = ready_inspection_report();
1886 report.ready_for_verify = false;
1887 report
1888 .path_mismatches
1889 .push(canic_backup::persistence::ArtifactPathMismatch {
1890 canister_id: ROOT.to_string(),
1891 snapshot_id: "root-snapshot".to_string(),
1892 manifest: "artifacts/root".to_string(),
1893 journal: "artifacts/other-root".to_string(),
1894 });
1895
1896 let err = enforce_inspection_requirements(&options, &report)
1897 .expect_err("unready inspection should fail");
1898
1899 assert!(matches!(
1900 err,
1901 BackupCommandError::InspectionNotReady {
1902 path_mismatches: 1,
1903 ..
1904 }
1905 ));
1906 }
1907
1908 #[test]
1910 fn require_ready_rejects_topology_receipt_drift() {
1911 let options = BackupInspectOptions {
1912 dir: PathBuf::from("unused"),
1913 out: None,
1914 require_ready: true,
1915 };
1916 let mut report = ready_inspection_report();
1917 report.ready_for_verify = false;
1918 report.topology_receipt_mismatches.push(
1919 canic_backup::persistence::TopologyReceiptMismatch {
1920 field: "discovery_topology_hash".to_string(),
1921 manifest: HASH.to_string(),
1922 journal: None,
1923 },
1924 );
1925
1926 let err = enforce_inspection_requirements(&options, &report)
1927 .expect_err("topology receipt drift should fail");
1928
1929 assert!(matches!(
1930 err,
1931 BackupCommandError::InspectionNotReady {
1932 topology_receipts_match: false,
1933 topology_mismatches: 1,
1934 ..
1935 }
1936 ));
1937 }
1938
1939 #[test]
1941 fn require_complete_accepts_complete_status() {
1942 let options = BackupStatusOptions {
1943 dir: PathBuf::from("unused"),
1944 out: None,
1945 require_complete: true,
1946 };
1947 let report = journal_with_checksum(HASH.to_string()).resume_report();
1948
1949 enforce_status_requirements(&options, &report).expect("complete status should pass");
1950 }
1951
1952 #[test]
1954 fn require_complete_rejects_incomplete_status() {
1955 let options = BackupStatusOptions {
1956 dir: PathBuf::from("unused"),
1957 out: None,
1958 require_complete: true,
1959 };
1960 let report = created_journal().resume_report();
1961
1962 let err = enforce_status_requirements(&options, &report)
1963 .expect_err("incomplete status should fail");
1964
1965 assert!(matches!(
1966 err,
1967 BackupCommandError::IncompleteJournal {
1968 pending_artifacts: 1,
1969 total_artifacts: 1,
1970 ..
1971 }
1972 ));
1973 }
1974
1975 #[test]
1977 fn verify_backup_reads_layout_and_artifacts() {
1978 let root = temp_dir("canic-cli-backup-verify");
1979 let layout = BackupLayout::new(root.clone());
1980 let checksum = write_artifact(&root, b"root artifact");
1981
1982 layout
1983 .write_manifest(&valid_manifest())
1984 .expect("write manifest");
1985 layout
1986 .write_journal(&journal_with_checksum(checksum.hash.clone()))
1987 .expect("write journal");
1988
1989 let options = BackupVerifyOptions {
1990 dir: root.clone(),
1991 out: None,
1992 };
1993 let report = verify_backup(&options).expect("verify backup");
1994
1995 fs::remove_dir_all(root).expect("remove temp root");
1996 assert_eq!(report.backup_id, "backup-test");
1997 assert!(report.verified);
1998 assert_eq!(report.durable_artifacts, 1);
1999 assert_eq!(report.artifacts[0].checksum, checksum.hash);
2000 }
2001
2002 fn valid_manifest() -> FleetBackupManifest {
2004 FleetBackupManifest {
2005 manifest_version: 1,
2006 backup_id: "backup-test".to_string(),
2007 created_at: "2026-05-03T00:00:00Z".to_string(),
2008 tool: ToolMetadata {
2009 name: "canic".to_string(),
2010 version: "0.30.3".to_string(),
2011 },
2012 source: SourceMetadata {
2013 environment: "local".to_string(),
2014 root_canister: ROOT.to_string(),
2015 },
2016 consistency: ConsistencySection {
2017 mode: ConsistencyMode::CrashConsistent,
2018 backup_units: vec![BackupUnit {
2019 unit_id: "fleet".to_string(),
2020 kind: BackupUnitKind::SubtreeRooted,
2021 roles: vec!["root".to_string()],
2022 consistency_reason: None,
2023 dependency_closure: Vec::new(),
2024 topology_validation: "subtree-closed".to_string(),
2025 quiescence_strategy: None,
2026 }],
2027 },
2028 fleet: FleetSection {
2029 topology_hash_algorithm: "sha256".to_string(),
2030 topology_hash_input: "sorted(pid,parent_pid,role,module_hash)".to_string(),
2031 discovery_topology_hash: HASH.to_string(),
2032 pre_snapshot_topology_hash: HASH.to_string(),
2033 topology_hash: HASH.to_string(),
2034 members: vec![fleet_member()],
2035 },
2036 verification: VerificationPlan::default(),
2037 }
2038 }
2039
2040 fn fleet_member() -> FleetMember {
2042 FleetMember {
2043 role: "root".to_string(),
2044 canister_id: ROOT.to_string(),
2045 parent_canister_id: None,
2046 subnet_canister_id: Some(ROOT.to_string()),
2047 controller_hint: None,
2048 identity_mode: IdentityMode::Fixed,
2049 restore_group: 1,
2050 verification_class: "basic".to_string(),
2051 verification_checks: vec![VerificationCheck {
2052 kind: "status".to_string(),
2053 method: None,
2054 roles: vec!["root".to_string()],
2055 }],
2056 source_snapshot: SourceSnapshot {
2057 snapshot_id: "root-snapshot".to_string(),
2058 module_hash: None,
2059 wasm_hash: None,
2060 code_version: Some("v0.30.3".to_string()),
2061 artifact_path: "artifacts/root".to_string(),
2062 checksum_algorithm: "sha256".to_string(),
2063 checksum: None,
2064 },
2065 }
2066 }
2067
2068 fn journal_with_checksum(checksum: String) -> DownloadJournal {
2070 DownloadJournal {
2071 journal_version: 1,
2072 backup_id: "backup-test".to_string(),
2073 discovery_topology_hash: Some(HASH.to_string()),
2074 pre_snapshot_topology_hash: Some(HASH.to_string()),
2075 operation_metrics: canic_backup::journal::DownloadOperationMetrics::default(),
2076 artifacts: vec![ArtifactJournalEntry {
2077 canister_id: ROOT.to_string(),
2078 snapshot_id: "root-snapshot".to_string(),
2079 state: ArtifactState::Durable,
2080 temp_path: None,
2081 artifact_path: "artifacts/root".to_string(),
2082 checksum_algorithm: "sha256".to_string(),
2083 checksum: Some(checksum),
2084 updated_at: "2026-05-03T00:00:00Z".to_string(),
2085 }],
2086 }
2087 }
2088
2089 fn created_journal() -> DownloadJournal {
2091 DownloadJournal {
2092 journal_version: 1,
2093 backup_id: "backup-test".to_string(),
2094 discovery_topology_hash: Some(HASH.to_string()),
2095 pre_snapshot_topology_hash: Some(HASH.to_string()),
2096 operation_metrics: canic_backup::journal::DownloadOperationMetrics::default(),
2097 artifacts: vec![ArtifactJournalEntry {
2098 canister_id: ROOT.to_string(),
2099 snapshot_id: "root-snapshot".to_string(),
2100 state: ArtifactState::Created,
2101 temp_path: None,
2102 artifact_path: "artifacts/root".to_string(),
2103 checksum_algorithm: "sha256".to_string(),
2104 checksum: None,
2105 updated_at: "2026-05-03T00:00:00Z".to_string(),
2106 }],
2107 }
2108 }
2109
2110 fn ready_inspection_report() -> BackupInspectionReport {
2112 BackupInspectionReport {
2113 backup_id: "backup-test".to_string(),
2114 manifest_backup_id: "backup-test".to_string(),
2115 journal_backup_id: "backup-test".to_string(),
2116 backup_id_matches: true,
2117 journal_complete: true,
2118 ready_for_verify: true,
2119 manifest_members: 1,
2120 journal_artifacts: 1,
2121 matched_artifacts: 1,
2122 topology_receipt_mismatches: Vec::new(),
2123 missing_journal_artifacts: Vec::new(),
2124 unexpected_journal_artifacts: Vec::new(),
2125 path_mismatches: Vec::new(),
2126 checksum_mismatches: Vec::new(),
2127 }
2128 }
2129
2130 fn ready_provenance_report() -> BackupProvenanceReport {
2132 BackupProvenanceReport {
2133 backup_id: "backup-test".to_string(),
2134 manifest_backup_id: "backup-test".to_string(),
2135 journal_backup_id: "backup-test".to_string(),
2136 backup_id_matches: true,
2137 manifest_version: 1,
2138 journal_version: 1,
2139 created_at: "2026-05-03T00:00:00Z".to_string(),
2140 tool_name: "canic".to_string(),
2141 tool_version: "0.30.12".to_string(),
2142 source_environment: "local".to_string(),
2143 source_root_canister: ROOT.to_string(),
2144 topology_hash_algorithm: "sha256".to_string(),
2145 topology_hash_input: "sorted(pid,parent_pid,role,module_hash)".to_string(),
2146 discovery_topology_hash: HASH.to_string(),
2147 pre_snapshot_topology_hash: HASH.to_string(),
2148 accepted_topology_hash: HASH.to_string(),
2149 journal_discovery_topology_hash: Some(HASH.to_string()),
2150 journal_pre_snapshot_topology_hash: Some(HASH.to_string()),
2151 topology_receipts_match: true,
2152 topology_receipt_mismatches: Vec::new(),
2153 backup_unit_count: 1,
2154 member_count: 1,
2155 consistency_mode: "crash-consistent".to_string(),
2156 backup_units: Vec::new(),
2157 members: Vec::new(),
2158 }
2159 }
2160
2161 fn write_artifact(root: &Path, bytes: &[u8]) -> ArtifactChecksum {
2163 let path = root.join("artifacts/root");
2164 fs::create_dir_all(path.parent().expect("artifact has parent")).expect("create artifacts");
2165 fs::write(&path, bytes).expect("write artifact");
2166 ArtifactChecksum::from_bytes(bytes)
2167 }
2168
2169 fn temp_dir(prefix: &str) -> PathBuf {
2171 let nanos = SystemTime::now()
2172 .duration_since(UNIX_EPOCH)
2173 .expect("system time after epoch")
2174 .as_nanos();
2175 std::env::temp_dir().join(format!("{prefix}-{}-{nanos}", std::process::id()))
2176 }
2177}