1use crate::{
2 execution::{BackupExecutionJournalOperation, BackupExecutionResumeSummary},
3 plan::{BackupExecutionPreflightReceipts, BackupPlan},
4};
5use serde::Serialize;
6use std::{error::Error as StdError, fmt, path::Path, path::PathBuf};
7use thiserror::Error as ThisError;
8
9use crate::persistence::JournalLockError;
10
11#[derive(Clone, Debug, Eq, PartialEq)]
16pub struct BackupRunnerConfig {
17 pub out: PathBuf,
18 pub max_steps: Option<usize>,
19 pub updated_at: Option<String>,
20 pub tool_name: String,
21 pub tool_version: String,
22}
23
24pub trait BackupRunnerExecutor {
29 fn preflight_receipts(
31 &mut self,
32 plan: &BackupPlan,
33 preflight_id: &str,
34 validated_at: &str,
35 expires_at: &str,
36 ) -> Result<BackupExecutionPreflightReceipts, BackupRunnerCommandError>;
37
38 fn stop_canister(&mut self, canister_id: &str) -> Result<(), BackupRunnerCommandError>;
40
41 fn start_canister(&mut self, canister_id: &str) -> Result<(), BackupRunnerCommandError>;
43
44 fn create_snapshot(
46 &mut self,
47 canister_id: &str,
48 ) -> Result<BackupRunnerSnapshotReceipt, BackupRunnerCommandError>;
49
50 fn download_snapshot(
52 &mut self,
53 canister_id: &str,
54 snapshot_id: &str,
55 artifact_path: &Path,
56 ) -> Result<(), BackupRunnerCommandError>;
57}
58
59#[derive(Clone, Debug, Eq, PartialEq)]
64pub struct BackupRunnerSnapshotReceipt {
65 pub snapshot_id: String,
66 pub taken_at_timestamp: Option<u64>,
67 pub total_size_bytes: Option<u64>,
68}
69
70#[derive(Clone, Debug, Eq, PartialEq)]
75pub struct BackupRunnerCommandError {
76 pub status: String,
77 pub message: String,
78}
79
80impl BackupRunnerCommandError {
81 #[must_use]
82 pub fn failed(status: impl Into<String>, message: impl Into<String>) -> Self {
83 Self {
84 status: status.into(),
85 message: message.into(),
86 }
87 }
88}
89
90impl fmt::Display for BackupRunnerCommandError {
91 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
92 write!(formatter, "{}: {}", self.status, self.message)
93 }
94}
95
96impl StdError for BackupRunnerCommandError {}
97
98#[derive(Debug, ThisError)]
103pub enum BackupRunnerError {
104 #[error("backup execution journal is locked: {lock_path}")]
105 JournalLocked { lock_path: String },
106
107 #[error("backup execution journal lock path is unsafe: {lock_path} ({kind})")]
108 JournalLockUnsafeEntry { lock_path: String, kind: String },
109
110 #[error(
111 "download journal backup id does not match the backup plan: expected={expected}, actual={actual}"
112 )]
113 DownloadJournalBackupIdMismatch { expected: String, actual: String },
114
115 #[error(
116 "download journal topology receipt {field} does not match the backup plan: expected={expected}, actual={actual}"
117 )]
118 DownloadJournalTopologyMismatch {
119 field: &'static str,
120 expected: String,
121 actual: String,
122 },
123
124 #[error("backup operation {sequence} has no target canister")]
125 MissingOperationTarget { sequence: usize },
126
127 #[error("backup operation {sequence} has no snapshot id for target {target_canister_id}")]
128 MissingSnapshotId {
129 sequence: usize,
130 target_canister_id: String,
131 },
132
133 #[error(
134 "backup operation {sequence} has no artifact journal entry for target {target_canister_id}"
135 )]
136 MissingArtifactEntry {
137 sequence: usize,
138 target_canister_id: String,
139 },
140
141 #[error(
142 "backup operation {sequence} artifact temp path for target {target_canister_id} does not match expected runner path: journal={journal_path}, expected={expected_path}"
143 )]
144 ArtifactTempPathMismatch {
145 sequence: usize,
146 target_canister_id: String,
147 journal_path: String,
148 expected_path: String,
149 },
150
151 #[error("backup operation {sequence} failed: {status}: {message}")]
152 CommandFailed {
153 sequence: usize,
154 status: String,
155 message: String,
156 },
157
158 #[error("backup preflight failed: {status}: {message}")]
159 PreflightFailed { status: String, message: String },
160
161 #[error("backup execution has no operation ready to run")]
162 NoReadyOperation,
163
164 #[error("backup execution is blocked: {reasons:?}")]
165 Blocked { reasons: Vec<String> },
166
167 #[error(transparent)]
168 Io(#[from] std::io::Error),
169
170 #[error(transparent)]
171 Json(#[from] serde_json::Error),
172
173 #[error(transparent)]
174 Persistence(#[from] crate::persistence::PersistenceError),
175
176 #[error(transparent)]
177 BackupPlan(#[from] crate::plan::BackupPlanError),
178
179 #[error(transparent)]
180 ExecutionJournal(#[from] crate::execution::BackupExecutionJournalError),
181
182 #[error(transparent)]
183 Journal(#[from] crate::journal::JournalValidationError),
184
185 #[error(transparent)]
186 Checksum(#[from] crate::artifacts::ArtifactChecksumError),
187
188 #[error(transparent)]
189 Manifest(#[from] crate::manifest::ManifestValidationError),
190}
191
192impl From<JournalLockError> for BackupRunnerError {
193 fn from(error: JournalLockError) -> Self {
194 match error {
195 JournalLockError::Locked { lock_path } => Self::JournalLocked { lock_path },
196 JournalLockError::UnsafeEntry { lock_path, kind } => {
197 Self::JournalLockUnsafeEntry { lock_path, kind }
198 }
199 JournalLockError::Io(error) => Self::Io(error),
200 }
201 }
202}
203
204#[derive(Clone, Debug, Serialize)]
209pub struct BackupRunResponse {
210 pub run_id: String,
211 pub plan_id: String,
212 pub backup_id: String,
213 pub complete: bool,
214 pub max_steps_reached: bool,
215 pub executed_operation_count: usize,
216 pub executed_operations: Vec<BackupRunExecutedOperation>,
217 pub execution: BackupExecutionResumeSummary,
218}
219
220#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
225pub struct BackupRunExecutedOperation {
226 pub sequence: usize,
227 pub operation_id: String,
228 pub kind: String,
229 pub target_canister_id: Option<String>,
230 pub outcome: String,
231}
232
233impl BackupRunExecutedOperation {
234 pub(super) fn completed(operation: &BackupExecutionJournalOperation) -> Self {
235 Self::from_operation(operation, "completed")
236 }
237
238 pub(super) fn failed(operation: &BackupExecutionJournalOperation) -> Self {
239 Self::from_operation(operation, "failed")
240 }
241
242 fn from_operation(operation: &BackupExecutionJournalOperation, outcome: &str) -> Self {
243 Self {
244 sequence: operation.sequence,
245 operation_id: operation.operation_id.clone(),
246 kind: format!("{:?}", operation.kind),
247 target_canister_id: operation.target_canister_id.clone(),
248 outcome: outcome.to_string(),
249 }
250 }
251}