all lists on lists.proxmox.com
 help / color / mirror / Atom feed
From: Aaron Lauterer <a.lauterer@proxmox.com>
To: pve-devel@lists.proxmox.com
Subject: [pve-devel] [PATCH installer v5 30/36] add proxmox-chroot utility
Date: Tue, 16 Apr 2024 17:33:19 +0200	[thread overview]
Message-ID: <20240416153325.1154224-31-a.lauterer@proxmox.com> (raw)
In-Reply-To: <20240416153325.1154224-1-a.lauterer@proxmox.com>

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@proxmox.com>
---
 Cargo.toml                 |   1 +
 Makefile                   |   5 +-
 proxmox-chroot/Cargo.toml  |  16 ++
 proxmox-chroot/src/main.rs | 356 +++++++++++++++++++++++++++++++++++++
 4 files changed, 377 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@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..c1a4785
--- /dev/null
+++ b/proxmox-chroot/src/main.rs
@@ -0,0 +1,356 @@
+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 = match get_low_level_config() {
+                Ok(c) => c,
+                Err(_) => bail!("Could not fetch config from previous installation. Please specify file system with -f."),
+            };
+            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





  parent reply	other threads:[~2024-04-16 15:34 UTC|newest]

Thread overview: 41+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2024-04-16 15:32 [pve-devel] [PATCH installer v5 00/36] add automated/unattended installation Aaron Lauterer
2024-04-16 15:32 ` [pve-devel] [PATCH installer v5 01/36] tui: common: move InstallConfig struct to common crate Aaron Lauterer
2024-04-16 15:32 ` [pve-devel] [PATCH installer v5 02/36] common: make InstallZfsOption members public Aaron Lauterer
2024-04-16 15:32 ` [pve-devel] [PATCH installer v5 03/36] common: tui: use BTreeMap for predictable ordering Aaron Lauterer
2024-04-16 15:32 ` [pve-devel] [PATCH installer v5 04/36] common: utils: add deserializer for CidrAddress Aaron Lauterer
2024-04-16 15:32 ` [pve-devel] [PATCH installer v5 05/36] common: options: add Deserialize trait Aaron Lauterer
2024-04-16 15:32 ` [pve-devel] [PATCH installer v5 06/36] low-level: add dump-udev command Aaron Lauterer
2024-04-16 15:32 ` [pve-devel] [PATCH installer v5 07/36] add auto-installer crate Aaron Lauterer
2024-04-16 15:32 ` [pve-devel] [PATCH installer v5 08/36] auto-installer: add dependencies Aaron Lauterer
2024-04-16 15:32 ` [pve-devel] [PATCH installer v5 09/36] auto-installer: add answer file definition Aaron Lauterer
2024-04-16 15:32 ` [pve-devel] [PATCH installer v5 10/36] auto-installer: add struct to hold udev info Aaron Lauterer
2024-04-16 15:33 ` [pve-devel] [PATCH installer v5 11/36] auto-installer: add utils Aaron Lauterer
2024-04-16 15:33 ` [pve-devel] [PATCH installer v5 12/36] auto-installer: add simple logging Aaron Lauterer
2024-04-16 15:33 ` [pve-devel] [PATCH installer v5 13/36] auto-installer: add tests for answer file parsing Aaron Lauterer
2024-04-16 15:36   ` [pve-devel] [PATCH installer v5 13/36, follow-up] " Aaron Lauterer
2024-04-16 15:33 ` [pve-devel] [PATCH installer v5 14/36] auto-installer: add auto-installer binary Aaron Lauterer
2024-04-16 15:33 ` [pve-devel] [PATCH installer v5 15/36] auto-installer: add fetch answer binary Aaron Lauterer
2024-04-16 15:33 ` [pve-devel] [PATCH installer v5 16/36] unconfigured: add proxauto as option to start auto installer Aaron Lauterer
2024-04-16 15:33 ` [pve-devel] [PATCH installer v5 17/36] auto-installer: use glob crate for pattern matching Aaron Lauterer
2024-04-16 15:33 ` [pve-devel] [PATCH installer v5 18/36] auto-installer: utils: make get_udev_index functions public Aaron Lauterer
2024-04-16 15:33 ` [pve-devel] [PATCH installer v5 19/36] auto-installer: add proxmox-autoinst-helper tool Aaron Lauterer
2024-04-16 15:33 ` [pve-devel] [PATCH installer v5 20/36] common: add Display trait to ProxmoxProduct Aaron Lauterer
2024-04-16 15:33 ` [pve-devel] [PATCH installer v5 21/36] auto-installer: fetch: add gathering of system identifiers and restructure code Aaron Lauterer
2024-04-16 15:33 ` [pve-devel] [PATCH installer v5 22/36] auto-installer: helper: add subcommand to view indentifiers Aaron Lauterer
2024-04-16 15:33 ` [pve-devel] [PATCH installer v5 23/36] auto-installer: fetch: add http post utility module Aaron Lauterer
2024-04-16 15:33 ` [pve-devel] [PATCH installer v5 24/36] auto-installer: fetch: add http plugin to fetch answer Aaron Lauterer
2024-04-16 15:33 ` [pve-devel] [PATCH installer v5 25/36] control: update build depends for auto installer Aaron Lauterer
2024-04-16 15:33 ` [pve-devel] [PATCH installer v5 26/36] auto installer: factor out fetch-answer and autoinst-helper Aaron Lauterer
2024-04-16 15:33 ` [pve-devel] [PATCH installer v5 27/36] low-level: write low level config to /tmp Aaron Lauterer
2024-04-16 15:33 ` [pve-devel] [PATCH installer v5 28/36] common: add deserializer for FsType Aaron Lauterer
2024-04-16 15:33 ` [pve-devel] [PATCH installer v5 29/36] common: skip target_hd when deserializing InstallConfig Aaron Lauterer
2024-04-16 15:33 ` Aaron Lauterer [this message]
2024-04-16 15:33 ` [pve-devel] [PATCH installer v5 31/36] auto-installer: answer: deny unknown fields Aaron Lauterer
2024-04-16 15:33 ` [pve-devel] [PATCH installer v5 32/36] fetch-answer: move get_answer_file to utils Aaron Lauterer
2024-04-16 15:33 ` [pve-devel] [PATCH installer v5 33/36] auto-installer: utils: define ISO specified settings Aaron Lauterer
2024-04-16 15:33 ` [pve-devel] [PATCH installer v5 34/36] fetch-answer: use ISO specified configurations Aaron Lauterer
2024-04-16 15:33 ` [pve-devel] [PATCH installer v5 35/36] fetch-answer: dpcp: improve logging of steps taken Aaron Lauterer
2024-04-16 15:33 ` [pve-devel] [PATCH installer v5 36/36] autoinst-helper: add prepare-iso subcommand Aaron Lauterer
2024-04-17  5:22 ` [pve-devel] [PATCH installer v5 00/36] add automated/unattended installation Thomas Lamprecht
2024-04-17  7:30   ` Aaron Lauterer
2024-04-17 12:32 ` Aaron Lauterer

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=20240416153325.1154224-31-a.lauterer@proxmox.com \
    --to=a.lauterer@proxmox.com \
    --cc=pve-devel@lists.proxmox.com \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.
Service provided by Proxmox Server Solutions GmbH | Privacy | Legal