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}