installer/
lib.rs

1// Copyright 2022 The Fuchsia Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5//! # Installer library
6//!
7//! The installer library exposes functionality to install Fuchsia to a disk by copying images from
8//! one block device to another. The primary use case is an installer USB, which contains the images
9//! to write to persistent storage.
10
11pub mod partition;
12
13use anyhow::{Context, Error, anyhow};
14use fidl::endpoints::{Proxy, ServerEnd};
15use fidl_fuchsia_hardware_power_statecontrol::{
16    AdminMarker, ShutdownAction, ShutdownOptions, ShutdownReason,
17};
18use fidl_fuchsia_paver::{
19    BootManagerMarker, Configuration, DynamicDataSinkProxy, PaverMarker, PaverProxy,
20};
21use fidl_fuchsia_sysinfo::SysInfoMarker;
22use fuchsia_component::client;
23
24use fuchsia_sync::Mutex;
25use partition::Partition;
26use recovery_util_block::BlockDevice;
27
28#[derive(Clone, Copy, Debug, PartialEq)]
29pub enum BootloaderType {
30    Efi,
31    Coreboot,
32}
33
34#[derive(Clone, Debug, PartialEq)]
35pub struct InstallationPaths {
36    pub install_source: Option<BlockDevice>,
37    pub install_target: Option<BlockDevice>,
38    pub bootloader_type: Option<BootloaderType>,
39    pub install_destinations: Vec<BlockDevice>,
40    pub available_disks: Vec<BlockDevice>,
41}
42
43impl InstallationPaths {
44    pub fn new() -> InstallationPaths {
45        InstallationPaths {
46            install_source: None,
47            install_target: None,
48            bootloader_type: None,
49            install_destinations: Vec::new(),
50            available_disks: Vec::new(),
51        }
52    }
53}
54
55pub async fn find_install_source(
56    block_devices: &Vec<BlockDevice>,
57    bootloader: BootloaderType,
58) -> Result<&BlockDevice, Error> {
59    let mut candidate = Err(anyhow!("Could not find the installer disk. Is it plugged in?"));
60    for device in block_devices.iter().filter(|d| d.is_disk()) {
61        // get_partitions returns an empty vector if it doesn't find any partitions
62        // with the workstation-installer GUID on the disk.
63        let partitions = Partition::get_partitions(device, block_devices, bootloader).await?;
64        if !partitions.is_empty() {
65            if candidate.is_err() {
66                candidate = Ok(device);
67            } else {
68                return Err(anyhow!(
69                    "Please check you only have one installation disk plugged in!"
70                ));
71            }
72        }
73    }
74    candidate
75}
76
77fn paver_connect() -> Result<(PaverProxy, DynamicDataSinkProxy), Error> {
78    let (data_sink_chan, data_remote) = zx::Channel::create();
79    let paver: PaverProxy =
80        client::connect_to_protocol::<PaverMarker>().context("Could not connect to paver")?;
81    paver.find_partition_table_manager(ServerEnd::from(data_remote))?;
82
83    let data_sink =
84        DynamicDataSinkProxy::from_channel(fidl::AsyncChannel::from_channel(data_sink_chan));
85    Ok((paver, data_sink))
86}
87
88pub async fn get_bootloader_type() -> Result<BootloaderType, Error> {
89    let proxy = fuchsia_component::client::connect_to_protocol::<SysInfoMarker>()
90        .context("Could not connect to 'fuchsia.sysinfo.SysInfo' service")?;
91    let (status, bootloader) =
92        proxy.get_bootloader_vendor().await.context("Getting bootloader vendor")?;
93    if let Some(bootloader) = bootloader {
94        log::info!("Bootloader vendor = {}", bootloader);
95        if bootloader == "coreboot" {
96            Ok(BootloaderType::Coreboot)
97        } else {
98            // The installer only supports coreboot and EFI,
99            // and EFI BIOS vendor depends on the manufacturer,
100            // so we assume that non-coreboot bootloader vendors
101            // mean EFI.
102            Ok(BootloaderType::Efi)
103        }
104    } else {
105        Err(Error::new(zx::Status::from_raw(status)))
106    }
107}
108
109/// Restart the machine.
110pub async fn restart() {
111    let proxy = fuchsia_component::client::connect_to_protocol::<AdminMarker>()
112        .expect("Could not connect to 'fuchsia.hardware.power.statecontrol.Admin' service");
113
114    proxy
115        .shutdown(&ShutdownOptions {
116            action: Some(ShutdownAction::Reboot),
117            reasons: Some(vec![ShutdownReason::DeveloperRequest]),
118            ..Default::default()
119        })
120        .await
121        .expect("Failed to reboot")
122        .expect("Failed to reboot");
123}
124
125/// Set the active boot configuration for the newly-installed system. We always boot from the "A"
126/// slot to start with.
127async fn set_active_configuration(paver: &PaverProxy) -> Result<(), Error> {
128    let (boot_manager, server) = fidl::endpoints::create_proxy::<BootManagerMarker>();
129
130    paver.find_boot_manager(server).context("Could not find boot manager")?;
131
132    zx::Status::ok(
133        boot_manager
134            .set_configuration_active(Configuration::A)
135            .await
136            .context("Sending set configuration active")?,
137    )
138    .context("Setting active configuration")?;
139
140    zx::Status::ok(boot_manager.flush().await.context("Sending boot manager flush")?)
141        .context("Flushing active configuration")
142}
143
144pub async fn do_install<F>(
145    installation_paths: InstallationPaths,
146    progress_callback: &F,
147) -> Result<(), Error>
148where
149    F: Send + Sync + Fn(String),
150{
151    if installation_paths.install_target.is_some() {
152        log::warn!("Ignoring install target; this will be removed in the future.");
153    }
154    let install_source =
155        installation_paths.install_source.ok_or_else(|| anyhow!("No installation source?"))?;
156    let bootloader_type = installation_paths.bootloader_type.unwrap();
157
158    let (paver, data_sink) = paver_connect().context("Could not contact paver")?;
159
160    log::info!("Initializing Fuchsia partition tables...");
161    progress_callback(String::from("Initializing Fuchsia partition tables..."));
162    data_sink.initialize_partition_tables().await?;
163
164    log::info!("Getting source partitions");
165    progress_callback(String::from("Getting source partitions"));
166    let to_install = Partition::get_partitions(
167        &install_source,
168        &installation_paths.available_disks,
169        bootloader_type,
170    )
171    .await
172    .context("Getting source partitions")?;
173
174    let num_partitions = to_install.len();
175    let mut current_partition = 1;
176    for part in to_install {
177        progress_callback(String::from(format!(
178            "paving partition {} of {}",
179            current_partition, num_partitions
180        )));
181
182        let prev_percent = Mutex::new(0 as i64);
183
184        let pave_progress_callback = |data_read, data_total| {
185            if data_total == 0 {
186                return;
187            }
188            let cur_percent: i64 =
189                unsafe { (((data_read as f64) / (data_total as f64)) * 100.0).to_int_unchecked() };
190            let mut prev = prev_percent.lock();
191            if cur_percent == *prev {
192                return;
193            }
194            *prev = cur_percent;
195
196            progress_callback(String::from(format!(
197                "paving partition {} of {}: {}%",
198                current_partition, num_partitions, cur_percent
199            )));
200        };
201
202        log::info!("Paving partition: {:?}", part);
203        part.pave(&data_sink, &pave_progress_callback).await?;
204        if part.is_ab() {
205            log::info!("Paving partition: {:?} [-B]", part);
206            part.pave_b(&data_sink).await?
207        }
208
209        current_partition += 1;
210    }
211
212    progress_callback(String::from("Flushing Partitions"));
213    zx::Status::ok(data_sink.flush().await.context("Sending flush")?)
214        .context("Flushing partitions")?;
215
216    progress_callback(String::from("Setting active configuration for the new system"));
217    set_active_configuration(&paver)
218        .await
219        .context("Setting active configuration for the new system")?;
220
221    Ok(())
222}