2019-08-13 21:43:55 +03:00

501 lines
14 KiB
Rust

mod args;
mod error;
mod initcpio;
mod presets;
mod process;
mod storage;
mod tool;
use crate::args::*;
use crate::error::*;
use crate::process::CommandExt;
use crate::storage::*;
use crate::tool::Tool;
use byte_unit::Byte;
use console::style;
use dialoguer::{theme::ColorfulTheme, Select};
use failure::{Fail, ResultExt};
use log::{debug, error, info, log_enabled, Level, LevelFilter};
use pretty_env_logger;
use std::collections::HashSet;
use std::fs;
use std::io::Write;
use std::os::unix::{fs::PermissionsExt, process::CommandExt as UnixCommandExt};
use std::path::{Path, PathBuf};
use std::process::{exit, Command as ProcessCommand};
use std::thread;
use std::time::Duration;
use structopt::StructOpt;
use tempfile::tempdir;
const BOOT_PARTITION_INDEX: u8 = 1;
const ROOT_PARTITION_INDEX: u8 = 3;
static JOURNALD_CONF: &'static str = "
[Journal]
Storage=volatile
SystemMaxUse=16M
";
fn mount<'a>(
mount_path: &Path,
boot_filesystem: &'a Filesystem,
root_filesystem: &'a Filesystem,
) -> Result<MountStack<'a>, Error> {
let mut mount_stack = MountStack::new();
debug!(
"Root partition: {}",
root_filesystem.block().path().display()
);
info!("Mounting filesystems to {}", mount_path.display());
mount_stack
.mount(&root_filesystem, mount_path.into(), None)
.context(ErrorKind::Mounting)?;
let boot_point = mount_path.join("boot");
if !boot_point.exists() {
fs::create_dir(&boot_point).context(ErrorKind::CreateBoot)?;
}
mount_stack
.mount(&boot_filesystem, boot_point, None)
.context(ErrorKind::Mounting)?;
Ok(mount_stack)
}
fn fix_fstab(fstab: &str) -> String {
fstab
.lines()
.filter(|line| !line.contains("swap") && !line.starts_with('#'))
.collect::<Vec<&str>>()
.join("\n")
}
fn create_image(path: &Path, size: Byte, overwrite: bool) -> Result<LoopDevice, Error> {
{
let mut options = fs::OpenOptions::new();
options.write(true);
if overwrite {
options.create(true);
} else {
options.create_new(true);
}
let file = options.open(path).context(ErrorKind::Image)?;
file.set_len(size.get_bytes() as u64)
.context(ErrorKind::Image)?;
}
LoopDevice::create(path)
}
fn select_block_device(allow_non_removable: bool) -> Result<PathBuf, Error> {
let devices = get_storage_devices(allow_non_removable)?;
if devices.is_empty() {
Err(ErrorKind::NoRemovableDevices)?
}
if allow_non_removable {
println!(
"{}\n",
style("Showing non-removable devices. Make sure you select the correct device.")
.red()
.bold()
);
}
let selection = Select::with_theme(&ColorfulTheme::default())
.with_prompt("Select a removable device")
.default(0)
.items(&devices)
.interact()
.unwrap();
Ok(PathBuf::from("/dev").join(&devices[selection].name))
}
#[allow(clippy::cognitive_complexity)]
fn create(command: CreateCommand) -> Result<(), Error> {
let presets = presets::Presets::load(&command.presets)?;
let sgdisk = Tool::find("sgdisk")?;
let pacstrap = Tool::find("pacstrap")?;
let arch_chroot = Tool::find("arch-chroot")?;
let genfstab = Tool::find("genfstab")?;
let mkfat = Tool::find("mkfs.fat")?;
let mkext4 = Tool::find("mkfs.ext4")?;
let cryptsetup = if command.encrypted_root {
Some(Tool::find("cryptsetup")?)
} else {
None
};
let blkid = if command.encrypted_root {
Some(Tool::find("blkid")?)
} else {
None
};
let storage_device_path = if let Some(path) = command.path {
path
} else {
select_block_device(command.allow_non_removable)?
};
let image_loop = if let Some(size) = command.image {
Some(create_image(&storage_device_path, size, command.overwrite)?)
} else {
None
};
let storage_device = storage::StorageDevice::from_path(
image_loop
.as_ref()
.map(|loop_dev| {
info!("Using loop device at {}", loop_dev.path().display());
loop_dev.path()
})
.unwrap_or(&storage_device_path),
command.allow_non_removable,
)?;
let mount_point = tempdir().context(ErrorKind::TmpDirError)?;
let disk_path = storage_device.path();
info!("Partitioning the block device");
debug!("{:?}", disk_path);
sgdisk
.execute()
.args(&[
"-Z",
"-o",
"--new=1::+100M",
"--new=2::+1M",
"--largest-new=3",
"--typecode=1:EF00",
"--typecode=2:EF02",
])
.arg(&disk_path)
.run(ErrorKind::Partitioning)?;
thread::sleep(Duration::from_millis(1000));
info!("Formatting filesystems");
let boot_partition = storage_device.get_partition(BOOT_PARTITION_INDEX)?;
let boot_filesystem = Filesystem::format(&boot_partition, FilesystemType::Vfat, &mkfat)?;
let root_partition_base = storage_device.get_partition(ROOT_PARTITION_INDEX)?;
let encrypted_root = if let Some(cryptsetup) = &cryptsetup {
info!("Encrypting the root filesystem");
EncryptedDevice::prepare(&cryptsetup, &root_partition_base)?;
Some(EncryptedDevice::open(
cryptsetup,
&root_partition_base,
"alma_root".into(),
)?)
} else {
None
};
let root_partition = if let Some(e) = encrypted_root.as_ref() {
e as &BlockDevice
} else {
&root_partition_base as &BlockDevice
};
let root_filesystem = Filesystem::format(root_partition, FilesystemType::Ext4, &mkext4)?;
let mount_stack = mount(mount_point.path(), &boot_filesystem, &root_filesystem)?;
if log_enabled!(Level::Debug) {
debug!("lsblk:");
ProcessCommand::new("lsblk")
.arg("--fs")
.spawn()
.and_then(|mut p| p.wait())
.map_err(|e| {
error!("Error running lsblk: {}", e);
})
.ok();
}
let mut packages: HashSet<String> = [
"base",
"grub",
"efibootmgr",
"intel-ucode",
"networkmanager",
"broadcom-wl",
]
.iter()
.map(|s| String::from(*s))
.collect();
packages.extend(presets.packages);
info!("Bootstrapping system");
pacstrap
.execute()
.arg("-c")
.arg(mount_point.path())
.args(packages)
.args(&command.extra_packages)
.run(ErrorKind::Pacstrap)?;
let fstab = fix_fstab(
&genfstab
.execute()
.arg("-U")
.arg(mount_point.path())
.run_text_output(ErrorKind::Fstab)?,
);
debug!("fstab:\n{}", fstab);
fs::write(mount_point.path().join("etc/fstab"), fstab).context(ErrorKind::Fstab)?;
if !presets.scripts.is_empty() {
info!("Running custom scripts");
}
for script in presets.scripts {
let mut script_file =
tempfile::NamedTempFile::new_in(mount_point.path()).context(ErrorKind::PresetScript)?;
script_file
.write_all(script.as_bytes())
.and_then(|_| script_file.as_file_mut().metadata())
.and_then(|metadata| {
let mut permissions = metadata.permissions();
permissions.set_mode(0o755);
fs::set_permissions(script_file.path(), permissions)
})
.context(ErrorKind::PresetScript)?;
let script_path = script_file.into_temp_path();
arch_chroot
.execute()
.arg(mount_point.path())
.arg(Path::new("/").join(script_path.file_name().unwrap()))
.run(ErrorKind::PostInstallation)?;
}
info!("Performing post installation tasks");
arch_chroot
.execute()
.arg(mount_point.path())
.args(&["systemctl", "enable", "NetworkManager"])
.run(ErrorKind::PostInstallation)?;
info!("Configuring journald");
fs::write(
mount_point.path().join("etc/systemd/journald.conf"),
JOURNALD_CONF,
)
.context(ErrorKind::PostInstallation)?;
info!("Setting locale");
fs::OpenOptions::new()
.append(true)
.write(true)
.open(mount_point.path().join("etc/locale.gen"))
.and_then(|mut locale_gen| locale_gen.write_all(b"en_US.UTF-8 UTF-8\n"))
.context(ErrorKind::Locale)?;
fs::write(
mount_point.path().join("etc/locale.conf"),
"LANG=en_US.UTF-8",
)
.context(ErrorKind::Locale)?;
arch_chroot
.execute()
.arg(mount_point.path())
.arg("locale-gen")
.run(ErrorKind::Locale)?;
info!("Generating initramfs");
fs::write(
mount_point.path().join("etc/mkinitcpio.conf"),
initcpio::Initcpio::new(encrypted_root.is_some()).to_config(),
)
.context(ErrorKind::Initramfs)?;
arch_chroot
.execute()
.arg(mount_point.path())
.args(&["mkinitcpio", "-p", "linux"])
.run(ErrorKind::Initramfs)?;
if encrypted_root.is_some() {
debug!("Setting up GRUB for an encrypted root partition");
let uuid = blkid
.unwrap()
.execute()
.arg(root_partition_base.path())
.args(&["-o", "value", "-s", "UUID"])
.run_text_output(ErrorKind::Partitioning)?;
let trimmed = uuid.trim();
debug!("Root partition UUID: {}", trimmed);
let mut grub_file = fs::OpenOptions::new()
.append(true)
.open(mount_point.path().join("etc/default/grub"))
.context(ErrorKind::Bootloader)?;
write!(
&mut grub_file,
"GRUB_CMDLINE_LINUX=\"cryptdevice=UUID={}:luks_root\"",
trimmed
)
.context(ErrorKind::Bootloader)?;
}
info!("Installing the Bootloader");
arch_chroot
.execute()
.arg(mount_point.path())
.args(&["bash", "-c"])
.arg(format!("grub-install --target=i386-pc --boot-directory /boot {} && grub-install --target=x86_64-efi --efi-directory /boot --boot-directory /boot --removable && grub-mkconfig -o /boot/grub/grub.cfg", disk_path.display()))
.run(ErrorKind::Bootloader)?;
debug!(
"GRUB configuration: {}",
fs::read_to_string(mount_point.path().join("boot/grub/grub.cfg"))
.unwrap_or_else(|e| e.to_string())
);
if command.interactive {
info!("Dropping you to chroot. Do as you wish to customize the installation. Please exit by typing 'exit' instead of using Ctrl+D");
arch_chroot
.execute()
.arg(mount_point.path())
.run(ErrorKind::Interactive)?;
}
info!("Unmounting filesystems");
mount_stack.umount()?;
Ok(())
}
fn chroot(command: ChrootCommand) -> Result<(), Error> {
let arch_chroot = Tool::find("arch-chroot")?;
let mut cryptsetup;
let mut loop_device: Option<LoopDevice>;
let storage_device =
match storage::StorageDevice::from_path(&command.block_device, command.allow_non_removable)
{
Ok(b) => b,
Err(_) => {
loop_device = Some(LoopDevice::create(&command.block_device)?);
storage::StorageDevice::from_path(
loop_device.as_ref().unwrap().path(),
command.allow_non_removable,
)?
}
};
let mount_point = tempdir().context(ErrorKind::TmpDirError)?;
let boot_partition = storage_device.get_partition(BOOT_PARTITION_INDEX)?;
let boot_filesystem = Filesystem::from_partition(&boot_partition, FilesystemType::Vfat);
let root_partition_base = storage_device.get_partition(ROOT_PARTITION_INDEX)?;
let encrypted_root = if is_encrypted_device(&root_partition_base)? {
cryptsetup = Some(Tool::find("cryptsetup")?);
Some(EncryptedDevice::open(
cryptsetup.as_ref().unwrap(),
&root_partition_base,
"alma_root".into(),
)?)
} else {
None
};
let root_partition = if let Some(e) = encrypted_root.as_ref() {
e as &BlockDevice
} else {
&root_partition_base as &BlockDevice
};
let root_filesystem = Filesystem::from_partition(root_partition, FilesystemType::Ext4);
let mount_stack = mount(mount_point.path(), &boot_filesystem, &root_filesystem)?;
arch_chroot
.execute()
.arg(mount_point.path())
.args(&command.command)
.run(ErrorKind::Interactive)?;
info!("Unmounting filesystems");
mount_stack.umount()?;
Ok(())
}
fn qemu(command: QemuCommand) -> Result<(), Error> {
let qemu = Tool::find("qemu-system-x86_64")?;
let mut run = qemu.execute();
run.args(&[
"-m",
"4G",
"-netdev",
"user,id=user.0",
"-device",
"virtio-net-pci,netdev=user.0",
"-device",
"qemu-xhci,id=xhci",
"-device",
"usb-tablet,bus=xhci.0",
"-drive",
])
.arg(format!(
"file={},if=virtio,format=raw",
command.block_device.display()
))
.args(command.args);
if PathBuf::from("/dev/kvm").exists() {
debug!("KVM is enabled");
run.args(&["-enable-kvm", "-cpu", "host"]);
}
let err = run.exec();
Err(err).context(ErrorKind::Qemu)?
}
fn main() {
let app = App::from_args();
let mut builder = pretty_env_logger::formatted_timed_builder();
let log_level = if app.verbose {
LevelFilter::Debug
} else {
LevelFilter::Info
};
builder.filter_level(log_level);
builder.init();
let result = match app.cmd {
Command::Create(command) => create(command),
Command::Chroot(command) => chroot(command),
Command::Qemu(command) => qemu(command),
};
match result {
Ok(()) => {
exit(0);
}
Err(error) => {
error!("{}", error);
for cause in (&error as &Fail).iter_causes() {
error!("Caused by: {}", cause);
}
exit(1);
}
}
}