1use super::{RestorePlan, RestorePlanMember};
2use crate::{
3 artifacts::{ArtifactChecksum, ArtifactChecksumError},
4 manifest::VerificationCheck,
5 persistence::resolve_backup_artifact_path,
6};
7use serde::{Deserialize, Serialize};
8use std::path::{Path, PathBuf};
9use thiserror::Error as ThisError;
10
11mod journal;
12
13pub(in crate::restore) use journal::RestoreApplyCommandOutputPair;
14pub(in crate::restore) use journal::RestoreApplyJournalReport;
15pub use journal::{
16 RestoreApplyCommandConfig, RestoreApplyCommandOutput, RestoreApplyCommandPreview,
17 RestoreApplyJournal, RestoreApplyJournalError, RestoreApplyJournalOperation,
18 RestoreApplyOperationKind, RestoreApplyOperationKindCounts, RestoreApplyOperationReceipt,
19 RestoreApplyOperationReceiptOutcome, RestoreApplyOperationState, RestoreApplyPendingSummary,
20 RestoreApplyProgressSummary, RestoreApplyReportOperation, RestoreApplyReportOutcome,
21 RestoreApplyRunnerCommand,
22};
23
24#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
29pub struct RestoreApplyDryRun {
30 pub dry_run_version: u16,
31 pub backup_id: String,
32 pub ready: bool,
33 pub readiness_reasons: Vec<String>,
34 pub member_count: usize,
35 #[serde(default)]
36 pub planned_canister_stops: usize,
37 #[serde(default)]
38 pub planned_canister_starts: usize,
39 pub planned_snapshot_uploads: usize,
40 pub planned_snapshot_loads: usize,
41 pub planned_verification_checks: usize,
42 pub planned_operations: usize,
43 pub rendered_operations: usize,
44 #[serde(default)]
45 pub operation_counts: RestoreApplyOperationKindCounts,
46 pub artifact_validation: Option<RestoreApplyArtifactValidation>,
47 pub operations: Vec<RestoreApplyDryRunOperation>,
48}
49
50impl RestoreApplyDryRun {
51 #[must_use]
53 pub fn from_plan(plan: &RestorePlan) -> Self {
54 Self::from_validated_plan(plan)
55 }
56
57 pub fn try_from_plan_with_artifacts(
59 plan: &RestorePlan,
60 backup_root: &Path,
61 ) -> Result<Self, RestoreApplyDryRunError> {
62 let mut dry_run = Self::from_plan(plan);
63 dry_run.artifact_validation = Some(validate_restore_apply_artifacts(plan, backup_root)?);
64 Ok(dry_run)
65 }
66
67 fn from_validated_plan(plan: &RestorePlan) -> Self {
69 let mut next_sequence = 0;
70 let ordered_members = plan.ordered_members();
71 let mut operations = Vec::new();
72 append_member_phase(
73 &mut operations,
74 &mut next_sequence,
75 RestoreApplyOperationKind::UploadSnapshot,
76 ordered_members.iter().copied(),
77 );
78 append_member_phase(
79 &mut operations,
80 &mut next_sequence,
81 RestoreApplyOperationKind::StopCanister,
82 ordered_members.iter().copied(),
83 );
84 append_member_phase(
85 &mut operations,
86 &mut next_sequence,
87 RestoreApplyOperationKind::LoadSnapshot,
88 ordered_members.iter().copied(),
89 );
90 append_member_phase(
91 &mut operations,
92 &mut next_sequence,
93 RestoreApplyOperationKind::StartCanister,
94 ordered_members.iter().rev().copied(),
95 );
96 append_member_verification_operations(
97 &mut operations,
98 &mut next_sequence,
99 &ordered_members,
100 );
101 append_fleet_verification_operations(plan, &mut operations, &mut next_sequence);
102 let rendered_operations = operations.len();
103 let operation_counts =
104 RestoreApplyOperationKindCounts::from_dry_run_operations(&operations);
105
106 Self {
107 dry_run_version: 1,
108 backup_id: plan.backup_id.clone(),
109 ready: plan.readiness_summary.ready,
110 readiness_reasons: plan.readiness_summary.reasons.clone(),
111 member_count: plan.member_count,
112 planned_canister_stops: plan.operation_summary.planned_canister_stops,
113 planned_canister_starts: plan.operation_summary.planned_canister_starts,
114 planned_snapshot_uploads: plan.operation_summary.planned_snapshot_uploads,
115 planned_snapshot_loads: plan.operation_summary.planned_snapshot_loads,
116 planned_verification_checks: plan.operation_summary.planned_verification_checks,
117 planned_operations: plan.operation_summary.planned_operations,
118 rendered_operations,
119 operation_counts,
120 artifact_validation: None,
121 operations,
122 }
123 }
124}
125
126fn validate_restore_apply_artifacts(
128 plan: &RestorePlan,
129 backup_root: &Path,
130) -> Result<RestoreApplyArtifactValidation, RestoreApplyDryRunError> {
131 let mut checks = Vec::new();
132
133 for member in plan.ordered_members() {
134 checks.push(validate_restore_apply_artifact(member, backup_root)?);
135 }
136
137 let members_with_expected_checksums = checks
138 .iter()
139 .filter(|check| check.checksum_expected.is_some())
140 .count();
141 let artifacts_present = checks.iter().all(|check| check.exists);
142 let checksums_verified = members_with_expected_checksums == plan.member_count
143 && checks.iter().all(|check| check.checksum_verified);
144
145 Ok(RestoreApplyArtifactValidation {
146 backup_root: backup_root.to_string_lossy().to_string(),
147 checked_members: checks.len(),
148 artifacts_present,
149 checksums_verified,
150 members_with_expected_checksums,
151 checks,
152 })
153}
154
155fn validate_restore_apply_artifact(
157 member: &RestorePlanMember,
158 backup_root: &Path,
159) -> Result<RestoreApplyArtifactCheck, RestoreApplyDryRunError> {
160 let resolved_path = safe_restore_artifact_path(
161 backup_root,
162 &member.source_canister,
163 &member.source_snapshot.artifact_path,
164 )?;
165
166 if !resolved_path.exists() {
167 return Err(RestoreApplyDryRunError::ArtifactMissing {
168 source_canister: member.source_canister.clone(),
169 artifact_path: member.source_snapshot.artifact_path.clone(),
170 resolved_path: resolved_path.to_string_lossy().to_string(),
171 });
172 }
173
174 let (checksum_actual, checksum_verified) =
175 if let Some(expected) = &member.source_snapshot.checksum {
176 let checksum = ArtifactChecksum::from_path(&resolved_path).map_err(|source| {
177 RestoreApplyDryRunError::ArtifactChecksum {
178 source_canister: member.source_canister.clone(),
179 artifact_path: member.source_snapshot.artifact_path.clone(),
180 source,
181 }
182 })?;
183 checksum.verify(expected).map_err(|source| {
184 RestoreApplyDryRunError::ArtifactChecksum {
185 source_canister: member.source_canister.clone(),
186 artifact_path: member.source_snapshot.artifact_path.clone(),
187 source,
188 }
189 })?;
190 (Some(checksum.hash), true)
191 } else {
192 (None, false)
193 };
194
195 Ok(RestoreApplyArtifactCheck {
196 source_canister: member.source_canister.clone(),
197 target_canister: member.target_canister.clone(),
198 snapshot_id: member.source_snapshot.snapshot_id.clone(),
199 artifact_path: member.source_snapshot.artifact_path.clone(),
200 resolved_path: resolved_path.to_string_lossy().to_string(),
201 exists: true,
202 checksum_algorithm: member.source_snapshot.checksum_algorithm.clone(),
203 checksum_expected: member.source_snapshot.checksum.clone(),
204 checksum_actual,
205 checksum_verified,
206 })
207}
208
209fn safe_restore_artifact_path(
211 backup_root: &Path,
212 source_canister: &str,
213 artifact_path: &str,
214) -> Result<PathBuf, RestoreApplyDryRunError> {
215 if let Some(path) = resolve_backup_artifact_path(backup_root, artifact_path) {
216 return Ok(path);
217 }
218
219 Err(RestoreApplyDryRunError::ArtifactPathEscapesBackup {
220 source_canister: source_canister.to_string(),
221 artifact_path: artifact_path.to_string(),
222 })
223}
224
225#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
230pub struct RestoreApplyArtifactValidation {
231 pub backup_root: String,
232 pub checked_members: usize,
233 pub artifacts_present: bool,
234 pub checksums_verified: bool,
235 pub members_with_expected_checksums: usize,
236 pub checks: Vec<RestoreApplyArtifactCheck>,
237}
238
239#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
244pub struct RestoreApplyArtifactCheck {
245 pub source_canister: String,
246 pub target_canister: String,
247 pub snapshot_id: String,
248 pub artifact_path: String,
249 pub resolved_path: String,
250 pub exists: bool,
251 pub checksum_algorithm: String,
252 pub checksum_expected: Option<String>,
253 pub checksum_actual: Option<String>,
254 pub checksum_verified: bool,
255}
256
257fn append_member_phase<'a>(
259 operations: &mut Vec<RestoreApplyDryRunOperation>,
260 next_sequence: &mut usize,
261 operation: RestoreApplyOperationKind,
262 members: impl IntoIterator<Item = &'a RestorePlanMember>,
263) {
264 for member in members {
265 push_member_operation(operations, next_sequence, operation.clone(), member, None);
266 }
267}
268
269fn append_member_verification_operations(
271 operations: &mut Vec<RestoreApplyDryRunOperation>,
272 next_sequence: &mut usize,
273 members: &[&RestorePlanMember],
274) {
275 for member in members {
276 for check in &member.verification_checks {
277 push_member_operation(
278 operations,
279 next_sequence,
280 RestoreApplyOperationKind::VerifyMember,
281 member,
282 Some(check),
283 );
284 }
285 }
286}
287
288fn push_member_operation(
290 operations: &mut Vec<RestoreApplyDryRunOperation>,
291 next_sequence: &mut usize,
292 operation: RestoreApplyOperationKind,
293 member: &RestorePlanMember,
294 check: Option<&VerificationCheck>,
295) {
296 let sequence = *next_sequence;
297 *next_sequence += 1;
298
299 let snapshot_id = operation_snapshot_id(&operation, member);
300 let artifact_path = operation_artifact_path(&operation, member);
301
302 operations.push(RestoreApplyDryRunOperation {
303 sequence,
304 operation,
305 member_order: member.member_order,
306 source_canister: member.source_canister.clone(),
307 target_canister: member.target_canister.clone(),
308 role: member.role.clone(),
309 snapshot_id,
310 artifact_path,
311 verification_kind: check.map(|check| check.kind.clone()),
312 });
313}
314
315fn operation_snapshot_id(
316 operation: &RestoreApplyOperationKind,
317 member: &RestorePlanMember,
318) -> Option<String> {
319 matches!(
320 operation,
321 RestoreApplyOperationKind::UploadSnapshot | RestoreApplyOperationKind::LoadSnapshot
322 )
323 .then(|| member.source_snapshot.snapshot_id.clone())
324}
325
326fn operation_artifact_path(
327 operation: &RestoreApplyOperationKind,
328 member: &RestorePlanMember,
329) -> Option<String> {
330 matches!(
331 operation,
332 RestoreApplyOperationKind::UploadSnapshot | RestoreApplyOperationKind::LoadSnapshot
333 )
334 .then(|| member.source_snapshot.artifact_path.clone())
335}
336
337fn append_fleet_verification_operations(
339 plan: &RestorePlan,
340 operations: &mut Vec<RestoreApplyDryRunOperation>,
341 next_sequence: &mut usize,
342) {
343 if plan.fleet_verification_checks.is_empty() {
344 return;
345 }
346
347 let root = plan
348 .members
349 .iter()
350 .find(|member| member.source_canister == plan.source_root_canister);
351 let source_canister = root.map_or_else(
352 || plan.source_root_canister.clone(),
353 |member| member.source_canister.clone(),
354 );
355 let target_canister = root.map_or_else(
356 || plan.source_root_canister.clone(),
357 |member| member.target_canister.clone(),
358 );
359 for check in &plan.fleet_verification_checks {
360 push_fleet_operation(
361 operations,
362 next_sequence,
363 &source_canister,
364 &target_canister,
365 check,
366 );
367 }
368}
369
370fn push_fleet_operation(
372 operations: &mut Vec<RestoreApplyDryRunOperation>,
373 next_sequence: &mut usize,
374 source_canister: &str,
375 target_canister: &str,
376 check: &VerificationCheck,
377) {
378 let sequence = *next_sequence;
379 *next_sequence += 1;
380 let member_order = operations.len();
381
382 operations.push(RestoreApplyDryRunOperation {
383 sequence,
384 operation: RestoreApplyOperationKind::VerifyFleet,
385 member_order,
386 source_canister: source_canister.to_string(),
387 target_canister: target_canister.to_string(),
388 role: "fleet".to_string(),
389 snapshot_id: None,
390 artifact_path: None,
391 verification_kind: Some(check.kind.clone()),
392 });
393}
394
395#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
400pub struct RestoreApplyDryRunOperation {
401 pub sequence: usize,
402 pub operation: RestoreApplyOperationKind,
403 pub member_order: usize,
404 pub source_canister: String,
405 pub target_canister: String,
406 pub role: String,
407 pub snapshot_id: Option<String>,
408 pub artifact_path: Option<String>,
409 pub verification_kind: Option<String>,
410}
411
412#[derive(Debug, ThisError)]
417pub enum RestoreApplyDryRunError {
418 #[error("restore artifact path for {source_canister} escapes backup root: {artifact_path}")]
419 ArtifactPathEscapesBackup {
420 source_canister: String,
421 artifact_path: String,
422 },
423
424 #[error(
425 "restore artifact for {source_canister} is missing: {artifact_path} at {resolved_path}"
426 )]
427 ArtifactMissing {
428 source_canister: String,
429 artifact_path: String,
430 resolved_path: String,
431 },
432
433 #[error("restore artifact checksum failed for {source_canister} at {artifact_path}: {source}")]
434 ArtifactChecksum {
435 source_canister: String,
436 artifact_path: String,
437 #[source]
438 source: ArtifactChecksumError,
439 },
440}