| // Copyright 2022 The ChromiumOS Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| //! Implement LVM helper functions. |
| |
| use std::fs::{self, File, OpenOptions}; |
| use std::io::{Read, Seek, SeekFrom, Write}; |
| use std::path::{Path, PathBuf}; |
| use std::process::Command; |
| use std::str; |
| |
| use anyhow::{Context, Result}; |
| use log::{info, warn}; |
| |
| use crate::hiberutil::{ |
| checked_command, checked_command_output, is_snapshot_active, stateful_block_partition_one, |
| }; |
| use crate::mmapbuf::MmapBuffer; |
| |
| /// Define the minimum size of a block device sector. |
| const SECTOR_SIZE: usize = 512; |
| /// Define the size of an LVM extent. |
| const LVM_EXTENT_SIZE: i64 = 64 * 1024; |
| |
| /// Helper function to determine if this is a system where the stateful |
| /// partition is running on top of LVM. |
| pub fn is_lvm_system() -> Result<bool> { |
| let partition1 = stateful_block_partition_one()?; |
| let mut file = File::open(&partition1)?; |
| let mut buffer = MmapBuffer::new(4096)?; |
| let buf = buffer.u8_slice_mut(); |
| file.read_exact(buf) |
| .context(format!("Failed to read {}", partition1))?; |
| // LVM systems have a Physical Volume Label header that starts with |
| // "LABELONE" as its magic. If that's found, this is an LVM system. |
| // https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/4/html/cluster_logical_volume_manager/lvm_metadata |
| match str::from_utf8(&buf[512..520]) { |
| Ok(l) => Ok(l == "LABELONE"), |
| Err(_) => Ok(false), |
| } |
| } |
| |
| /// Get the path to the given logical volume. |
| pub fn lv_path(volume_group: &str, name: &str) -> PathBuf { |
| PathBuf::from(format!("/dev/{}/{}", volume_group, name)) |
| } |
| |
| /// Get the volume group name for the stateful block device. |
| pub fn get_vg_name(blockdev: &str) -> Result<String> { |
| let output = checked_command_output(Command::new("/sbin/pvdisplay").args([ |
| "-C", |
| "--noheadings", |
| "-o", |
| "vg_name", |
| blockdev, |
| ])) |
| .context("Cannot run pvdisplay to get volume group name")?; |
| |
| Ok(String::from_utf8_lossy(&output.stdout).trim().to_string()) |
| } |
| |
| /// Determine if the given logical volume exists. |
| pub fn lv_exists(volume_group: &str, name: &str) -> Result<bool> { |
| let volume = format!("{}/{}", volume_group, name); |
| let output = Command::new("/sbin/lvdisplay") |
| .arg(&volume) |
| .output() |
| .context("Failed to get output for child process")?; |
| Ok(output.status.success()) |
| } |
| |
| /// Enumerate all activated logical volumes in the system. |
| pub fn get_active_lvs() -> Result<Vec<String>> { |
| let output = checked_command_output(Command::new("/sbin/lvdisplay").args([ |
| "-C", |
| "--options=name", |
| "--noheadings", |
| ])) |
| .context("Failed to get active LVs")?; |
| let output_string = String::from_utf8_lossy(&output.stdout); |
| let mut elements: Vec<String> = vec![]; |
| output_string.split_whitespace().for_each(|e| { |
| elements.push(e.trim().to_string()); |
| }); |
| |
| Ok(elements) |
| } |
| |
| /// Create a new thinpool volume under the given volume group, with the |
| /// specified name and size. |
| pub fn create_thin_volume(volume_group: &str, size_mb: i64, name: &str) -> Result<()> { |
| // lvcreate --thin -V "${lv_size}M" -n "{name}" "${volume_group}/thinpool" |
| let size = format!("{}M", size_mb); |
| let thinpool = format!("{}/thinpool", volume_group); |
| checked_command( |
| Command::new("/sbin/lvcreate").args(["--thin", "-V", &size, "-n", name, &thinpool]), |
| ) |
| .context("Cannot create logical volume") |
| } |
| |
| /// Take a newly created thin volume and ensure space is fully allocated for it |
| /// from the thinpool. This is destructive to the data on the volume. |
| pub fn thicken_thin_volume<P: AsRef<Path>>(path: P, size_mb: i64) -> Result<()> { |
| let mut file = OpenOptions::new() |
| .write(true) |
| .open(path.as_ref()) |
| .context(format!( |
| "Failed to open thin disk: {}", |
| path.as_ref().display() |
| ))?; |
| let buf = [0u8; SECTOR_SIZE]; |
| let size_bytes = size_mb * 1024 * 1024; |
| let skip = LVM_EXTENT_SIZE - (SECTOR_SIZE as i64); |
| let mut offset = 0; |
| |
| loop { |
| file.write_all(&buf).context("Failed to thicken LV")?; |
| offset += LVM_EXTENT_SIZE; |
| if offset >= size_bytes { |
| break; |
| } |
| file.seek(SeekFrom::Current(skip)).context(format!( |
| "Failed to seek {}/{} in LV", |
| offset + skip, |
| size_bytes |
| ))?; |
| } |
| |
| Ok(()) |
| } |
| |
| pub struct ActivatedLogicalVolume { |
| lv_arg: Option<String>, |
| } |
| |
| impl ActivatedLogicalVolume { |
| pub fn new(vg_name: &str, lv_name: &str) -> Result<Option<Self>> { |
| // If it already exists, don't reactivate it. |
| if fs::metadata(format!("/dev/{}/{}", vg_name, lv_name)).is_ok() { |
| return Ok(None); |
| } |
| |
| let lv_arg = format!("{}/{}", vg_name, lv_name); |
| checked_command(Command::new("/sbin/lvchange").args(["-ay", &lv_arg])) |
| .context("Cannot activate logical volume")?; |
| |
| Ok(Some(Self { |
| lv_arg: Some(lv_arg), |
| })) |
| } |
| |
| /// Don't deactivate the logical volume on drop. |
| pub fn dont_deactivate(&mut self) { |
| self.lv_arg = None; |
| } |
| } |
| |
| impl Drop for ActivatedLogicalVolume { |
| fn drop(&mut self) { |
| if let Some(lv_arg) = self.lv_arg.take() { |
| let r = checked_command(Command::new("/sbin/lvchange").args(["-an", &lv_arg])); |
| |
| match r { |
| Ok(_) => { |
| info!("Deactivated LV {}", lv_arg); |
| } |
| Err(e) => { |
| warn!("Failed to deactivate LV {}: {}", lv_arg, e); |
| } |
| } |
| } |
| } |
| } |
| |
| pub fn activate_physical_lv(lv_name: &str) -> Result<Option<ActivatedLogicalVolume>> { |
| if !is_snapshot_active() { |
| return Ok(None); |
| } |
| |
| let partition1 = stateful_block_partition_one()?; |
| // Assume that a failure to get the VG name indicates a non-LVM system. |
| let vg_name = match get_vg_name(&partition1) { |
| Ok(vg) => vg, |
| Err(_) => { |
| return Ok(None); |
| } |
| }; |
| |
| ActivatedLogicalVolume::new(&vg_name, lv_name) |
| } |