system_update_committer/metadata/
policy.rs1use super::configuration_without_recovery::ConfigurationWithoutRecovery;
6use super::errors::{BootManagerError, BootManagerResultExt, PolicyError};
7use fidl_fuchsia_paver as paver;
8use log::{info, warn};
9use zx::Status;
10
11#[derive(Debug)]
14pub struct PolicyEngine(State);
15
16#[derive(Debug)]
17enum State {
18 NoOp,
23 Active {
24 current_config: ConfigurationWithoutRecovery,
25 boot_attempts: Option<u8>,
27 },
28}
29
30impl PolicyEngine {
31 pub async fn build(boot_manager: &paver::BootManagerProxy) -> Result<Self, PolicyError> {
33 let current_config = match boot_manager
34 .query_current_configuration()
35 .await
36 .into_boot_manager_result("query_current_configuration")
37 {
38 Err(BootManagerError::Fidl {
39 error: fidl::Error::ClientChannelClosed { status: Status::NOT_SUPPORTED, .. },
40 ..
41 }) => {
42 info!("ABR not supported: skipping health verification and boot metadata updates");
43 return Ok(Self(State::NoOp));
44 }
45 Err(e) => return Err(PolicyError::Build(e)),
46 Ok(paver::Configuration::Recovery) => {
47 info!("System in recovery: skipping health verification and boot metadata updates");
48 return Ok(Self(State::NoOp));
49 }
50 Ok(paver::Configuration::A) => ConfigurationWithoutRecovery::A,
51 Ok(paver::Configuration::B) => ConfigurationWithoutRecovery::B,
52 };
53
54 let status_and_boot_attempts = boot_manager
55 .query_configuration_status_and_boot_attempts((¤t_config).into())
56 .await
57 .into_boot_manager_result("query_configuration_status")
58 .map_err(PolicyError::Build)?;
59 match status_and_boot_attempts
60 .status
61 .ok_or(PolicyError::Build(BootManagerError::StatusNotSet))?
62 {
63 paver::ConfigurationStatus::Healthy => {
64 return Ok(Self(State::NoOp));
65 }
66 paver::ConfigurationStatus::Pending => {}
67 paver::ConfigurationStatus::Unbootable => {
68 return Err(PolicyError::CurrentConfigurationUnbootable(
69 (¤t_config).into(),
70 status_and_boot_attempts.unbootable_reason,
71 ));
72 }
73 };
74
75 let boot_attempts = status_and_boot_attempts.boot_attempts;
76 if boot_attempts.is_none() {
77 warn!("Current config status is pending but boot attempts was not set");
78 }
79
80 Ok(Self(State::Active { current_config, boot_attempts }))
81 }
82
83 pub fn should_verify_and_commit(&self) -> Option<(&ConfigurationWithoutRecovery, Option<u8>)> {
88 match &self.0 {
89 State::Active { current_config, boot_attempts } => {
90 Some((current_config, *boot_attempts))
91 }
92 State::NoOp => None,
93 }
94 }
95}
96
97#[cfg(test)]
98mod tests {
99 use super::*;
100 use assert_matches::assert_matches;
101 use fuchsia_async as fasync;
102 use mock_paver::{MockPaverServiceBuilder, PaverEvent, hooks as mphooks};
103 use std::sync::Arc;
104
105 #[fasync::run_singlethreaded(test)]
107 async fn test_skip_when_device_in_recovery() {
108 let paver = Arc::new(
109 MockPaverServiceBuilder::new()
110 .current_config(paver::Configuration::Recovery)
111 .insert_hook(mphooks::config_status_and_boot_attempts(|_| {
112 Ok((paver::ConfigurationStatus::Healthy, None))
113 }))
114 .build(),
115 );
116 let engine = PolicyEngine::build(&paver.spawn_boot_manager_service()).await.unwrap();
117
118 assert_eq!(engine.should_verify_and_commit(), None);
119
120 assert_eq!(paver.take_events(), vec![PaverEvent::QueryCurrentConfiguration]);
121 }
122
123 #[fasync::run_singlethreaded(test)]
125 async fn test_skip_when_device_does_not_support_abr() {
126 let paver = Arc::new(
127 MockPaverServiceBuilder::new()
128 .boot_manager_close_with_epitaph(Status::NOT_SUPPORTED)
129 .build(),
130 );
131 let engine = PolicyEngine::build(&paver.spawn_boot_manager_service()).await.unwrap();
132
133 assert_eq!(engine.should_verify_and_commit(), None);
134
135 assert_eq!(paver.take_events(), vec![]);
136 }
137
138 async fn test_skip_when_current_is_healthy(current_config: paver::Configuration) {
140 let paver = Arc::new(
141 MockPaverServiceBuilder::new()
142 .current_config(current_config)
143 .insert_hook(mphooks::config_status_and_boot_attempts(|_| {
144 Ok((paver::ConfigurationStatus::Healthy, None))
145 }))
146 .build(),
147 );
148 let engine = PolicyEngine::build(&paver.spawn_boot_manager_service()).await.unwrap();
149
150 assert_eq!(engine.should_verify_and_commit(), None);
151
152 assert_eq!(
153 paver.take_events(),
154 vec![
155 PaverEvent::QueryCurrentConfiguration,
156 PaverEvent::QueryConfigurationStatusAndBootAttempts {
157 configuration: current_config
158 },
159 ]
160 );
161 }
162
163 #[fasync::run_singlethreaded(test)]
164 async fn test_skip_when_current_is_healthy_a() {
165 test_skip_when_current_is_healthy(paver::Configuration::A).await
166 }
167
168 #[fasync::run_singlethreaded(test)]
169 async fn test_skip_when_current_is_healthy_b() {
170 test_skip_when_current_is_healthy(paver::Configuration::B).await
171 }
172
173 async fn test_verify_and_commit_when_current_is_pending(
175 current_config: &ConfigurationWithoutRecovery,
176 ) {
177 let paver = Arc::new(
178 MockPaverServiceBuilder::new()
179 .current_config(current_config.into())
180 .insert_hook(mphooks::config_status_and_boot_attempts(|_| {
181 Ok((paver::ConfigurationStatus::Pending, Some(1)))
182 }))
183 .build(),
184 );
185 let engine = PolicyEngine::build(&paver.spawn_boot_manager_service()).await.unwrap();
186
187 assert_eq!(engine.should_verify_and_commit(), Some((current_config, Some(1))));
188
189 assert_eq!(
190 paver.take_events(),
191 vec![
192 PaverEvent::QueryCurrentConfiguration,
193 PaverEvent::QueryConfigurationStatusAndBootAttempts {
194 configuration: current_config.into()
195 },
196 ]
197 );
198 }
199
200 #[fasync::run_singlethreaded(test)]
201 async fn test_verify_and_commit_when_current_is_pending_a() {
202 test_verify_and_commit_when_current_is_pending(&ConfigurationWithoutRecovery::A).await
203 }
204
205 #[fasync::run_singlethreaded(test)]
206 async fn test_verify_and_commit_when_current_is_pending_b() {
207 test_verify_and_commit_when_current_is_pending(&ConfigurationWithoutRecovery::B).await
208 }
209
210 async fn test_returns_error_when_current_unbootable(
212 current_config: &ConfigurationWithoutRecovery,
213 ) {
214 let paver = Arc::new(
215 MockPaverServiceBuilder::new()
216 .current_config(current_config.into())
217 .insert_hook(mphooks::config_status_and_boot_attempts(|_| {
218 Ok((paver::ConfigurationStatus::Unbootable, None))
219 }))
220 .build(),
221 );
222
223 assert_matches!(
224 PolicyEngine::build(&paver.spawn_boot_manager_service()).await,
225 Err(PolicyError::CurrentConfigurationUnbootable(cc, _)) if cc == current_config.into()
226 );
227
228 assert_eq!(
229 paver.take_events(),
230 vec![
231 PaverEvent::QueryCurrentConfiguration,
232 PaverEvent::QueryConfigurationStatusAndBootAttempts {
233 configuration: current_config.into()
234 },
235 ]
236 );
237 }
238
239 #[fasync::run_singlethreaded(test)]
240 async fn test_returns_error_when_current_unbootable_a() {
241 test_returns_error_when_current_unbootable(&ConfigurationWithoutRecovery::A).await
242 }
243
244 #[fasync::run_singlethreaded(test)]
245 async fn test_returns_error_when_current_unbootable_b() {
246 test_returns_error_when_current_unbootable(&ConfigurationWithoutRecovery::B).await
247 }
248
249 #[fasync::run_singlethreaded(test)]
251 async fn test_build_fails_when_paver_fails() {
252 let paver = Arc::new(
253 MockPaverServiceBuilder::new()
254 .insert_hook(mphooks::return_error(|e| match e {
255 PaverEvent::QueryCurrentConfiguration => Status::OUT_OF_RANGE,
256 _ => Status::OK,
257 }))
258 .build(),
259 );
260
261 assert_matches!(
262 PolicyEngine::build(&paver.spawn_boot_manager_service()).await,
263 Err(PolicyError::Build(BootManagerError::Status {
264 method_name: "query_current_configuration",
265 status: Status::OUT_OF_RANGE
266 }))
267 );
268
269 assert_eq!(paver.take_events(), vec![PaverEvent::QueryCurrentConfiguration]);
270 }
271}