[pve-devel] [PATCH v3 30/30] add proxmox-chroot utility
Aaron Lauterer
a.lauterer at proxmox.com
Thu Mar 28 14:50:28 CET 2024
it is meant as a helper utility to prepare an installation for chroot
and clean up afterwards
It tries to determine the used FS from the previous installation, will
do what is necessary to mount/import the root FS to /target. It then
will set up all bind mounts.
Signed-off-by: Aaron Lauterer <a.lauterer at proxmox.com>
---
Cargo.toml | 1 +
Makefile | 5 +-
proxmox-chroot/Cargo.toml | 16 ++
proxmox-chroot/src/main.rs | 353 +++++++++++++++++++++++++++++++++++++
4 files changed, 374 insertions(+), 1 deletion(-)
create mode 100644 proxmox-chroot/Cargo.toml
create mode 100644 proxmox-chroot/src/main.rs
diff --git a/Cargo.toml b/Cargo.toml
index b694d5b..b3afc7c 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -2,6 +2,7 @@
members = [
"proxmox-auto-installer",
"proxmox-autoinst-helper",
+ "proxmox-chroot",
"proxmox-fetch-answer",
"proxmox-installer-common",
"proxmox-tui-installer",
diff --git a/Makefile b/Makefile
index e32d28f..d69dc6f 100644
--- a/Makefile
+++ b/Makefile
@@ -19,6 +19,7 @@ INSTALLER_SOURCES=$(shell git ls-files) country.dat
PREFIX = /usr
BINDIR = $(PREFIX)/bin
USR_BIN := \
+ proxmox-chroot\
proxmox-tui-installer\
proxmox-autoinst-helper\
proxmox-fetch-answer\
@@ -54,6 +55,7 @@ $(BUILDDIR):
proxmox-auto-installer/ \
proxmox-autoinst-helper/ \
proxmox-fetch-answer/ \
+ proxmox-chroot \
proxmox-tui-installer/ \
proxmox-installer-common/ \
test/ \
@@ -127,7 +129,8 @@ cargo-build:
$(CARGO) build --package proxmox-tui-installer --bin proxmox-tui-installer \
--package proxmox-auto-installer --bin proxmox-auto-installer \
--package proxmox-fetch-answer --bin proxmox-fetch-answer \
- --package proxmox-autoinst-helper --bin proxmox-autoinst-helper $(CARGO_BUILD_ARGS)
+ --package proxmox-autoinst-helper --bin proxmox-autoinst-helper \
+ --package proxmox-chroot --bin proxmox-chroot $(CARGO_BUILD_ARGS)
%-banner.png: %-banner.svg
rsvg-convert -o $@ $<
diff --git a/proxmox-chroot/Cargo.toml b/proxmox-chroot/Cargo.toml
new file mode 100644
index 0000000..43b96ff
--- /dev/null
+++ b/proxmox-chroot/Cargo.toml
@@ -0,0 +1,16 @@
+[package]
+name = "proxmox-chroot"
+version = "0.1.0"
+edition = "2021"
+authors = [ "Aaron Lauterer <a.lauterer at proxmox.com>" ]
+license = "AGPL-3"
+exclude = [ "build", "debian" ]
+homepage = "https://www.proxmox.com"
+
+[dependencies]
+anyhow = "1.0"
+clap = { version = "4.0", features = ["derive"] }
+nix = "0.26.1"
+proxmox-installer-common = { path = "../proxmox-installer-common" }
+regex = "1.7"
+serde_json = "1.0"
diff --git a/proxmox-chroot/src/main.rs b/proxmox-chroot/src/main.rs
new file mode 100644
index 0000000..075ddc6
--- /dev/null
+++ b/proxmox-chroot/src/main.rs
@@ -0,0 +1,353 @@
+use std::{fs, io, path, process::Command};
+
+use anyhow::{bail, Result};
+use clap::{Args, Parser, Subcommand, ValueEnum};
+use nix::mount::{mount, umount, MsFlags};
+use proxmox_installer_common::{
+ options::FsType,
+ setup::{InstallConfig, SetupInfo},
+};
+use regex::Regex;
+
+const ANSWER_MP: &str = "answer";
+static BINDMOUNTS: [&str; 4] = ["dev", "proc", "run", "sys"];
+const TARGET_DIR: &str = "/target";
+const ZPOOL_NAME: &str = "rpool";
+
+/// Helper tool to prepare eveything to `chroot` into an installation
+#[derive(Parser, Debug)]
+#[command(author, version, about, long_about = None)]
+struct Cli {
+ #[command(subcommand)]
+ command: Commands,
+}
+
+#[derive(Subcommand, Debug)]
+enum Commands {
+ Prepare(CommandPrepare),
+ Cleanup(CommandCleanup),
+}
+
+/// Mount the root file system and bind mounts in preparation to chroot into the installation
+#[derive(Args, Debug)]
+struct CommandPrepare {
+ /// Filesystem used for the installation. Will try to automatically detect it after a
+ /// successful installation.
+ #[arg(short, long, value_enum)]
+ filesystem: Option<Filesystems>,
+
+ /// Numerical ID of `rpool` ZFS pool to import. Needed if multiple pools of name `rpool` are present.
+ #[arg(long)]
+ rpool_id: Option<u64>,
+
+ /// UUID of the BTRFS file system to mount. Needed if multiple BTRFS file systems are present.
+ #[arg(long)]
+ btrfs_uuid: Option<String>,
+}
+
+/// Unmount everything. Use once done with chroot.
+#[derive(Args, Debug)]
+struct CommandCleanup {
+ /// Filesystem used for the installation. Will try to automatically detect it by default.
+ #[arg(short, long, value_enum)]
+ filesystem: Option<Filesystems>,
+}
+
+#[derive(Copy, Clone, Debug, ValueEnum)]
+enum Filesystems {
+ Zfs,
+ Ext4,
+ Xfs,
+ Btrfs,
+}
+
+impl From<FsType> for Filesystems {
+ fn from(fs: FsType) -> Self {
+ match fs {
+ FsType::Xfs => Self::Xfs,
+ FsType::Ext4 => Self::Ext4,
+ FsType::Zfs(_) => Self::Zfs,
+ FsType::Btrfs(_) => Self::Btrfs,
+ }
+ }
+}
+
+fn main() {
+ let args = Cli::parse();
+ let res = match &args.command {
+ Commands::Prepare(args) => prepare(args),
+ Commands::Cleanup(args) => cleanup(args),
+ };
+ if let Err(err) = res {
+ eprintln!("{err}");
+ std::process::exit(1);
+ }
+}
+
+fn prepare(args: &CommandPrepare) -> Result<()> {
+ let fs = get_fs(args.filesystem)?;
+
+ fs::create_dir_all(TARGET_DIR)?;
+
+ match fs {
+ Filesystems::Zfs => mount_zpool(args.rpool_id)?,
+ Filesystems::Xfs => mount_fs()?,
+ Filesystems::Ext4 => mount_fs()?,
+ Filesystems::Btrfs => mount_btrfs(args.btrfs_uuid.clone())?,
+ }
+
+ if let Err(e) = bindmount() {
+ eprintln!("{e}")
+ }
+
+ println!("Done. You can now use 'chroot /target /bin/bash'!");
+ Ok(())
+}
+
+fn cleanup(args: &CommandCleanup) -> Result<()> {
+ let fs = get_fs(args.filesystem)?;
+
+ if let Err(e) = bind_umount() {
+ eprintln!("{e}")
+ }
+
+ match fs {
+ Filesystems::Zfs => umount_zpool(),
+ Filesystems::Xfs => umount_fs()?,
+ Filesystems::Ext4 => umount_fs()?,
+ _ => (),
+ }
+
+ println!("Chroot cleanup done. You can now reboot or leave the shell.");
+ Ok(())
+}
+
+fn get_fs(filesystem: Option<Filesystems>) -> Result<Filesystems> {
+ let fs = match filesystem {
+ None => {
+ let low_level_config = get_low_level_config()?;
+ Filesystems::from(low_level_config.filesys)
+ }
+ Some(fs) => fs,
+ };
+
+ Ok(fs)
+}
+
+fn get_low_level_config() -> Result<InstallConfig> {
+ let file = fs::File::open("/tmp/low-level-config.json")?;
+ let reader = io::BufReader::new(file);
+ let config: InstallConfig = serde_json::from_reader(reader)?;
+ Ok(config)
+}
+
+fn get_iso_info() -> Result<SetupInfo> {
+ let file = fs::File::open("/run/proxmox-installer/iso-info.json")?;
+ let reader = io::BufReader::new(file);
+ let setup_info: SetupInfo = serde_json::from_reader(reader)?;
+ Ok(setup_info)
+}
+
+fn mount_zpool(pool_id: Option<u64>) -> Result<()> {
+ println!("importing ZFS pool to {TARGET_DIR}");
+ let mut import = Command::new("zpool");
+ import.arg("import").args(["-R", TARGET_DIR]);
+ match pool_id {
+ None => {
+ import.arg(ZPOOL_NAME);
+ }
+ Some(id) => {
+ import.arg(id.to_string());
+ }
+ }
+ match import.status() {
+ Ok(s) if !s.success() => bail!("Could not import ZFS pool. Abort!"),
+ _ => (),
+ }
+ println!("successfully imported ZFS pool to {TARGET_DIR}");
+ Ok(())
+}
+
+fn umount_zpool() {
+ match Command::new("zpool").arg("export").arg(ZPOOL_NAME).status() {
+ Ok(s) if !s.success() => println!("failure on exporting {ZPOOL_NAME}"),
+ _ => (),
+ }
+}
+
+fn mount_fs() -> Result<()> {
+ let iso_info = get_iso_info()?;
+ let product = iso_info.config.product;
+
+ println!("Activating VG '{product}'");
+ let res = Command::new("vgchange")
+ .arg("-ay")
+ .arg(product.to_string())
+ .output();
+ match res {
+ Err(e) => bail!("{e}"),
+ Ok(output) => {
+ if output.status.success() {
+ println!(
+ "successfully activated VG '{product}': {}",
+ String::from_utf8(output.stdout)?
+ );
+ } else {
+ bail!(
+ "activation of VG '{product}' failed: {}",
+ String::from_utf8(output.stderr)?
+ )
+ }
+ }
+ }
+
+ match Command::new("mount")
+ .arg(format!("/dev/mapper/{product}-root"))
+ .arg("/target")
+ .output()
+ {
+ Err(e) => bail!("{e}"),
+ Ok(output) => {
+ if output.status.success() {
+ println!("mounted root file system successfully");
+ } else {
+ bail!(
+ "mounting of root file system failed: {}",
+ String::from_utf8(output.stderr)?
+ )
+ }
+ }
+ }
+
+ Ok(())
+}
+
+fn umount_fs() -> Result<()> {
+ umount(TARGET_DIR)?;
+ Ok(())
+}
+
+fn mount_btrfs(btrfs_uuid: Option<String>) -> Result<()> {
+ let uuid = match btrfs_uuid {
+ Some(uuid) => uuid,
+ None => get_btrfs_uuid()?,
+ };
+
+ match Command::new("mount")
+ .arg("--uuid")
+ .arg(uuid)
+ .arg("/target")
+ .output()
+ {
+ Err(e) => bail!("{e}"),
+ Ok(output) => {
+ if output.status.success() {
+ println!("mounted BTRFS root file system successfully");
+ } else {
+ bail!(
+ "mounting of BTRFS root file system failed: {}",
+ String::from_utf8(output.stderr)?
+ )
+ }
+ }
+ }
+
+ Ok(())
+}
+
+fn get_btrfs_uuid() -> Result<String> {
+ let output = Command::new("btrfs")
+ .arg("filesystem")
+ .arg("show")
+ .output()?;
+ if !output.status.success() {
+ bail!(
+ "Error checking for BTRFS file systems: {}",
+ String::from_utf8(output.stderr)?
+ );
+ }
+ let out = String::from_utf8(output.stdout)?;
+ let mut uuids = Vec::new();
+
+ let re_uuid =
+ Regex::new(r"uuid: ([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})$")?;
+ for line in out.lines() {
+ if let Some(cap) = re_uuid.captures(line) {
+ if let Some(uuid) = cap.get(1) {
+ uuids.push(uuid.as_str());
+ }
+ }
+ }
+ match uuids.len() {
+ 0 => bail!("Could not find any BTRFS UUID"),
+ i if i > 1 => {
+ let uuid_list = uuids
+ .iter()
+ .fold(String::new(), |acc, &arg| format!("{acc}\n{arg}"));
+ bail!("Found {i} UUIDs:{uuid_list}\nPlease specify the UUID to use with the --btrfs-uuid parameter")
+ }
+ _ => (),
+ }
+ Ok(uuids[0].into())
+}
+
+fn bindmount() -> Result<()> {
+ println!("Bind mounting");
+ // https://github.com/nix-rust/nix/blob/7badbee1e388618457ed0d725c1091359f253012/test/test_mount.rs#L19
+ // https://github.com/nix-rust/nix/blob/7badbee1e388618457ed0d725c1091359f253012/test/test_mount.rs#L146
+ const NONE: Option<&'static [u8]> = None;
+
+ let flags = MsFlags::MS_BIND;
+ for item in BINDMOUNTS {
+ let source = path::Path::new("/").join(item);
+ let target = path::Path::new(TARGET_DIR).join(item);
+
+ println!("Bindmount {} to {}", source.display(), target.display());
+ mount(Some(source.as_path()), target.as_path(), NONE, flags, NONE)?;
+ }
+
+ let answer_path = path::Path::new("/mnt").join(ANSWER_MP);
+ if answer_path.exists() {
+ let target = path::Path::new(TARGET_DIR).join("mnt").join(ANSWER_MP);
+
+ println!("Create dir {}", target.display());
+ fs::create_dir_all(&target)?;
+
+ println!(
+ "Bindmount {} to {}",
+ answer_path.display(),
+ target.display()
+ );
+ mount(
+ Some(answer_path.as_path()),
+ target.as_path(),
+ NONE,
+ flags,
+ NONE,
+ )?;
+ }
+ Ok(())
+}
+
+fn bind_umount() -> Result<()> {
+ for item in BINDMOUNTS {
+ let target = path::Path::new(TARGET_DIR).join(item);
+ println!("Unmounting {}", target.display());
+ if let Err(e) = umount(target.as_path()) {
+ eprintln!("{e}");
+ }
+ }
+
+ let answer_target = path::Path::new(TARGET_DIR).join("mnt").join(ANSWER_MP);
+ if answer_target.exists() {
+ println!("Unmounting and removing answer mountpoint");
+ if let Err(e) = umount(answer_target.as_os_str()) {
+ eprintln!("{e}");
+ }
+ if let Err(e) = fs::remove_dir(answer_target) {
+ eprintln!("{e}");
+ }
+ }
+
+ Ok(())
+}
--
2.39.2
More information about the pve-devel
mailing list