ic_backup/policy/consistency/
mod.rs1use crate::model::{
4 artifacts::ArtifactChecksumRecord,
5 consistency::{
6 ApplicationFenceEvidence, ApplicationFenceState, CaptureState, ConsistencyEvidence,
7 ConsistencyGuaranteeRecord, ConsistencyObservation, ConsistencyRequest,
8 TargetCaptureEvidence,
9 },
10};
11use thiserror::Error;
12
13#[derive(Clone, Debug)]
15pub struct ConsistencyView<'a> {
16 request: ArtifactChecksumRecord,
17 requirement: ArtifactChecksumRecord,
18 observation: &'a ConsistencyObservation,
19}
20impl ConsistencyView<'_> {
21 #[must_use]
23 pub const fn request(&self) -> &ArtifactChecksumRecord {
24 &self.request
25 }
26 #[must_use]
28 pub const fn requirement(&self) -> &ArtifactChecksumRecord {
29 &self.requirement
30 }
31 #[must_use]
33 pub fn targets(&self) -> &[TargetCaptureEvidence] {
34 self.observation.targets()
35 }
36 #[must_use]
38 pub fn fence(&self) -> Option<&ApplicationFenceEvidence> {
39 match self.observation.consistency() {
40 ConsistencyEvidence::PerCanister => None,
41 ConsistencyEvidence::ApplicationCoordinated(fence) => Some(fence),
42 }
43 }
44 #[must_use]
46 pub const fn evidence(&self) -> &ArtifactChecksumRecord {
47 self.observation.evidence()
48 }
49 #[must_use]
51 pub const fn remote_observations(&self) -> u32 {
52 self.observation.remote_observations()
53 }
54}
55pub fn validate<'a>(
65 request: &ConsistencyRequest<'_>,
66 observation: &'a ConsistencyObservation,
67) -> Result<ConsistencyView<'a>, ConsistencyError> {
68 let digest = request.digest();
69 if *observation.request() != digest {
70 return Err(ConsistencyError::RequestMismatch);
71 }
72 let binding = request.binding();
73 for (field, expected, actual) in [
74 (
75 "network",
76 binding.network(),
77 observation.context().network(),
78 ),
79 ("caller", binding.caller(), observation.context().caller()),
80 (
81 "release",
82 binding.release(),
83 observation.context().release(),
84 ),
85 ] {
86 if actual != expected {
87 return Err(ConsistencyError::ContextMismatch(field));
88 }
89 }
90 if observation.inventory() != request.inventory() {
91 return Err(ConsistencyError::InventoryMismatch);
92 }
93 if !observation
94 .targets()
95 .iter()
96 .map(|target| &target.target)
97 .eq(request.selected_targets())
98 {
99 return Err(ConsistencyError::SelectionMismatch);
100 }
101 if observation.remote_observations() > request.max_remote_observations() {
102 return Err(ConsistencyError::ObservationLimitExceeded {
103 limit: request.max_remote_observations(),
104 reported: observation.remote_observations(),
105 });
106 }
107 if observation
108 .targets()
109 .iter()
110 .any(|target| target.state != CaptureState::Stopped)
111 {
112 return Err(ConsistencyError::TargetNotStopped);
113 }
114 match (request.requirement().guarantee(), observation.consistency()) {
115 (ConsistencyGuaranteeRecord::PerCanister, ConsistencyEvidence::PerCanister) => {}
116 (
117 ConsistencyGuaranteeRecord::ApplicationCoordinated,
118 ConsistencyEvidence::ApplicationCoordinated(fence),
119 ) => {
120 let expected = request
121 .expected_fence()
122 .ok_or(ConsistencyError::FenceMismatch)?;
123 if fence.identity != expected.identity {
124 return Err(ConsistencyError::FenceMismatch);
125 }
126 if fence.state != ApplicationFenceState::Active {
127 return Err(ConsistencyError::FenceNotActive);
128 }
129 let original_revision_matches =
130 observation.membership_revision() == Some(&expected.membership_revision);
131 let fence_revision_matches = fence.membership_revision == expected.membership_revision;
132 if !original_revision_matches || !fence_revision_matches {
133 return Err(ConsistencyError::MembershipRevisionMismatch);
134 }
135 }
136 _ => return Err(ConsistencyError::GuaranteeMismatch),
137 }
138 Ok(ConsistencyView {
139 request: digest,
140 requirement: request.requirement().digest(),
141 observation,
142 })
143}
144#[derive(Debug, Eq, Error, PartialEq)]
146pub enum ConsistencyError {
147 #[error("consistency request mismatch")]
149 RequestMismatch,
150 #[error("consistency observed {0} mismatch")]
152 ContextMismatch(&'static str),
153 #[error("consistency inventory mismatch")]
155 InventoryMismatch,
156 #[error("consistency selected targets mismatch")]
158 SelectionMismatch,
159 #[error("consistency evidence guarantee mismatch")]
161 GuaranteeMismatch,
162 #[error("consistency target is not stopped")]
164 TargetNotStopped,
165 #[error("consistency fence identity mismatch")]
167 FenceMismatch,
168 #[error("consistency fence is not active")]
170 FenceNotActive,
171 #[error("consistency fence membership revision mismatch")]
173 MembershipRevisionMismatch,
174 #[error("consistency reports {reported} observations above ceiling {limit}")]
176 ObservationLimitExceeded {
177 limit: u32,
179 reported: u32,
181 },
182}
183
184#[cfg(test)]
185mod tests;