blob: 6c3cabc373a93f223104560f93156715ea9b1865 [file] [log] [blame]
// Copyright 2020 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
pub mod partition;
pub mod payload_streamer;
use {
crate::partition::Partition,
anyhow::{anyhow, Context, Error},
fdio,
fidl::endpoints::{ClientEnd, Proxy, ServerEnd},
fidl_fuchsia_device::ControllerProxy,
fidl_fuchsia_hardware_block::BlockProxy,
fidl_fuchsia_paver::{DynamicDataSinkProxy, PaverMarker, PaverProxy},
fidl_fuchsia_sysinfo as fsysinfo, fuchsia_async as fasync,
fuchsia_component::client,
fuchsia_zircon as zx, fuchsia_zircon_status as zx_status,
std::{
fs,
io::{self, BufRead, Write},
path::Path,
},
structopt::StructOpt,
};
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum BootloaderType {
Efi,
Coreboot,
}
async fn connect_to_service(path: &str) -> Result<fidl::AsyncChannel, Error> {
let (local, remote) = zx::Channel::create().context("Creating channel")?;
fdio::service_connect(path, remote).context("Connecting to service")?;
let local = fidl::AsyncChannel::from_channel(local).context("Creating AsyncChannel")?;
Ok(local)
}
// There's no nice way to use a service without losing the channel,
// so this function returns the controller.
async fn get_topological_path(
channel: fidl::AsyncChannel,
) -> Result<(Option<String>, fidl::AsyncChannel), Error> {
let controller = ControllerProxy::from_channel(channel);
let topo_resp = controller.get_topological_path().await.context("Getting topological path")?;
Ok((topo_resp.ok(), controller.into_channel().unwrap()))
}
async fn block_device_get_info(
block_channel: fidl::AsyncChannel,
) -> Result<Option<(String, u64)>, Error> {
// Figure out topological path of the block device, so we can guess if it's a disk or a
// partition.
let (maybe_path, block_channel) = get_topological_path(block_channel).await?;
let topo_path = maybe_path.ok_or(anyhow!("Failed to get topo path for device"))?;
// partitions have paths like this:
// /dev/sys/pci/00:14.0/xhci/usb-bus/001/001/ifc-000/ums/lun-000/block/part-000/block
// while disks are like this:
// /dev/sys/pci/00:17.0/ahci/sata2/block
if topo_path.contains("/block/part-") {
// This is probably a partition, skip it
return Ok(None);
}
let block = BlockProxy::from_channel(block_channel);
let (status, maybe_info) = block.get_info().await?;
if let Some(info) = maybe_info {
let blocks = info.block_count;
let block_size = info.block_size as u64;
return Ok(Some((topo_path, blocks * block_size)));
}
return Err(Error::new(zx_status::Status::from_raw(status)));
}
fn get_user_selection(mut choices: Vec<&String>) -> Result<&String, Error> {
print!("Enter a selection (0..{}): ", choices.len() - 1);
io::stdout().flush()?;
let stdin = io::stdin();
let mut handle = stdin.lock();
let selected = loop {
let mut input = String::new();
handle.read_line(&mut input).context("Reading stdin")?;
print!("{}", input); // TODO(fxbug.dev/47155): why does echo not work?
match input.trim().parse::<usize>() {
Ok(selection) if selection < choices.len() => {
break selection;
}
_ => {
print!("Invalid selection, try again? (0..{}): ", choices.len() - 1);
io::stdout().flush()?;
}
}
};
Ok(choices.swap_remove(selected))
}
async fn get_block_devices() -> Result<Vec<(String, u64)>, Error> {
let block_dir = Path::new("/dev/class/block");
let mut devices: Vec<(String, u64)> = Vec::new();
for entry in fs::read_dir(block_dir)? {
let block_channel = connect_to_service(entry?.path().to_str().unwrap()).await?;
let result = block_device_get_info(block_channel).await?;
if let Some(device) = result {
devices.push(device);
}
}
Ok(devices)
}
fn select_block_device(devices: Vec<&(String, u64)>) -> Result<&String, Error> {
if devices.is_empty() {
return Err(anyhow!("Found no block devices."));
}
println!("Please select the disk you want to install Fuchsia to:");
for (i, (topo_path, device_size)) in devices.iter().enumerate() {
println!("[{}] {} ({}G)", i, topo_path, device_size / (1024 * 1024 * 1024));
}
get_user_selection(devices.iter().map(|(path, _)| path).collect())
}
async fn find_install_source(
block_devices: Vec<&String>,
bootloader: BootloaderType,
) -> Result<&String, Error> {
let mut candidate = Err(anyhow!("Could not find the installer disk. Is it plugged in?"));
for device in block_devices.iter() {
// get_partitions returns an empty vector if it doesn't find any partitions
// with the workstation-installer GUID on the disk.
let partitions = Partition::get_partitions(&device, bootloader).await?;
if !partitions.is_empty() {
if candidate.is_err() {
candidate = Ok(*device);
} else {
return Err(anyhow!(
"Found more than one possible installation source. Please check you only \
have one installation disk plugged in!"
));
}
}
}
candidate
}
fn paver_connect(path: &str) -> Result<(PaverProxy, DynamicDataSinkProxy), Error> {
let (block_device_chan, block_remote) = zx::Channel::create()?;
fdio::service_connect(&path, block_remote)?;
let (data_sink_chan, data_remote) = zx::Channel::create()?;
let paver: PaverProxy =
client::connect_to_service::<PaverMarker>().context("Could not connect to paver")?;
paver.use_block_device(ClientEnd::from(block_device_chan), ServerEnd::from(data_remote))?;
let data_sink =
DynamicDataSinkProxy::from_channel(fidl::AsyncChannel::from_channel(data_sink_chan)?);
Ok((paver, data_sink))
}
async fn get_bootloader_type() -> Result<BootloaderType, Error> {
let (sysinfo_chan, remote) = zx::Channel::create()?;
fdio::service_connect(&"/dev/sys/platform", remote).context("Connect to sysinfo")?;
let sysinfo =
fsysinfo::SysInfoProxy::from_channel(fidl::AsyncChannel::from_channel(sysinfo_chan)?);
let (status, bootloader) =
sysinfo.get_bootloader_vendor().await.context("Getting bootloader vendor")?;
if let Some(bootloader) = bootloader {
println!("Bootloader vendor = {}", bootloader);
if bootloader == "coreboot" {
Ok(BootloaderType::Coreboot)
} else {
// The installer only supports coreboot and EFI,
// and EFI BIOS vendor depends on the manufacturer,
// so we assume that non-coreboot bootloader vendors
// mean EFI.
Ok(BootloaderType::Efi)
}
} else {
Err(Error::new(zx::Status::from_raw(status)))
}
}
fn do_confirmation_prompt() -> Result<bool, Error> {
print!("Do you wish to proceed? (yes/[no]) ");
io::stdout().flush()?;
// TODO(fxbug.dev/47155): Why does this not echo what you type in?
let mut input = String::new();
if let Err(e) = io::stdin().read_line(&mut input) {
println!("Failed to read stdin: {}", e);
return Err(Error::new(e));
}
print!("{}", input);
if input.to_lowercase().trim() != "yes" {
return Ok(false);
}
Ok(true)
}
#[derive(Debug, StructOpt)]
#[structopt(name = "installer", about = "install Fuchsia to a disk")]
struct Opt {
/// Don't prompt before wiping the disk
#[structopt(long = "no-wipe-prompt")]
force: bool,
/// Block device to install to
#[structopt(name = "FILE")]
block_device: Option<String>,
}
#[fasync::run_singlethreaded]
async fn main() -> Result<(), Error> {
let opt = Opt::from_args();
let block_devices = get_block_devices().await?;
let bootloader_type = get_bootloader_type().await?;
let install_source =
find_install_source(block_devices.iter().map(|(part, _)| part).collect(), bootloader_type)
.await?;
let block_device_path = opt.block_device.as_ref().unwrap_or_else(|| {
select_block_device(
block_devices.iter().filter(|(part, _)| part != install_source).collect(),
)
.expect("Couldn't select a block device!")
});
println!("Using {} as installation target.", block_device_path);
println!();
println!(
"WARNING: Installing Fuchsia will WIPE YOUR DISK. Make sure you've backed everything \
up before proceeding!"
);
if !opt.force {
if !do_confirmation_prompt()? {
println!("Aborting!");
return Ok(());
}
}
if !block_device_path.starts_with("/dev/") {
println!("Invalid block device path!");
return Ok(());
}
let (_paver, data_sink) =
paver_connect(&block_device_path).context("Could not contact paver")?;
println!("Wiping old partition tables...");
data_sink.wipe_partition_tables().await?;
println!("Initializing Fuchsia partition tables...");
data_sink.initialize_partition_tables().await?;
println!("Success.");
let to_install = Partition::get_partitions(&install_source, bootloader_type)
.await
.context("Getting source partitions")?;
for part in to_install {
print!("{:?}... ", part);
io::stdout().flush()?;
if part.pave(&data_sink).await.is_err() {
println!("Failed");
} else {
println!("OK");
if part.is_ab() {
print!("{:?} [-B]... ", part);
io::stdout().flush()?;
if part.pave_b(&data_sink).await.is_err() {
println!("Failed");
} else {
println!("OK");
}
}
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use {
super::*,
fidl::endpoints::RequestStream,
fidl_fuchsia_device::{ControllerRequest, ControllerRequestStream},
fidl_fuchsia_hardware_block::{BlockInfo, BlockRequest, BlockRequestStream},
futures::prelude::*,
};
async fn serve_block_device(
full_path: &str,
block_count: u64,
block_size: u32,
expect_get_info: bool,
server_end: fidl::AsyncChannel,
) -> Result<(), Error> {
// we expect one get_topological_path request,
// and one get_info request.
let mut stream = ControllerRequestStream::from_channel(server_end);
let req = stream.try_next().await?.unwrap();
if let ControllerRequest::GetTopologicalPath { responder } = req {
responder.send(&mut Ok(full_path.to_string()))?;
} else {
panic!("Expected a GetTopologicalPath request, did not get one.");
}
if !expect_get_info {
return Ok(());
}
let inner = stream.into_inner();
let mut stream = BlockRequestStream::from_inner(inner.0, inner.1);
let req = stream.try_next().await?;
if !expect_get_info {
return Ok(());
}
if let BlockRequest::GetInfo { responder } = req.unwrap() {
responder.send(
0,
Some(&mut BlockInfo {
block_count: block_count,
block_size,
max_transfer_size: 0,
flags: 0,
reserved: 0,
}),
)?;
} else {
panic!("Expected a GetInfo request, but did not get one.");
}
Ok(())
}
fn mock_block_device(
full_path: &'static str,
block_count: u64,
block_size: u32,
expect_get_info: bool,
) -> Result<fidl::AsyncChannel, Error> {
let (client, server) = zx::Channel::create()?;
let client = fidl::AsyncChannel::from_channel(client)?;
let server = fidl::AsyncChannel::from_channel(server)?;
fasync::Task::local(
serve_block_device(full_path, block_count, block_size, expect_get_info, server)
.unwrap_or_else(|e| panic!("Error while serving fake block device: {}", e)),
)
.detach();
Ok(client)
}
#[fasync::run_singlethreaded(test)]
async fn test_get_info_basic() -> Result<(), Error> {
let chan = mock_block_device("/dev/sys/pci/00:17.0/ahci/sata2/block", 100, 512, true)?;
let info = block_device_get_info(chan).await?;
assert!(info.is_some());
let (path, size) = info.unwrap();
assert_eq!(path, "/dev/sys/pci/00:17.0/ahci/sata2/block");
assert_eq!(size, 100 * 512);
Ok(())
}
#[fasync::run_singlethreaded(test)]
async fn test_get_info_weird_block_size() -> Result<(), Error> {
let chan = mock_block_device(
"/dev/sys/pci/00:14.0/xhci/usb-bus/001/001/ifc-000/ums/lun-000/block",
1000,
4,
true,
)?;
let info = block_device_get_info(chan).await?;
assert!(info.is_some());
let (path, size) = info.unwrap();
assert_eq!(path, "/dev/sys/pci/00:14.0/xhci/usb-bus/001/001/ifc-000/ums/lun-000/block");
assert_eq!(size, 1000 * 4);
Ok(())
}
#[fasync::run_singlethreaded(test)]
async fn test_get_info_partitions() -> Result<(), Error> {
let chan =
mock_block_device("/dev/sys/pci/00:17.0/ahci/sata2/block/part-000/block", 0, 0, false)?;
let info = block_device_get_info(chan).await?;
assert!(info.is_none());
Ok(())
}
#[fasync::run_singlethreaded(test)]
async fn test_get_info_usb_partitions() -> Result<(), Error> {
let chan = mock_block_device(
"/dev/sys/pci/00:14.0/xhci/usb-bus/001/001/ifc-000/ums/lun-000/block/part-003/block",
0,
0,
false,
)?;
let info = block_device_get_info(chan).await?;
assert!(info.is_none());
Ok(())
}
}