| // Copyright 2023 The ChromiumOS Authors | |
| // Use of this source code is governed by a BSD-style license that can be | |
| // found in the LICENSE file. | |
| use std::{ | |
| os::unix::fs::{chown, PermissionsExt}, | |
| path::{Path, PathBuf}, | |
| process::ExitCode, | |
| }; | |
| use anyhow::{anyhow, bail, Context, Result}; | |
| use libchromeos::panic_handler; | |
| use log::{error, info}; | |
| use nix::sys::{reboot::reboot, stat::Mode}; | |
| mod cgpt; | |
| mod chromeos_install; | |
| mod disk; | |
| mod gpt; | |
| mod logger; | |
| mod lsblk; | |
| mod mount; | |
| mod util; | |
| const FLEX_IMAGE_FILENAME: &str = "flex_image.tar.xz"; | |
| const FLEX_CONFIG_SRC_FILENAME: &str = "flex_config.json"; | |
| const FLEX_CONFIG_TARGET_FILEPATH: &str = "unencrypted/flex_config/config.json"; | |
| const FLEXOR_LOG_FILE: &str = "/var/log/messages"; | |
| // User ID and Group ID for the oobe_config_restore user in ChromeOS. | |
| const OOBE_CFG_RESTORE_UID: u32 = 20121; | |
| const OOBE_CFG_RESTORE_GID: u32 = 20121; | |
| // File and folder permissions for the Flex config. | |
| const FLEX_CONFIG_DIR_PERM: u32 = 0o740; | |
| const FLEX_CONFIG_FILE_PERM: u32 = 0o640; | |
| // This struct contains all of the information commonly passed around | |
| // during an installation. | |
| struct InstallConfig { | |
| target_device: PathBuf, | |
| install_partition: PathBuf, | |
| } | |
| impl InstallConfig { | |
| pub fn new() -> Result<Self> { | |
| let disk_info = disk::disk_info()?; | |
| Ok(InstallConfig { | |
| target_device: disk_info.target_device, | |
| install_partition: disk_info.install_partition, | |
| }) | |
| } | |
| } | |
| /// Copies the ChromeOS Flex image and other install files (e.g. config) to | |
| /// rootfs (residing in RAM). This is done since we are about to repartition the | |
| /// disk and can't loose the image. Since the image size is about 2.5GB, we | |
| /// assume that much free space in RAM. | |
| fn copy_installation_files_to_rootfs(config: &InstallConfig) -> Result<()> { | |
| let mount = mount::Mount::mount_by_path(&config.install_partition, mount::FsType::Vfat) | |
| .context("Unable to mount the install partition")?; | |
| // Copy the image to rootfs. | |
| std::fs::copy( | |
| mount.mount_path().join(FLEX_IMAGE_FILENAME), | |
| Path::new("/root").join(FLEX_IMAGE_FILENAME), | |
| ) | |
| .context("Unable to copy image to rootfs")?; | |
| // Copy the config to rootfs. | |
| if std::fs::copy( | |
| mount.mount_path().join(FLEX_CONFIG_SRC_FILENAME), | |
| Path::new("root").join(FLEX_CONFIG_SRC_FILENAME), | |
| ) | |
| .is_err() | |
| { | |
| info!("Unable to copy flex config to rootfs, proceeding since it is optional"); | |
| } | |
| Ok(()) | |
| } | |
| /// Setup the disk for a ChromeOS Flex installation performing the following | |
| /// two steps: | |
| /// 1. Put the ChromeOS partition layout and write stateful partition. | |
| /// 2. Insert a thirteenth partition on disk for our own data. | |
| fn setup_disk(config: &InstallConfig) -> Result<()> { | |
| // Install the layout and stateful partition. | |
| chromeos_install::write_partition_table_and_stateful(&config.target_device) | |
| .context("Unable to put initial partition table")?; | |
| // Insert a thirtheenth partition. | |
| disk::insert_thirteenth_partition(&config.target_device) | |
| .context("Unable to insert thirtheenth partition")?; | |
| // Reread the partition table. | |
| disk::reload_partitions(&config.target_device).context("Unable to reload partition table") | |
| } | |
| /// Sets up the thirteenth partition on disk and then proceeds to install the | |
| /// provided image on the device. | |
| fn setup_flex_deploy_partition_and_install(config: &InstallConfig) -> Result<()> { | |
| // Create an ext4 filesystem on the disk. | |
| let new_partition_path = | |
| libchromeos::disk::get_partition_device(&config.target_device, disk::FLEX_DEPLOY_PART_NUM) | |
| .context("Unable to find correct partition path")?; | |
| disk::mkfs_ext4(new_partition_path.as_path()) | |
| .context("Unable to write ext4 to the flex deployment partition")?; | |
| let new_part_mount = | |
| mount::Mount::mount_by_path(new_partition_path.as_path(), mount::FsType::EXT4) | |
| .context("Unable to mount flex deployment partition")?; | |
| // Then uncompress the image on disk. | |
| let entries = util::uncompress_tar_xz( | |
| &Path::new("/root").join(FLEX_IMAGE_FILENAME), | |
| new_part_mount.mount_path(), | |
| ) | |
| .context("Unable to uncompress the image")?; | |
| // A compressed ChromeOS image only contains the image path. | |
| let image_path = entries | |
| .first() | |
| .context("Got malformed ChromeOS Flex image")?; | |
| // Finally install the image on disk. | |
| chromeos_install::install_image_to_disk( | |
| &config.target_device, | |
| new_part_mount.mount_path().join(image_path).as_path(), | |
| ) | |
| .context("Unable to install the image to disk") | |
| } | |
| /// Copies the flex config to stateful partition. | |
| fn copy_flex_config_to_stateful(config: &InstallConfig) -> Result<()> { | |
| let stateful_mount = mount::Mount::mount_by_path( | |
| &libchromeos::disk::get_partition_device( | |
| &config.target_device, | |
| disk::STATEFUL_PARTITION_NUM, | |
| ) | |
| .context("Unable to find stateful partition")?, | |
| mount::FsType::EXT4, | |
| )?; | |
| let config_path = stateful_mount | |
| .mount_path() | |
| .join(FLEX_CONFIG_TARGET_FILEPATH); | |
| // Create the new `flex_config` folder and copy the file. | |
| nix::unistd::mkdir( | |
| config_path.parent().unwrap(), | |
| Mode::from_bits(FLEX_CONFIG_DIR_PERM).unwrap(), | |
| )?; | |
| std::fs::copy( | |
| Path::new("root").join(FLEX_CONFIG_SRC_FILENAME), | |
| &config_path, | |
| ) | |
| .context("Unable to copy config")?; | |
| // Set correct owner for both the new `flex_config` folder and | |
| // `config.json` file. | |
| fn set_oobe_cfg_restore_owner(path: &Path) -> Result<()> { | |
| chown(path, Some(OOBE_CFG_RESTORE_UID), Some(OOBE_CFG_RESTORE_GID)).context(format!( | |
| "Unable to set correct owner for {}", | |
| path.display() | |
| )) | |
| } | |
| set_oobe_cfg_restore_owner(config_path.parent().unwrap())?; | |
| set_oobe_cfg_restore_owner(&config_path)?; | |
| // Set the correct file permissions for the config. | |
| let mut perm = std::fs::metadata(&config_path)?.permissions(); | |
| perm.set_mode(FLEX_CONFIG_FILE_PERM); | |
| std::fs::set_permissions(&config_path, perm)?; | |
| Ok(()) | |
| } | |
| /// Performs the actual installation of ChromeOS. | |
| fn perform_installation(config: &InstallConfig) -> Result<()> { | |
| info!("Setting up the disk"); | |
| setup_disk(config)?; | |
| info!("Setting up the new partition and installing ChromeOS Flex"); | |
| setup_flex_deploy_partition_and_install(config)?; | |
| info!("Trying to remove the flex deployment partition"); | |
| disk::try_remove_thirteenth_partition(&config.target_device)?; | |
| if matches!( | |
| Path::new("root") | |
| .join(FLEX_CONFIG_SRC_FILENAME) | |
| .try_exists(), | |
| Ok(true) | |
| ) { | |
| if let Err(err) = copy_flex_config_to_stateful(config) { | |
| error!("Unable to copy Flex config due to: {err}"); | |
| // If this fails, we can't do anything about it, so ignore the error. | |
| let _ = try_save_logs(config); | |
| } else { | |
| info!("Successfully copied a flex config to stateful partition."); | |
| } | |
| return Ok(()); | |
| } | |
| info!("Didn't find a flex config to copy to stateful partition, still proceeding"); | |
| Ok(()) | |
| } | |
| /// Installs ChromeOS Flex and retries the actual installation steps at most three times. | |
| fn run(config: &InstallConfig) -> Result<()> { | |
| copy_installation_files_to_rootfs(config)?; | |
| // Try installing on the device three times at most. | |
| let mut return_err = anyhow!(""); | |
| for _ in 0..3 { | |
| match perform_installation(config) { | |
| Ok(_) => { | |
| // On success we reboot and end execution. | |
| info!("Install successful, rebooting into ChromeOS Flex"); | |
| reboot(nix::sys::reboot::RebootMode::RB_AUTOBOOT) | |
| .context("Unable to reboot after successful installation")?; | |
| return Ok(()); | |
| } | |
| Err(err) => { | |
| error!("Flexor couldn't complete due to error: {err}"); | |
| return_err = err; | |
| } | |
| } | |
| } | |
| Err(return_err) | |
| } | |
| /// Tries to save logs to the disk depending on what state the installation fails in. | |
| /// We basically have two option: | |
| /// 1. Either we are in the state before the disk was reformatted, in that case we write | |
| /// the logs back to the partition that also has the installation payload. | |
| /// 2. Otherwise we hope to already have the Flex layout including the FLEX_DEPLOY partition | |
| /// in that case we write the logs to that partition (may need to create a filesystem on that | |
| /// partition though). | |
| fn try_save_logs(config: &InstallConfig) -> Result<()> { | |
| // Case 1: The install partition still exists, so we write the logs to it. | |
| if matches!(config.install_partition.try_exists(), Ok(true)) { | |
| let install_mount = | |
| mount::Mount::mount_by_path(&config.install_partition, mount::FsType::Vfat)?; | |
| std::fs::copy(FLEXOR_LOG_FILE, install_mount.mount_path()) | |
| .context("Unable to copy the logfile to the install partition")?; | |
| return Ok(()); | |
| } | |
| let flex_depl_partition_path = | |
| libchromeos::disk::get_partition_device(&config.target_device, disk::FLEX_DEPLOY_PART_NUM) | |
| .context("Error finding a place to write logs to")?; | |
| // Case 2: We already have the Flex layout and can try to write to the FLEX_DEPLOY partition. | |
| if let Ok(true) = flex_depl_partition_path.try_exists() { | |
| match mount::Mount::mount_by_path(&flex_depl_partition_path, mount::FsType::EXT4) { | |
| Ok(flex_depl_mount) => { | |
| std::fs::copy(FLEXOR_LOG_FILE, flex_depl_mount.mount_path()) | |
| .context("Unable to copy the logfile to the flex deployment partition")?; | |
| } | |
| Err(_) => { | |
| // The partition seems to exist, but we can't mount it as ext4, | |
| // so we try to create a file system and retry. | |
| disk::mkfs_ext4(&flex_depl_partition_path)?; | |
| let flex_depl_mount = | |
| mount::Mount::mount_by_path(&flex_depl_partition_path, mount::FsType::EXT4)?; | |
| std::fs::copy(FLEXOR_LOG_FILE, flex_depl_mount.mount_path()).context( | |
| "Unable to copy the logfile to the formatted flex deployment partition", | |
| )?; | |
| } | |
| } | |
| } else { | |
| bail!( | |
| "Unable to write logs since neither the data partition | |
| nor the flex deployment partition exist" | |
| ); | |
| } | |
| Ok(()) | |
| } | |
| fn main() -> ExitCode { | |
| // Setup the panic handler and logging. | |
| panic_handler::install_memfd_handler(); | |
| if let Err(err) = logger::init() { | |
| eprintln!("Failed to initialize logger: {err}"); | |
| } | |
| info!("Installing ChromeOS Flex, please wait and don't turn off the device"); | |
| let config = match InstallConfig::new() { | |
| Ok(config) => config, | |
| Err(err) => { | |
| error!("Error getting information for the install: {err}"); | |
| return ExitCode::FAILURE; | |
| } | |
| }; | |
| if let Err(err) = run(&config) { | |
| error!("Unable to perform installation due to error: {err}"); | |
| // If we weren't successful, try to save the logs. | |
| if let Err(err) = try_save_logs(&config) { | |
| error!("Unable to save logs due to: {err}") | |
| } | |
| // TODO(b/314965086): Add an error screen displaying the log. | |
| ExitCode::FAILURE | |
| } else { | |
| info!("Successfully installed, rebooting"); | |
| ExitCode::SUCCESS | |
| } | |
| } |