use anyhow::{anyhow, bail, Context, Result};
use gptman::{GPTPartitionEntry, GPT};
use nix::sys::stat::{major, minor};
use nix::{errno::Errno, mount, sched};
use regex::Regex;
use std::collections::{HashMap, HashSet};
use std::fs::{
canonicalize, metadata, read_dir, read_to_string, remove_dir, symlink_metadata, File,
OpenOptions,
};
use std::io::{Read, Seek, SeekFrom, Write};
use std::num::{NonZeroU32, NonZeroU64};
use std::os::linux::fs::MetadataExt;
use std::os::raw::c_int;
use std::os::unix::fs::FileTypeExt;
use std::os::unix::io::AsRawFd;
use std::path::{Path, PathBuf};
use std::process::Command;
use std::thread::sleep;
use std::time::Duration;
use uuid::Uuid;
use crate::cmdline::PartitionFilter;
use crate::util::*;
use crate::{runcmd, runcmd_output};
#[derive(Debug)]
pub struct Disk {
path: String,
}
impl Disk {
pub fn new<P: AsRef<Path>>(path: P) -> Result<Self> {
let path = path.as_ref();
let canon_path = path
.canonicalize()
.with_context(|| format!("canonicalizing {}", path.display()))?;
let canon_path = canon_path
.to_str()
.with_context(|| {
format!(
"path {} canonicalized from {} is not UTF-8",
canon_path.display(),
path.display()
)
})?
.to_string();
Ok(Disk { path: canon_path })
}
pub fn mount_partition_by_label(&self, label: &str, flags: mount::MsFlags) -> Result<Mount> {
let partitions = self.get_partitions()?;
if partitions.is_empty() {
bail!("couldn't find any partitions on {}", self.path);
}
let matching_partitions = partitions
.iter()
.filter(|d| d.label.as_ref().unwrap_or(&"".to_string()) == label)
.collect::<Vec<&Partition>>();
let part = match matching_partitions.len() {
0 => bail!("couldn't find {} device for {}", label, self.path),
1 => matching_partitions[0],
_ => bail!(
"found multiple devices on {} with label \"{}\"",
self.path,
label
),
};
match &part.fstype {
Some(fstype) => Mount::try_mount(&part.path, fstype, flags),
None => bail!(
"couldn't get filesystem type of {} device for {}",
label,
self.path
),
}
}
fn get_partitions(&self) -> Result<Vec<Partition>> {
let mut result: Vec<Partition> = Vec::new();
for devinfo in lsblk(Path::new(&self.path), true)? {
if let Some(name) = devinfo.get("NAME") {
if devinfo.get("TYPE").map(|s| s.as_str()) != Some("part") {
continue;
}
let (mountpoint, swap) = match devinfo.get("MOUNTPOINT") {
Some(mp) if mp == "[SWAP]" => (None, true),
Some(mp) => (Some(mp.to_string()), false),
None => (None, false),
};
result.push(Partition {
path: name.to_owned(),
label: devinfo.get("LABEL").map(<_>::to_string),
fstype: devinfo.get("FSTYPE").map(<_>::to_string),
parent: self.path.to_owned(),
mountpoint,
swap,
});
}
}
Ok(result)
}
pub fn get_busy_partitions(self) -> Result<Vec<Partition>> {
let rereadpt_result = {
let mut f = OpenOptions::new()
.write(true)
.open(&self.path)
.with_context(|| format!("opening {}", &self.path))?;
reread_partition_table(&mut f, false).map(|_| Vec::new())
};
if rereadpt_result.is_ok() {
return rereadpt_result;
}
let mut busy: Vec<Partition> = Vec::new();
for d in self.get_partitions()? {
if d.mountpoint.is_some() || d.swap || !d.get_holders()?.is_empty() {
busy.push(d)
}
}
if !busy.is_empty() {
return Ok(busy);
}
if !self.is_dm_device() {
return rereadpt_result;
}
Ok(Vec::new())
}
pub fn get_partition_table(&self) -> Result<Box<dyn PartTable>> {
if self.is_dm_device() {
Ok(Box::new(PartTableKpartx::new(&self.path)?))
} else {
Ok(Box::new(PartTableKernel::new(&self.path)?))
}
}
pub fn is_dm_device(&self) -> bool {
self.path.starts_with("/dev/dm-")
}
pub fn is_luks_integrity(&self) -> Result<bool> {
if !self.is_dm_device() {
return Ok(false);
}
Ok(runcmd_output!(
"dmsetup",
"info",
"--columns",
"--noheadings",
"-o",
"uuid",
&self.path
)
.with_context(|| format!("checking if device {} is type LUKS integrity", self.path))?
.trim()
.starts_with("CRYPT-INTEGRITY-"))
}
}
pub trait PartTable {
fn reread(&mut self) -> Result<()>;
}
#[derive(Debug)]
pub struct PartTableKernel {
file: File,
}
impl PartTableKernel {
fn new(path: &str) -> Result<Self> {
let file = OpenOptions::new()
.write(true)
.open(path)
.with_context(|| format!("opening {}", path))?;
Ok(Self { file })
}
}
impl PartTable for PartTableKernel {
fn reread(&mut self) -> Result<()> {
reread_partition_table(&mut self.file, true)?;
udev_settle()
}
}
#[derive(Debug)]
pub struct PartTableKpartx {
path: String,
need_teardown: bool,
}
impl PartTableKpartx {
fn new(path: &str) -> Result<Self> {
let mut table = Self {
path: path.to_string(),
need_teardown: !Self::already_set_up(path)?,
};
table.reread()?;
Ok(table)
}
fn already_set_up(path: &str) -> Result<bool> {
let re = Regex::new(r"^p[0-9]+$").expect("compiling RE");
let expected = Path::new(path)
.file_name()
.with_context(|| format!("getting filename of {}", path))?
.to_os_string()
.into_string()
.map_err(|_| anyhow!("converting filename of {}", path))?;
for ent in read_dir("/dev/mapper").context("listing /dev/mapper")? {
let ent = ent.context("reading /dev/mapper entry")?;
let found = ent.file_name().into_string().map_err(|_| {
anyhow!(
"converting filename of {}",
Path::new(&ent.file_name()).display()
)
})?;
if found.starts_with(&expected) && re.is_match(&found[expected.len()..]) {
return Ok(true);
}
}
Ok(false)
}
fn run_kpartx(&self, flag: &str) -> Result<()> {
runcmd_output!("kpartx", flag, "-n", &self.path)?;
udev_settle()?;
Ok(())
}
}
impl PartTable for PartTableKpartx {
fn reread(&mut self) -> Result<()> {
let delay = 1;
for _ in 0..4 {
match self.run_kpartx("-u") {
Ok(()) => return Ok(()),
Err(e) => eprintln!("Error: {}", e),
}
eprintln!("Retrying in {} second", delay);
sleep(Duration::from_secs(delay));
}
self.run_kpartx("-u")
}
}
impl Drop for PartTableKpartx {
fn drop(&mut self) {
if self.need_teardown {
if let Err(e) = self.run_kpartx("-d") {
eprintln!("{}", e)
}
}
}
}
#[derive(Debug)]
pub struct Partition {
pub path: String,
pub label: Option<String>,
pub fstype: Option<String>,
pub parent: String,
pub mountpoint: Option<String>,
pub swap: bool,
}
impl Partition {
pub fn get_offsets(path: &str) -> Result<(u64, u64)> {
let dev = metadata(path)
.with_context(|| format!("getting metadata for {}", path))?
.st_rdev();
let maj: u64 = major(dev);
let min: u64 = minor(dev);
let start = read_sysfs_dev_block_value_u64(maj, min, "start")?;
let size = read_sysfs_dev_block_value_u64(maj, min, "size")?;
let start_offset: u64 = start
.checked_mul(512)
.context("start offset mult overflow")?;
let end_offset: u64 = start_offset
.checked_add(size.checked_mul(512).context("end offset mult overflow")?)
.context("end offset add overflow")?;
Ok((start_offset, end_offset))
}
pub fn get_holders(&self) -> Result<Vec<String>> {
let holders = self.get_sysfs_dir()?.join("holders");
let mut ret: Vec<String> = Vec::new();
for ent in read_dir(&holders).with_context(|| format!("reading {}", &holders.display()))? {
let ent = ent.with_context(|| format!("reading {} entry", &holders.display()))?;
ret.push(format!("/dev/{}", ent.file_name().to_string_lossy()));
}
Ok(ret)
}
fn get_sysfs_dir(&self) -> Result<PathBuf> {
let basedir = Path::new("/sys/block");
let devdir = basedir
.join(
Path::new(&self.parent)
.file_name()
.with_context(|| format!("parent {} has no filename", self.parent))?,
)
.join(
Path::new(&self.path)
.file_name()
.with_context(|| format!("path {} has no filename", self.path))?,
);
if devdir.exists() {
return Ok(devdir);
}
let is_link = symlink_metadata(&self.path)
.with_context(|| format!("reading metadata for {}", self.path))?
.file_type()
.is_symlink();
if is_link {
let target = canonicalize(&self.path)
.with_context(|| format!("getting absolute path to {}", self.path))?;
let devdir = basedir.join(
target
.file_name()
.with_context(|| format!("target {} has no filename", target.display()))?,
);
if devdir.exists() {
return Ok(devdir);
}
}
bail!(
"couldn't find /sys/block directory for partition {} of {}",
&self.path,
&self.parent
);
}
}
#[derive(Debug)]
pub struct Mount {
device: String,
mountpoint: PathBuf,
owned: bool,
}
impl Mount {
pub fn try_mount(device: &str, fstype: &str, flags: mount::MsFlags) -> Result<Mount> {
let tempdir = tempfile::Builder::new()
.prefix("coreos-installer-")
.tempdir()
.context("creating temporary directory")?;
let mountpoint = tempdir.into_path();
sched::unshare(sched::CloneFlags::CLONE_NEWNS).context("unsharing mount namespace")?;
mount::mount::<str, Path, str, str>(Some(device), &mountpoint, Some(fstype), flags, None)
.with_context(|| format!("mounting device {} on {}", device, mountpoint.display()))?;
Ok(Mount {
device: device.to_string(),
mountpoint,
owned: true,
})
}
pub fn from_existing(path: &str) -> Result<Mount> {
let mounts = read_to_string("/proc/self/mounts").context("reading mount table")?;
for line in mounts.lines() {
let mount: Vec<&str> = line.split_whitespace().collect();
if mount.len() != 6 {
bail!("invalid line in /proc/self/mounts: {}", line);
}
if mount[1] == path {
return Ok(Mount {
device: mount[0].to_string(),
mountpoint: path.into(),
owned: false,
});
}
}
bail!("mountpoint {} not found", path);
}
pub fn device(&self) -> &str {
self.device.as_str()
}
pub fn mountpoint(&self) -> &Path {
self.mountpoint.as_path()
}
pub fn get_partition_offsets(&self) -> Result<(u64, u64)> {
Partition::get_offsets(&self.device)
}
pub fn get_filesystem_uuid(&self) -> Result<String> {
let devinfo = lsblk_single(Path::new(&self.device))?;
devinfo
.get("UUID")
.map(String::from)
.with_context(|| format!("filesystem {} has no UUID", self.device))
}
}
impl Drop for Mount {
fn drop(&mut self) {
if !self.owned {
return;
}
for retries in (0..20).rev() {
match mount::umount(&self.mountpoint) {
Ok(_) => break,
Err(err) => {
if retries == 0 {
eprintln!("umounting {}: {}", self.device, err);
return;
} else {
sleep(Duration::from_millis(100));
}
}
}
}
if let Err(err) = remove_dir(&self.mountpoint) {
eprintln!("removing {}: {}", self.mountpoint.display(), err);
}
}
}
#[derive(Debug)]
pub struct SavedPartitions {
sector_size: u64,
partitions: Vec<(u32, GPTPartitionEntry)>,
}
impl SavedPartitions {
pub fn new_from_disk(disk: &mut File, filters: &[PartitionFilter]) -> Result<Self> {
if !disk
.metadata()
.context("getting disk metadata")?
.file_type()
.is_block_device()
{
bail!("specified file is not a block device");
}
Self::new(disk, get_sector_size(disk)?.get() as u64, filters)
}
#[cfg(test)]
pub fn new_from_file(
disk: &mut File,
sector_size: u64,
filters: &[PartitionFilter],
) -> Result<Self> {
if disk
.metadata()
.context("getting disk metadata")?
.file_type()
.is_block_device()
{
bail!("called new_from_file() on a block device");
}
match sector_size {
512 | 4096 => (),
_ => bail!("specified unreasonable sector size {}", sector_size),
}
Self::new(disk, sector_size, filters)
}
fn new(disk: &mut File, sector_size: u64, filters: &[PartitionFilter]) -> Result<Self> {
if filters.is_empty() {
return Ok(Self {
sector_size,
partitions: Vec::new(),
});
}
let gpt = match GPT::find_from(disk) {
Ok(gpt) => gpt,
Err(gptman::Error::InvalidSignature) => {
if filters
.iter()
.any(|f| matches!(f, PartitionFilter::Index(_, _)))
&& disk_has_mbr(disk).context("checking if disk has an MBR")?
{
bail!("saving partitions from an MBR disk is not yet supported");
}
return Ok(Self {
sector_size,
partitions: Vec::new(),
});
}
Err(e) => return Err(e).context("reading partition table"),
};
Self::verify_gpt_sector_size(&gpt, sector_size)?;
let mut partitions = Vec::new();
for (i, p) in gpt.iter() {
if Self::matches_filters(i, p, filters) {
partitions.push((i, p.clone()));
}
}
let result = Self {
sector_size,
partitions,
};
if !result.partitions.is_empty() {
let len = disk.seek(SeekFrom::End(0)).context("getting disk size")?;
let mut temp = tempfile::tempfile().context("creating dry run image")?;
temp.set_len(len)
.with_context(|| format!("setting test image size to {}", len))?;
result.overwrite(&mut temp).context(
"failed dry run restoring saved partitions; input partition table may be invalid",
)?;
}
Ok(result)
}
fn verify_disk_sector_size(&self, disk: &File) -> Result<()> {
if !disk
.metadata()
.context("getting disk metadata")?
.file_type()
.is_block_device()
{
return Ok(());
}
let disk_sector_size = get_sector_size(disk)?.get() as u64;
if disk_sector_size != self.sector_size {
bail!(
"disk sector size {} doesn't match expected {}",
disk_sector_size,
self.sector_size
);
}
Ok(())
}
fn verify_gpt_sector_size(gpt: &GPT, sector_size: u64) -> Result<()> {
if gpt.sector_size != sector_size {
bail!(
"GPT sector size {} doesn't match expected {}",
gpt.sector_size,
sector_size
);
}
Ok(())
}
fn matches_filters(i: u32, p: &GPTPartitionEntry, filters: &[PartitionFilter]) -> bool {
use PartitionFilter::*;
if !p.is_used() {
return false;
}
filters.iter().any(|f| match f {
Index(Some(first), _) if first.get() > i => false,
Index(_, Some(last)) if last.get() < i => false,
Index(_, _) => true,
Label(glob) if glob.matches(p.partition_name.as_str()) => true,
_ => false,
})
}
pub fn overwrite(&self, disk: &mut File) -> Result<()> {
self.verify_disk_sector_size(disk)?;
let mut gpt = GPT::new_from(disk, self.sector_size, *Uuid::new_v4().as_bytes())
.context("creating new GPT")?;
for (i, p) in &self.partitions {
gpt[*i] = p.clone();
}
gpt.write_into(disk).context("writing new GPT")?;
disk.seek(SeekFrom::Start(0)).context("seeking to MBR")?;
disk.write(&[0u8; 446])
.context("overwriting MBR boot code")?;
if self.sector_size > 512 {
disk.seek(SeekFrom::Start(512))
.context("seeking to end of MBR")?;
disk.write(&vec![0u8; self.sector_size as usize - 512])
.context("overwriting end of MBR")?;
}
GPT::write_protective_mbr_into(disk, self.sector_size).context("writing protective MBR")?;
Ok(())
}
pub fn merge(&self, source: &mut (impl Read + Seek), disk: &mut File) -> Result<()> {
if self.partitions.is_empty() {
return Ok(());
}
self.verify_disk_sector_size(disk)?;
let mut gpt =
GPT::find_from(source).context("couldn't read partition table from source")?;
Self::verify_gpt_sector_size(&gpt, self.sector_size)?;
gpt.header
.update_from(disk, self.sector_size)
.context("updating GPT header")?;
let mut next = gpt
.iter()
.fold(1, |prev, (i, e)| if e.is_used() { i + 1 } else { prev });
for (i, p) in &self.partitions {
next = next.max(*i);
eprintln!(
"Saving partition {} (\"{}\") to new partition {}",
i, p.partition_name, next
);
gpt[next] = p.clone();
next += 1;
}
gpt.write_into(disk).context("writing updated GPT")?;
GPT::write_protective_mbr_into(disk, self.sector_size).context("writing protective MBR")?;
Ok(())
}
pub fn get_sector_size(&self) -> u64 {
self.sector_size
}
pub fn get_offset(&self) -> Result<Option<(u64, String)>> {
match self.partitions.iter().min_by_key(|(_, p)| p.starting_lba) {
None => Ok(None),
Some((i, p)) => Ok(Some((
p.starting_lba
.checked_mul(self.sector_size)
.context("overflow calculating partition start")?,
format!("partition {} (\"{}\")", i, p.partition_name.as_str()),
))),
}
}
pub fn is_saved(&self) -> bool {
!self.partitions.is_empty()
}
}
fn read_sysfs_dev_block_value_u64(maj: u64, min: u64, field: &str) -> Result<u64> {
let s = read_sysfs_dev_block_value(maj, min, field).with_context(|| {
format!(
"reading partition {}:{} {} value from sysfs",
maj, min, field
)
})?;
s.parse().with_context(|| {
format!(
"parsing partition {}:{} {} value \"{}\" as u64",
maj, min, field, &s
)
})
}
fn read_sysfs_dev_block_value(maj: u64, min: u64, field: &str) -> Result<String> {
let path = PathBuf::from(format!("/sys/dev/block/{}:{}/{}", maj, min, field));
Ok(read_to_string(&path)?.trim_end().into())
}
pub fn lsblk_single(dev: &Path) -> Result<HashMap<String, String>> {
let mut devinfos = lsblk(Path::new(dev), false)?;
if devinfos.is_empty() {
bail!("no lsblk results for {}", dev.display());
}
Ok(devinfos.remove(0))
}
fn get_all_filesystems(rereadpt: bool) -> Result<Vec<HashMap<String, String>>> {
if rereadpt {
let mut cmd = Command::new("lsblk");
cmd.arg("--noheadings")
.arg("--nodeps")
.arg("--list")
.arg("--paths")
.arg("--output")
.arg("NAME");
let output = cmd_output(&mut cmd)?;
for dev in output.lines() {
if let Ok(mut fd) = std::fs::File::open(dev) {
let _ = reread_partition_table(&mut fd, false);
}
}
udev_settle()?;
}
blkid()
}
pub fn get_filesystems_with_label(label: &str, rereadpt: bool) -> Result<Vec<String>> {
let mut uuids = HashSet::new();
let result = get_all_filesystems(rereadpt)?
.iter()
.filter(|v| v.get("LABEL").map(|l| l.as_str()) == Some(label))
.filter(|v| match v.get("UUID") {
Some(uuid) => {
if !uuid.is_empty() {
uuids.insert(uuid)
} else {
true
}
}
None => true,
})
.filter_map(|v| v.get("NAME").map(<_>::to_owned))
.collect();
Ok(result)
}
pub fn lsblk(dev: &Path, with_deps: bool) -> Result<Vec<HashMap<String, String>>> {
let mut cmd = Command::new("lsblk");
cmd.arg("--pairs")
.arg("--paths")
.arg("--output")
.arg("NAME,LABEL,FSTYPE,TYPE,MOUNTPOINT,UUID")
.arg(dev);
if !with_deps {
cmd.arg("--nodeps");
}
let output = cmd_output(&mut cmd)?;
let mut result: Vec<HashMap<String, String>> = Vec::new();
for line in output.lines() {
result.push(split_lsblk_line(line));
}
Ok(result)
}
fn split_blkid_line(line: &str) -> HashMap<String, String> {
let (name, data) = match line.find(':') {
Some(n) => line.split_at(n),
None => return HashMap::new(),
};
let (name, data) = (name.trim(), data[1..].trim());
if name.is_empty() {
return HashMap::new();
}
let mut fields = split_lsblk_line(data);
fields.insert("NAME".to_string(), name.to_string());
fields
}
fn blkid() -> Result<Vec<HashMap<String, String>>> {
let mut cmd = Command::new("blkid");
let output = cmd_output(&mut cmd)?;
let mut result: Vec<HashMap<String, String>> = Vec::new();
for line in output.lines() {
result.push(split_blkid_line(line));
}
Ok(result)
}
pub fn find_parent_devices(device: &str) -> Result<Vec<String>> {
let mut cmd = Command::new("lsblk");
cmd.arg("--pairs")
.arg("--paths")
.arg("--inverse")
.arg("--output")
.arg("NAME,TYPE")
.arg(device);
let output = cmd_output(&mut cmd)?;
let mut parents = Vec::new();
for line in output.lines().skip(1) {
let dev = split_lsblk_line(line);
let name = dev
.get("NAME")
.with_context(|| format!("device in hierarchy of {} missing NAME", device))?;
let kind = dev
.get("TYPE")
.with_context(|| format!("device in hierarchy of {} missing TYPE", device))?;
if kind == "disk" {
parents.push(name.clone());
} else if kind == "mpath" {
parents.push(name.clone());
break;
}
}
if parents.is_empty() {
bail!("no parent devices found for {}", device);
}
Ok(parents)
}
pub fn find_colocated_esps(device: &str) -> Result<Vec<String>> {
const ESP_TYPE_GUID: &str = "c12a7328-f81f-11d2-ba4b-00a0c93ec93b";
let parent_devices = find_parent_devices(device)
.with_context(|| format!("while looking for colocated ESPs of '{}'", device))?;
let mut esps = Vec::new();
for parent_device in parent_devices {
let mut cmd = Command::new("lsblk");
cmd.arg("--pairs")
.arg("--paths")
.arg("--output")
.arg("NAME,PARTTYPE")
.arg(parent_device);
for line in cmd_output(&mut cmd)?.lines() {
let dev = split_lsblk_line(line);
if dev.get("PARTTYPE").map(|t| t.as_str()) == Some(ESP_TYPE_GUID) {
esps.push(
dev.get("NAME")
.cloned()
.context("ESP device with missing NAME")?,
)
}
}
}
Ok(esps)
}
pub fn find_efi_vendor_dir(efi_mount: &Mount) -> Result<PathBuf> {
let p = efi_mount.mountpoint().join("EFI");
let mut vendor_dir: Vec<PathBuf> = Vec::new();
for ent in p.read_dir()? {
let ent = ent.with_context(|| format!("reading directory entry in {}", p.display()))?;
if !ent.file_type()?.is_dir() {
continue;
}
let path = ent.path();
if path.join("grub.cfg").is_file() {
vendor_dir.push(path);
}
}
if vendor_dir.len() != 1 {
bail!(
"Expected one vendor dir on {}, got {} ({:?})",
efi_mount.device(),
vendor_dir.len(),
vendor_dir,
);
}
Ok(vendor_dir.pop().unwrap())
}
fn split_lsblk_line(line: &str) -> HashMap<String, String> {
let re = Regex::new(r#"([A-Z-_]+)="([^"]+)""#).unwrap();
let mut fields: HashMap<String, String> = HashMap::new();
for cap in re.captures_iter(line) {
fields.insert(cap[1].to_string(), cap[2].to_string());
}
fields
}
pub fn get_blkdev_deps(device: &Path) -> Result<Vec<PathBuf>> {
let deps = {
let mut p = PathBuf::from("/sys/block");
p.push(
device
.canonicalize()
.with_context(|| format!("canonicalizing {}", device.display()))?
.file_name()
.with_context(|| format!("path {} has no filename", device.display()))?,
);
p.push("slaves");
p
};
let mut ret: Vec<PathBuf> = Vec::new();
let dir_iter = match read_dir(&deps) {
Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(ret),
Err(e) => return Err(e).with_context(|| format!("reading dir {}", &deps.display())),
Ok(it) => it,
};
for ent in dir_iter {
let ent = ent.with_context(|| format!("reading {} entry", &deps.display()))?;
ret.push(Path::new("/dev").join(ent.file_name()));
}
Ok(ret)
}
pub fn get_blkdev_deps_recursing(device: &Path) -> Result<Vec<PathBuf>> {
let mut ret: Vec<PathBuf> = Vec::new();
for dep in get_blkdev_deps(device)? {
ret.extend(get_blkdev_deps_recursing(&dep)?);
ret.push(dep);
}
Ok(ret)
}
fn reread_partition_table(file: &mut File, retry: bool) -> Result<()> {
let fd = file.as_raw_fd();
let max_tries = if retry { 20 } else { 1 };
for retries in (0..max_tries).rev() {
let result = unsafe { ioctl::blkrrpart(fd) };
match result {
Ok(_) => break,
Err(err) if retries == 0 && err == Errno::EINVAL => {
return Err(err)
.context("couldn't reread partition table: device may not support partitions")
}
Err(err) if retries == 0 && err == Errno::EBUSY => {
return Err(err).context("couldn't reread partition table: device is in use")
}
Err(err) if retries == 0 => return Err(err).context("couldn't reread partition table"),
Err(_) => sleep(Duration::from_millis(100)),
}
}
Ok(())
}
pub fn get_sector_size_for_path(device: &Path) -> Result<NonZeroU32> {
let dev = OpenOptions::new()
.read(true)
.open(device)
.with_context(|| format!("opening {:?}", device))?;
if !dev
.metadata()
.with_context(|| format!("getting metadata for {:?}", device))?
.file_type()
.is_block_device()
{
bail!("{:?} is not a block device", device);
}
get_sector_size(&dev)
}
pub fn get_sector_size(file: &File) -> Result<NonZeroU32> {
let fd = file.as_raw_fd();
let mut size: c_int = 0;
match unsafe { ioctl::blksszget(fd, &mut size) } {
Ok(_) => {
let size_u32: u32 = size
.try_into()
.with_context(|| format!("sector size {} doesn't fit in u32", size))?;
NonZeroU32::new(size_u32).context("found sector size of zero")
}
Err(e) => Err(anyhow!(e).context("getting sector size")),
}
}
pub fn get_block_device_size(file: &File) -> Result<NonZeroU64> {
let fd = file.as_raw_fd();
let mut size: libc::size_t = 0;
match unsafe { ioctl::blkgetsize64(fd, &mut size) } {
Ok(_) => NonZeroU64::new(size as u64).context("found block size of zero"),
Err(e) => Err(anyhow!(e).context("getting block size")),
}
}
pub fn get_gpt_size(file: &mut (impl Read + Seek)) -> Result<u64> {
let gpt = GPT::find_from(file).context("reading GPT")?;
Ok(gpt.header.first_usable_lba * gpt.sector_size)
}
fn disk_has_mbr(file: &mut (impl Read + Seek)) -> Result<bool> {
let mut sig = [0u8; 2];
file.seek(SeekFrom::Start(510))
.context("seeking to MBR signature")?;
file.read_exact(&mut sig).context("reading MBR signature")?;
Ok(sig == [0x55, 0xaa])
}
pub fn udev_settle() -> Result<()> {
if !Path::new("/run/udev/control").exists() {
bail!("udevd socket missing; are we running in a container without /run/udev mounted?");
}
sleep(Duration::from_millis(200));
runcmd!("udevadm", "settle")?;
Ok(())
}
pub fn detect_formatted_sector_size(buf: &[u8]) -> Option<NonZeroU32> {
let gpt_magic: &[u8; 8] = b"EFI PART";
if buf.len() >= 520 && buf[512..520] == gpt_magic[..] {
NonZeroU32::new(512)
} else if buf.len() >= 4104 && buf[4096..4104] == gpt_magic[..] {
NonZeroU32::new(4096)
} else {
None
}
}
pub fn is_dasd(device: &str, fd: Option<&mut File>) -> Result<bool> {
let target =
canonicalize(device).with_context(|| format!("getting absolute path to {}", device))?;
if target.to_string_lossy().starts_with("/dev/dasd") {
return Ok(true);
}
let read_magic = |device: &str, disk: &mut File| -> Result<[u8; 4]> {
let offset = disk
.seek(SeekFrom::Current(0))
.with_context(|| format!("saving offset {}", device))?;
disk.seek(SeekFrom::Start(8194))
.with_context(|| format!("seeking {}", device))?;
let mut lbl = [0u8; 4];
disk.read_exact(&mut lbl)
.with_context(|| format!("reading label {}", device))?;
disk.seek(SeekFrom::Start(offset))
.with_context(|| format!("restoring offset {}", device))?;
Ok(lbl)
};
if target.to_string_lossy().starts_with("/dev/vd") {
let cdl_magic = [0xd3, 0xf1, 0xe5, 0xd6];
let lbl = if let Some(t) = fd {
read_magic(device, t)?
} else {
let mut disk = File::open(device).with_context(|| format!("opening {}", device))?;
read_magic(device, &mut disk)?
};
return Ok(cdl_magic == lbl);
}
Ok(false)
}
#[allow(clippy::missing_safety_doc)]
mod ioctl {
use super::c_int;
use nix::{ioctl_none, ioctl_read, ioctl_read_bad, request_code_none};
ioctl_none!(blkrrpart, 0x12, 95);
ioctl_read_bad!(blksszget, request_code_none!(0x12, 104), c_int);
ioctl_read!(blkgetsize64, 0x12, 114, libc::size_t);
}
#[cfg(test)]
mod tests {
use super::*;
use maplit::hashmap;
use std::io::copy;
use tempfile::tempfile;
use xz2::read::XzDecoder;
#[test]
fn lsblk_split() {
assert_eq!(
split_lsblk_line(r#"NAME="sda" LABEL="" FSTYPE="""#),
hashmap! {
String::from("NAME") => String::from("sda"),
}
);
assert_eq!(
split_lsblk_line(r#"NAME="sda1" LABEL="" FSTYPE="vfat""#),
hashmap! {
String::from("NAME") => String::from("sda1"),
String::from("FSTYPE") => String::from("vfat")
}
);
assert_eq!(
split_lsblk_line(r#"NAME="sda2" LABEL="boot" FSTYPE="ext4""#),
hashmap! {
String::from("NAME") => String::from("sda2"),
String::from("LABEL") => String::from("boot"),
String::from("FSTYPE") => String::from("ext4"),
}
);
assert_eq!(
split_lsblk_line(r#"NAME="sda3" LABEL="foo=\x22bar\x22 baz" FSTYPE="ext4""#),
hashmap! {
String::from("NAME") => String::from("sda3"),
String::from("LABEL") => String::from(r#"foo=\x22bar\x22 baz"#),
String::from("FSTYPE") => String::from("ext4"),
}
);
}
#[test]
fn blkid_split() {
assert_eq!(split_blkid_line(r#""#), std::collections::HashMap::new());
assert_eq!(split_blkid_line(r#" : "#), std::collections::HashMap::new());
assert_eq!(
split_blkid_line(r#": UUID="0000""#),
std::collections::HashMap::new()
);
assert_eq!(
split_blkid_line(r#"/dev/empty:"#),
hashmap! {
String::from("NAME") => String::from("/dev/empty")
}
);
assert_eq!(
split_blkid_line(
r#"/dev/mapper/luks-f022921b-0100-4d48-9812-cfa6c225060a: UUID="2ff16ac3-103f-41d4-8e02-03686e255270" BLOCK_SIZE="4096" TYPE="ext4""#
),
hashmap! {
String::from("NAME") => String::from("/dev/mapper/luks-f022921b-0100-4d48-9812-cfa6c225060a"),
String::from("UUID") => String::from("2ff16ac3-103f-41d4-8e02-03686e255270"),
String::from("TYPE") => String::from("ext4"),
String::from("BLOCK_SIZE") => String::from("4096")
}
);
assert_eq!(
split_blkid_line(
r#"/dev/vdb4: UUID="fdc69fb1-d7f3-4696-846e-b2275504f63c" LABEL="crypt_rootfs" TYPE="crypto_LUKS" PARTLABEL="root" PARTUUID="835753cb-d7f0-465e-84db-07860d3da2f6""#
),
hashmap! {
String::from("NAME") => String::from("/dev/vdb4"),
String::from("LABEL") => String::from("crypt_rootfs"),
String::from("UUID") => String::from("fdc69fb1-d7f3-4696-846e-b2275504f63c"),
String::from("TYPE") => String::from("crypto_LUKS"),
String::from("PARTLABEL") => String::from("root"),
String::from("PARTUUID") => String::from("835753cb-d7f0-465e-84db-07860d3da2f6"),
}
);
}
#[test]
fn disk_sector_size_reader() {
struct Test {
name: &'static str,
data: &'static [u8],
compressed: bool,
result: Option<NonZeroU32>,
}
let tests = vec![
Test {
name: "zero-length",
data: b"",
compressed: false,
result: None,
},
Test {
name: "empty-disk",
data: include_bytes!("../fixtures/empty.xz"),
compressed: true,
result: None,
},
Test {
name: "gpt-512",
data: include_bytes!("../fixtures/gpt-512.xz"),
compressed: true,
result: NonZeroU32::new(512),
},
Test {
name: "gpt-4096",
data: include_bytes!("../fixtures/gpt-4096.xz"),
compressed: true,
result: NonZeroU32::new(4096),
},
];
for test in tests {
let data = if test.compressed {
let mut decoder = XzDecoder::new(test.data);
let mut data: Vec<u8> = Vec::new();
decoder.read_to_end(&mut data).expect("decompress failed");
data
} else {
test.data.to_vec()
};
assert_eq!(
detect_formatted_sector_size(&data),
test.result,
"{}",
test.name
);
}
}
#[test]
fn test_saved_partitions() {
use PartitionFilter::*;
let make_part = |i: u32, name: &str, start: u64, end: u64| {
(
i,
GPTPartitionEntry {
partition_type_guid: make_guid("type"),
unique_partition_guid: make_guid(&format!("{} {} {}", name, start, end)),
starting_lba: start * 2048,
ending_lba: end * 2048 - 1,
attribute_bits: 0,
partition_name: name.into(),
},
)
};
let base_parts = vec![
make_part(1, "one", 1, 1024),
make_part(2, "two", 1024, 2048),
make_part(3, "three", 2048, 3072),
make_part(4, "four", 3072, 4096),
make_part(5, "five", 4096, 5120),
make_part(7, "seven", 5120, 6144),
make_part(8, "eight", 6144, 7168),
make_part(9, "nine", 7168, 8192),
make_part(10, "", 8192, 8193),
make_part(11, "", 8193, 8194),
];
let image_parts = vec![
make_part(1, "boot", 1, 384),
make_part(2, "EFI-SYSTEM", 384, 512),
make_part(4, "root", 1024, 2200),
];
let merge_base_parts = vec![make_part(2, "unused", 500, 3500)];
let index = |i| Some(NonZeroU32::new(i).unwrap());
let label = |l| Label(glob::Pattern::new(l).unwrap());
let tests = vec![
(
vec![Index(index(5), None)],
vec![
make_part(5, "five", 4096, 5120),
make_part(7, "seven", 5120, 6144),
make_part(8, "eight", 6144, 7168),
make_part(9, "nine", 7168, 8192),
make_part(10, "", 8192, 8193),
make_part(11, "", 8193, 8194),
],
vec![
make_part(1, "boot", 1, 384),
make_part(2, "EFI-SYSTEM", 384, 512),
make_part(4, "root", 1024, 2200),
make_part(5, "five", 4096, 5120),
make_part(7, "seven", 5120, 6144),
make_part(8, "eight", 6144, 7168),
make_part(9, "nine", 7168, 8192),
make_part(10, "", 8192, 8193),
make_part(11, "", 8193, 8194),
],
),
(
vec![label("*i*")],
vec![
make_part(5, "five", 4096, 5120),
make_part(8, "eight", 6144, 7168),
make_part(9, "nine", 7168, 8192),
],
vec![
make_part(1, "boot", 1, 384),
make_part(2, "EFI-SYSTEM", 384, 512),
make_part(4, "root", 1024, 2200),
make_part(5, "five", 4096, 5120),
make_part(8, "eight", 6144, 7168),
make_part(9, "nine", 7168, 8192),
],
),
(
vec![
label("six"),
Index(index(7), index(7)),
Index(index(15), None),
],
vec![make_part(7, "seven", 5120, 6144)],
vec![
make_part(1, "boot", 1, 384),
make_part(2, "EFI-SYSTEM", 384, 512),
make_part(4, "root", 1024, 2200),
make_part(7, "seven", 5120, 6144),
],
),
(
vec![label("")],
vec![make_part(10, "", 8192, 8193), make_part(11, "", 8193, 8194)],
vec![
make_part(1, "boot", 1, 384),
make_part(2, "EFI-SYSTEM", 384, 512),
make_part(4, "root", 1024, 2200),
make_part(10, "", 8192, 8193),
make_part(11, "", 8193, 8194),
],
),
(
vec![Index(index(4), None)],
vec![
make_part(4, "four", 3072, 4096),
make_part(5, "five", 4096, 5120),
make_part(7, "seven", 5120, 6144),
make_part(8, "eight", 6144, 7168),
make_part(9, "nine", 7168, 8192),
make_part(10, "", 8192, 8193),
make_part(11, "", 8193, 8194),
],
vec![
make_part(1, "boot", 1, 384),
make_part(2, "EFI-SYSTEM", 384, 512),
make_part(4, "root", 1024, 2200),
make_part(5, "four", 3072, 4096),
make_part(6, "five", 4096, 5120),
make_part(7, "seven", 5120, 6144),
make_part(8, "eight", 6144, 7168),
make_part(9, "nine", 7168, 8192),
make_part(10, "", 8192, 8193),
make_part(11, "", 8193, 8194),
],
),
(
vec![Index(index(15), None)],
vec![],
merge_base_parts.clone(),
),
(vec![], vec![], merge_base_parts.clone()),
];
let mut base = make_disk(512, &base_parts);
let mut image = make_disk(512, &image_parts);
for (testnum, (filter, expected_blank, expected_image)) in tests.iter().enumerate() {
let saved = SavedPartitions::new_from_file(&mut base, 512, filter).unwrap();
let mut disk = make_unformatted_disk();
saved.overwrite(&mut disk).unwrap();
assert!(disk_has_mbr(&mut disk).unwrap(), "test {}", testnum);
let result = GPT::find_from(&mut disk).unwrap();
assert_eq!(
get_gpt_size(&mut disk).unwrap(),
512 * result.header.first_usable_lba
);
assert_partitions_eq(expected_blank, &result, &format!("test {} blank", testnum));
let mut disk = make_disk(512, &merge_base_parts);
saved.merge(&mut image, &mut disk).unwrap();
assert!(
disk_has_mbr(&mut disk).unwrap() == !expected_blank.is_empty(),
"test {}",
testnum
);
let result = GPT::find_from(&mut disk).unwrap();
assert_eq!(
get_gpt_size(&mut disk).unwrap(),
512 * result.header.first_usable_lba
);
assert_partitions_eq(expected_image, &result, &format!("test {} image", testnum));
assert_eq!(
saved.get_offset().unwrap(),
match expected_blank.is_empty() {
true => None,
false => {
let (i, p) = &expected_blank[0];
Some((
p.starting_lba * 512,
format!("partition {} (\"{}\")", i, p.partition_name.as_str()),
))
}
},
"test {}",
testnum
);
}
for sector_size in [512 as usize, 4096 as usize].iter() {
let mut disk = make_unformatted_disk();
disk.write_all(&vec![0xdau8; *sector_size]).unwrap();
let saved =
SavedPartitions::new_from_file(&mut disk, *sector_size as u64, &vec![]).unwrap();
saved.overwrite(&mut disk).unwrap();
assert!(disk_has_mbr(&mut disk).unwrap(), "{}", *sector_size);
disk.seek(SeekFrom::Start(0)).unwrap();
let mut buf = vec![0u8; *sector_size + 1];
disk.read_exact(&mut buf).unwrap();
assert_eq!(
buf.iter().position(|v| *v == 0xda),
None,
"{}",
*sector_size
);
assert_eq!(buf[*sector_size], 0x45u8, "{}", *sector_size);
}
let mut disk = make_unformatted_disk();
let saved = SavedPartitions::new_from_file(&mut disk, 512, &vec![label("z")]).unwrap();
let mut disk = make_disk(512, &merge_base_parts);
saved.merge(&mut image, &mut disk).unwrap();
let result = GPT::find_from(&mut disk).unwrap();
assert_partitions_eq(&merge_base_parts, &result, "unformatted disk");
let saved =
SavedPartitions::new_from_file(&mut base, 512, &vec![Index(index(1), index(1))])
.unwrap();
let mut disk = make_disk(512, &merge_base_parts);
let err = saved.merge(&mut image, &mut disk).unwrap_err();
assert!(
format!("{:#}", err).contains(&gptman::Error::InvalidPartitionBoundaries.to_string()),
"incorrect error: {:#}",
err
);
let mut disk = make_unformatted_disk();
gptman::GPT::write_protective_mbr_into(&mut disk, 512).unwrap();
SavedPartitions::new(&mut disk, 512, &vec![label("*i*")]).unwrap();
assert_eq!(
SavedPartitions::new(&mut disk, 512, &vec![Index(index(1), index(1))])
.unwrap_err()
.to_string(),
"saving partitions from an MBR disk is not yet supported"
);
assert_eq!(
SavedPartitions::new(
&mut disk,
512,
&vec![Index(index(1), index(1)), label("*i*")]
)
.unwrap_err()
.to_string(),
"saving partitions from an MBR disk is not yet supported"
);
let saved = SavedPartitions::new_from_file(&mut base, 512, &vec![label("*i*")]).unwrap();
let mut image_4096 = make_disk(4096, &image_parts);
assert_eq!(
get_gpt_size(&mut image_4096).unwrap(),
4096 * GPT::find_from(&mut image_4096)
.unwrap()
.header
.first_usable_lba
);
let mut disk = make_disk(4096, &merge_base_parts);
assert_eq!(
saved
.merge(&mut image_4096, &mut disk)
.unwrap_err()
.to_string(),
"GPT sector size 4096 doesn't match expected 512"
);
let mut disk = make_unformatted_disk();
let data = include_bytes!("../fixtures/gpt-512-duplicate-partition-guids.xz");
copy(&mut XzDecoder::new(&data[..]), &mut disk).unwrap();
assert_eq!(
SavedPartitions::new_from_file(&mut disk, 512, &vec![label("*")])
.unwrap_err()
.to_string(),
"failed dry run restoring saved partitions; input partition table may be invalid"
);
for sector_size in &[512, 4096] {
let sector_size: u64 = *sector_size;
let mut disk = make_damaged_disk(sector_size, &base_parts, false, true);
let saved = SavedPartitions::new_from_file(&mut disk, sector_size, &vec![]).unwrap();
assert!(!saved.is_saved());
let saved = SavedPartitions::new_from_file(&mut disk, sector_size, &vec![label("one")])
.unwrap();
assert!(saved.is_saved());
let mut disk = make_damaged_disk(sector_size, &base_parts, true, false);
let saved = SavedPartitions::new_from_file(&mut disk, sector_size, &vec![]).unwrap();
assert!(!saved.is_saved());
let saved = SavedPartitions::new_from_file(&mut disk, sector_size, &vec![label("one")])
.unwrap();
assert!(saved.is_saved());
let mut disk = make_damaged_disk(sector_size, &base_parts, true, true);
let saved = SavedPartitions::new_from_file(&mut disk, sector_size, &vec![]).unwrap();
assert!(!saved.is_saved());
let err = SavedPartitions::new_from_file(&mut disk, sector_size, &vec![label("one")])
.unwrap_err();
assert!(
format!("{:#}", err).contains("could not read primary header"),
"incorrect error: {:#}",
err
);
}
}
fn make_disk(sector_size: u64, partitions: &Vec<(u32, GPTPartitionEntry)>) -> File {
let mut disk = make_unformatted_disk();
let len = if partitions.is_empty() {
1024 * 1024
} else {
partitions[partitions.len() - 1].1.ending_lba * sector_size + 1024 * 1024
};
disk.set_len(len).unwrap();
let mut gpt = GPT::new_from(&mut disk, sector_size, make_guid("disk")).unwrap();
for (partnum, entry) in partitions {
gpt[*partnum] = entry.clone();
}
gpt.write_into(&mut disk).unwrap();
disk.set_len(10 * 1024 * 1024 * 1024).unwrap();
disk
}
fn make_unformatted_disk() -> File {
let disk = tempfile().unwrap();
disk.set_len(10 * 1024 * 1024 * 1024).unwrap();
disk
}
fn make_damaged_disk(
sector_size: u64,
partitions: &Vec<(u32, GPTPartitionEntry)>,
damage_primary: bool,
damage_backup: bool,
) -> File {
let mut disk = make_unformatted_disk();
let mut gpt = GPT::new_from(&mut disk, sector_size, make_guid("disk")).unwrap();
for (partnum, entry) in partitions {
gpt[*partnum] = entry.clone();
gpt[*partnum].starting_lba /= sector_size / 512;
gpt[*partnum].ending_lba /= sector_size / 512;
}
gpt.write_into(&mut disk).unwrap();
if damage_primary {
disk.seek(SeekFrom::Start(gpt.header.primary_lba * sector_size + 16))
.unwrap();
disk.write_all(&[0x15, 0xcd, 0x5b, 0x07]).unwrap();
}
if damage_backup {
disk.seek(SeekFrom::Start(gpt.header.backup_lba * sector_size + 16))
.unwrap();
disk.write_all(&[0xb1, 0x68, 0xde, 0x3a]).unwrap();
}
disk
}
fn make_guid(seed: &str) -> [u8; 16] {
let mut guid = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15];
for (i, b) in seed.as_bytes().iter().enumerate() {
guid[i % guid.len()] ^= *b;
}
guid
}
fn assert_partitions_eq(expected: &Vec<(u32, GPTPartitionEntry)>, found: &GPT, message: &str) {
assert_eq!(
expected
.iter()
.map(|(i, p)| (*i, p))
.collect::<Vec<(u32, &GPTPartitionEntry)>>(),
found
.iter()
.filter(|(_, p)| p.is_used())
.collect::<Vec<(u32, &GPTPartitionEntry)>>(),
"{}",
message
);
}
}