1mod installer;
8mod policy;
9use crate::omaha::installer::IsolatedInstaller;
10use anyhow::{Context, Error};
11use futures::lock::Mutex;
12use futures::prelude::*;
13use log::error;
14use omaha_client::app_set::VecAppSet;
15use omaha_client::common::App;
16use omaha_client::configuration::{Config, Updater};
17use omaha_client::cup_ecdsa::StandardCupv2Handler;
18use omaha_client::http_request::HttpRequest;
19use omaha_client::metrics::StubMetricsReporter;
20use omaha_client::protocol::Cohort;
21use omaha_client::protocol::request::OS;
22use omaha_client::state_machine::{
23 StateMachineBuilder, StateMachineEvent, UpdateCheckError, update_check,
24};
25use omaha_client::storage::MemStorage;
26use omaha_client::time::StandardTimeSource;
27use omaha_client::version::Version;
28use omaha_client_fuchsia::{http_request, timer};
29use std::rc::Rc;
30
31async fn get_omaha_config(version: &str, service_url: &str) -> Config {
33 Config {
34 updater: Updater { name: "Fuchsia".to_string(), version: Version::from([0, 0, 1, 0]) },
35
36 os: OS {
37 platform: "Fuchsia".to_string(),
38 version: version.to_string(),
39 service_pack: "".to_string(),
40 arch: std::env::consts::ARCH.to_string(),
41 },
42
43 service_url: service_url.to_owned(),
44 omaha_public_keys: None,
45 }
46}
47
48pub async fn install_update(
50 updater: crate::updater::Updater,
51 appid: String,
52 service_url: String,
53 current_version: String,
54 channel: String,
55) -> Result<(), Error> {
56 let version = match current_version.parse::<Version>() {
57 Ok(version) => version,
58 Err(e) => {
59 error!("Unable to parse '{}' as Omaha version format: {:?}", current_version, e);
60 Version::from([0])
61 }
62 };
63
64 let cohort = Cohort { hint: Some(channel.clone()), name: Some(channel), ..Cohort::default() };
65 let app_set =
66 VecAppSet::new(vec![App::builder().id(appid).version(version).cohort(cohort).build()]);
67
68 let config = get_omaha_config(¤t_version, &service_url).await;
69 install_update_with_http(updater, app_set, config, http_request::FuchsiaHyperHttpRequest::new())
70 .await
71 .context("installing update via http(s)")
72}
73
74#[derive(Debug, thiserror::Error)]
75enum InstallUpdateError {
76 #[error("expected exactly one UpdateCheckResult from Omaha, got {0:?}")]
77 WrongNumberOfUpdateCheckResults(Vec<Result<update_check::Response, UpdateCheckError>>),
78 #[error("expected exactly one app_response from Omaha, got {0:?}")]
79 WrongNumberOfAppResponses(Vec<omaha_client::state_machine::update_check::AppResponse>),
80 #[error("update check failed: {0}")]
81 UpdateCheckFailed(UpdateCheckError),
82 #[error("update check did not produce an update, took action {0:?}")]
83 DidNotUpdate(update_check::Action),
84}
85
86async fn install_update_with_http<HR>(
87 updater: crate::updater::Updater,
88 app_set: VecAppSet,
89 config: Config,
90 http_request: HR,
91) -> Result<(), InstallUpdateError>
92where
93 HR: HttpRequest,
94{
95 let storage = Rc::new(Mutex::new(MemStorage::new()));
96 let installer = IsolatedInstaller { updater };
97 let cup_handler = config.omaha_public_keys.as_ref().map(StandardCupv2Handler::new);
98 let state_machine = StateMachineBuilder::new(
99 policy::IsolatedPolicyEngine::new(StandardTimeSource),
100 http_request,
101 installer,
102 timer::FuchsiaTimer,
103 StubMetricsReporter,
104 storage,
105 config,
106 Rc::new(Mutex::new(app_set)),
107 cup_handler,
108 );
109
110 let stream: Vec<StateMachineEvent> = state_machine.oneshot_check().await.collect().await;
111
112 let filtered_events: Vec<Result<update_check::Response, UpdateCheckError>> = stream
116 .into_iter()
117 .filter_map(|p| match p {
118 StateMachineEvent::UpdateCheckResult(val) => Some(val),
119 _ => None,
120 })
121 .collect();
122
123 let [response]: [_; 1] =
125 filtered_events.try_into().map_err(InstallUpdateError::WrongNumberOfUpdateCheckResults)?;
126 let response = response.map_err(InstallUpdateError::UpdateCheckFailed)?;
127
128 let [app_response]: [_; 1] =
130 response.app_responses.try_into().map_err(InstallUpdateError::WrongNumberOfAppResponses)?;
131
132 match app_response.result {
134 update_check::Action::Updated => Ok(()),
135 other_action => Err(InstallUpdateError::DidNotUpdate(other_action)),
136 }
137}
138
139#[cfg(test)]
140mod tests {
141 use super::*;
142 use crate::updater::for_tests::{UpdaterBuilder, UpdaterForTest, UpdaterResult};
143 use fuchsia_pkg_testing::PackageBuilder;
144
145 use mock_paver::{PaverEvent, hooks as mphooks};
146 use omaha_client::http_request::mock::MockHttpRequest;
147 use serde_json::json;
148
149 const TEST_REPO_URL: &str = "fuchsia-pkg://example.com";
150
151 const TEST_VERSION: &str = "20200101.0.0";
152 const TEST_CHANNEL: &str = "test-channel";
153 const TEST_APP_ID: &str = "qnzHyt4n";
154
155 async fn run_omaha(
164 updater: UpdaterForTest,
165 app_set: VecAppSet,
166 config: Config,
167 mock_responses: Vec<serde_json::Value>,
168 ) -> Result<UpdaterResult, Error> {
169 let mut http = MockHttpRequest::empty();
170
171 for response in mock_responses {
172 let response = serde_json::to_vec(&response).unwrap();
173 http.add_response(hyper::Response::new(response));
174 }
175
176 install_update_with_http(updater.updater, app_set, config, http)
177 .await
178 .context("Running omaha client")?;
179
180 Ok(UpdaterResult {
181 paver_events: updater.paver.take_events(),
182 expected_blobfs_contents: updater.expected_blobfs_contents,
183 resolver: updater.resolver,
184 realm_instance: updater.realm_instance,
185 })
186 }
187
188 fn get_test_app_set() -> VecAppSet {
189 VecAppSet::new(vec![
190 App::builder()
191 .id(TEST_APP_ID.to_owned())
192 .version([20200101, 0, 0, 0])
193 .cohort(Cohort::new(TEST_CHANNEL))
194 .build(),
195 ])
196 }
197
198 fn get_test_config() -> Config {
199 Config {
200 updater: Updater { name: "Fuchsia".to_owned(), version: Version::from([0, 0, 1, 0]) },
201 os: OS {
202 platform: "Fuchsia".to_owned(),
203 version: TEST_VERSION.to_owned(),
204 service_pack: "".to_owned(),
205 arch: std::env::consts::ARCH.to_owned(),
206 },
207
208 service_url: "http://example.com".to_owned(),
210 omaha_public_keys: None,
211 }
212 }
213
214 fn get_test_response(package_path: &str) -> Vec<serde_json::Value> {
215 let update_response = json!({"response":{
216 "server": "prod",
217 "protocol": "3.0",
218 "app": [{
219 "appid": TEST_APP_ID,
220 "status": "ok",
221 "updatecheck": {
222 "status": "ok",
223 "urls": { "url": [{ "codebase": format!("{TEST_REPO_URL}/") }] },
224 "manifest": {
225 "version": "20200101.1.0.0",
226 "actions": {
227 "action": [
228 {
229 "run": package_path,
230 "event": "install"
231 },
232 {
233 "event": "postinstall"
234 }
235 ]
236 },
237 "packages": {
238 "package": [
239 {
240 "name": package_path,
241 "fp": "2.20200101.1.0.0",
242 "required": true,
243 }
244 ]
245 }
246 }
247 }
248 }],
249 }});
250
251 let event_response = json!({"response":{
252 "server": "prod",
253 "protocol": "3.0",
254 "app": [{
255 "appid": TEST_APP_ID,
256 "status": "ok",
257 }]
258 }});
259
260 let response =
261 vec![update_response, event_response.clone(), event_response.clone(), event_response];
262 response
263 }
264
265 async fn build_updater() -> Result<UpdaterForTest, Error> {
267 let data = "hello world!".as_bytes();
268 let hook = |p: &PaverEvent| {
269 if let PaverEvent::QueryActiveConfiguration = p {
270 return zx::Status::NOT_SUPPORTED;
271 }
272 zx::Status::OK
273 };
274 let test_package = PackageBuilder::new("test_package")
275 .add_resource_at("bin/hello", "this is a test".as_bytes())
276 .add_resource_at("data/file", "this is a file".as_bytes())
277 .add_resource_at("meta/test_package.cm", "{}".as_bytes())
278 .build()
279 .await
280 .context("Building test_package")?;
281 let updater = UpdaterBuilder::new()
282 .await
283 .paver(|p| p.insert_hook(mphooks::return_error(hook)))
284 .repo_url(TEST_REPO_URL)
285 .add_package(test_package)
286 .fuchsia_image(data.to_vec(), Some(data.to_vec()))
287 .recovery_image(data.to_vec(), Some(data.to_vec()));
288 Ok(updater.build().await)
289 }
290
291 #[fuchsia::test]
292 pub async fn test_omaha_update() -> Result<(), Error> {
293 let updater = build_updater().await.context("Building updater")?;
294 let package_path = format!("update?hash={}", updater.update_merkle_root);
295 let app_set = get_test_app_set();
296 let config = get_test_config();
297 let response = get_test_response(&package_path);
298 let result =
299 run_omaha(updater, app_set, config, response).await.context("running omaha")?;
300
301 let () = result.verify_packages().await;
302 Ok(())
303 }
304
305 #[fuchsia::test]
306 pub async fn test_omaha_updater_reports_failure() -> Result<(), Error> {
307 let app_set = get_test_app_set();
308 let config = get_test_config();
309 let updater = build_updater().await.context("Building updater")?;
310
311 let response = vec![];
313
314 let result = run_omaha(updater, app_set, config, response).await;
315 assert!(result.is_err());
316 Ok(())
317 }
318
319 async fn build_updater_with_broken_paver() -> UpdaterForTest {
320 let hook = |_p: &PaverEvent| zx::Status::INTERNAL;
322
323 let updater = UpdaterBuilder::new()
324 .await
325 .paver(|p| p.insert_hook(mphooks::return_error(hook)))
326 .repo_url(TEST_REPO_URL)
327 .fuchsia_image(b"zbi-contents".to_vec(), None);
328
329 updater.build().await
330 }
331
332 #[fuchsia::test]
333 pub async fn test_installation_error_reports_failure() -> Result<(), Error> {
334 let app_set = get_test_app_set();
335 let config = get_test_config();
336 let updater = build_updater_with_broken_paver().await;
337 let package_path = format!("update?hash={}", updater.update_merkle_root);
338 let response = get_test_response(&package_path);
339
340 let result = run_omaha(updater, app_set, config, response).await;
341 assert!(result.is_err());
342 Ok(())
343 }
344}