1use super::{
2 BackupOperation, BackupOperationKind, BackupPlan, BackupPlanError, BackupScopeKind,
3 BackupTarget, ControlAuthority, ControlAuthoritySource, QuiescencePolicy,
4 SnapshotReadAuthority,
5};
6use crate::{
7 discovery::{SnapshotTarget, targets_from_registry},
8 manifest::IdentityMode,
9 registry::RegistryEntry,
10};
11use candid::Principal;
12use std::{
13 collections::{BTreeMap, BTreeSet},
14 str::FromStr,
15};
16
17pub struct BackupPlanBuildInput<'a> {
22 pub plan_id: String,
23 pub run_id: String,
24 pub fleet: String,
25 pub network: String,
26 pub root_canister_id: String,
27 pub selected_canister_id: Option<String>,
28 pub selected_scope_kind: BackupScopeKind,
29 pub include_descendants: bool,
30 pub topology_hash_before_quiesce: String,
31 pub registry: &'a [RegistryEntry],
32 pub control_authority: ControlAuthority,
33 pub snapshot_read_authority: SnapshotReadAuthority,
34 pub quiescence_policy: QuiescencePolicy,
35 pub identity_mode: IdentityMode,
36}
37
38pub fn build_backup_plan(input: BackupPlanBuildInput<'_>) -> Result<BackupPlan, BackupPlanError> {
40 let snapshot_read_authority = input.snapshot_read_authority.clone();
41 let quiescence_policy = input.quiescence_policy.clone();
42 let root_included = input.selected_scope_kind == BackupScopeKind::MaintenanceRoot;
43 let selected_subtree_root = selected_subtree_root(&input)?;
44 let snapshot_targets = snapshot_targets(&input)?;
45 let target_depths = target_depths(input.registry);
46 let targets = snapshot_targets
47 .into_iter()
48 .map(|target| {
49 backup_target(
50 target,
51 &target_depths,
52 input.control_authority.clone(),
53 input.snapshot_read_authority.clone(),
54 input.identity_mode.clone(),
55 )
56 })
57 .collect::<Vec<_>>();
58 let phases = build_backup_phases(&targets);
59
60 let plan = BackupPlan {
61 plan_id: input.plan_id,
62 run_id: input.run_id,
63 fleet: input.fleet,
64 network: input.network,
65 root_canister_id: input.root_canister_id,
66 selected_subtree_root,
67 selected_scope_kind: input.selected_scope_kind,
68 include_descendants: input.include_descendants,
69 root_included,
70 requires_root_controller: input.control_authority.source
71 == ControlAuthoritySource::RootController,
72 snapshot_read_authority,
73 quiescence_policy,
74 topology_hash_before_quiesce: input.topology_hash_before_quiesce,
75 targets,
76 phases,
77 };
78 plan.validate()?;
79 Ok(plan)
80}
81
82pub fn resolve_backup_selector(
84 registry: &[RegistryEntry],
85 selector: &str,
86) -> Result<String, BackupPlanError> {
87 validate_nonempty("selector", selector)?;
88 if Principal::from_str(selector).is_ok() {
89 return registry
90 .iter()
91 .find(|entry| entry.pid == selector)
92 .map(|entry| entry.pid.clone())
93 .ok_or_else(|| BackupPlanError::UnknownSelector(selector.to_string()));
94 }
95
96 let matches = registry
97 .iter()
98 .filter(|entry| entry.role.as_deref() == Some(selector))
99 .map(|entry| entry.pid.clone())
100 .collect::<Vec<_>>();
101 match matches.as_slice() {
102 [canister] => Ok(canister.clone()),
103 [] => Err(BackupPlanError::UnknownSelector(selector.to_string())),
104 _ => Err(BackupPlanError::AmbiguousSelector {
105 selector: selector.to_string(),
106 matches,
107 }),
108 }
109}
110
111fn selected_subtree_root(
112 input: &BackupPlanBuildInput<'_>,
113) -> Result<Option<String>, BackupPlanError> {
114 match input.selected_scope_kind {
115 BackupScopeKind::NonRootFleet => Ok(None),
116 BackupScopeKind::Member | BackupScopeKind::Subtree | BackupScopeKind::MaintenanceRoot => {
117 input
118 .selected_canister_id
119 .clone()
120 .ok_or(BackupPlanError::EmptyField("selected_canister_id"))
121 .map(Some)
122 }
123 }
124}
125
126fn snapshot_targets(
127 input: &BackupPlanBuildInput<'_>,
128) -> Result<Vec<SnapshotTarget>, BackupPlanError> {
129 match input.selected_scope_kind {
130 BackupScopeKind::Member | BackupScopeKind::Subtree | BackupScopeKind::MaintenanceRoot => {
131 let selected = input
132 .selected_canister_id
133 .as_deref()
134 .ok_or(BackupPlanError::EmptyField("selected_canister_id"))?;
135 let recursive = input.selected_scope_kind == BackupScopeKind::Subtree
136 || input.include_descendants
137 || input.selected_scope_kind == BackupScopeKind::MaintenanceRoot;
138 targets_from_registry(input.registry, selected, recursive)
139 .map_err(BackupPlanError::from)
140 }
141 BackupScopeKind::NonRootFleet => Ok(input
142 .registry
143 .iter()
144 .filter(|entry| entry.pid != input.root_canister_id)
145 .map(|entry| SnapshotTarget {
146 canister_id: entry.pid.clone(),
147 role: entry.role.clone(),
148 parent_canister_id: entry.parent_pid.clone(),
149 module_hash: entry.module_hash.clone(),
150 })
151 .collect()),
152 }
153}
154
155fn backup_target(
156 target: SnapshotTarget,
157 target_depths: &BTreeMap<String, u32>,
158 control_authority: ControlAuthority,
159 snapshot_read_authority: SnapshotReadAuthority,
160 identity_mode: IdentityMode,
161) -> BackupTarget {
162 BackupTarget {
163 depth: target_depths
164 .get(&target.canister_id)
165 .copied()
166 .unwrap_or_default(),
167 canister_id: target.canister_id,
168 role: target.role,
169 parent_canister_id: target.parent_canister_id,
170 control_authority,
171 snapshot_read_authority,
172 identity_mode,
173 expected_module_hash: target.module_hash,
174 }
175}
176
177fn target_depths(registry: &[RegistryEntry]) -> BTreeMap<String, u32> {
178 let parents = registry
179 .iter()
180 .map(|entry| (entry.pid.as_str(), entry.parent_pid.as_deref()))
181 .collect::<BTreeMap<_, _>>();
182 registry
183 .iter()
184 .map(|entry| {
185 (
186 entry.pid.clone(),
187 target_depth(entry.pid.as_str(), &parents),
188 )
189 })
190 .collect()
191}
192
193fn target_depth(canister_id: &str, parents: &BTreeMap<&str, Option<&str>>) -> u32 {
194 let mut depth = 0;
195 let mut current = canister_id;
196 let mut seen = BTreeSet::new();
197
198 while let Some(Some(parent)) = parents.get(current) {
199 if !seen.insert(current) {
200 break;
201 }
202 depth += 1;
203 current = parent;
204 }
205
206 depth
207}
208
209fn build_backup_phases(targets: &[BackupTarget]) -> Vec<BackupOperation> {
210 let mut phases = vec![
211 operation(
212 "validate-topology",
213 BackupOperationKind::ValidateTopology,
214 None,
215 ),
216 operation(
217 "validate-control-authority",
218 BackupOperationKind::ValidateControlAuthority,
219 None,
220 ),
221 operation(
222 "validate-snapshot-read-authority",
223 BackupOperationKind::ValidateSnapshotReadAuthority,
224 None,
225 ),
226 operation(
227 "validate-quiescence-policy",
228 BackupOperationKind::ValidateQuiescencePolicy,
229 None,
230 ),
231 ];
232
233 let mut top_down = targets.iter().collect::<Vec<_>>();
234 top_down.sort_by(|left, right| {
235 left.depth
236 .cmp(&right.depth)
237 .then_with(|| left.canister_id.cmp(&right.canister_id))
238 });
239 for target in &top_down {
240 phases.push(operation(
241 format!("stop-{}", target.canister_id),
242 BackupOperationKind::Stop,
243 Some(target.canister_id.clone()),
244 ));
245 }
246 for target in &top_down {
247 phases.push(operation(
248 format!("snapshot-{}", target.canister_id),
249 BackupOperationKind::CreateSnapshot,
250 Some(target.canister_id.clone()),
251 ));
252 }
253
254 let mut bottom_up = top_down;
255 bottom_up.reverse();
256 for target in &bottom_up {
257 phases.push(operation(
258 format!("start-{}", target.canister_id),
259 BackupOperationKind::Start,
260 Some(target.canister_id.clone()),
261 ));
262 }
263
264 for target in targets {
265 phases.push(operation(
266 format!("download-{}", target.canister_id),
267 BackupOperationKind::DownloadSnapshot,
268 Some(target.canister_id.clone()),
269 ));
270 phases.push(operation(
271 format!("verify-{}", target.canister_id),
272 BackupOperationKind::VerifyArtifact,
273 Some(target.canister_id.clone()),
274 ));
275 }
276 phases.push(operation(
277 "finalize-manifest",
278 BackupOperationKind::FinalizeManifest,
279 None,
280 ));
281
282 phases
283 .into_iter()
284 .enumerate()
285 .map(|(index, mut phase)| {
286 phase.order = u32::try_from(index).unwrap_or(u32::MAX);
287 phase
288 })
289 .collect()
290}
291
292fn operation(
293 operation_id: impl Into<String>,
294 kind: BackupOperationKind,
295 target_canister_id: Option<String>,
296) -> BackupOperation {
297 BackupOperation {
298 operation_id: operation_id.into(),
299 order: 0,
300 kind,
301 target_canister_id,
302 }
303}
304
305fn validate_nonempty(field: &'static str, value: &str) -> Result<(), BackupPlanError> {
306 if value.trim().is_empty() {
307 Err(BackupPlanError::EmptyField(field))
308 } else {
309 Ok(())
310 }
311}