1 use std
::{fs, io, path, process::Command}
;
3 use anyhow
::{bail, Result}
;
4 use clap
::{Args, Parser, Subcommand, ValueEnum}
;
5 use nix
::mount
::{mount, umount, MsFlags}
;
6 use proxmox_installer_common
::{
8 setup
::{InstallConfig, SetupInfo}
,
12 const ANSWER_MP
: &str = "answer";
13 static BINDMOUNTS
: [&str; 4] = ["dev", "proc", "run", "sys"];
14 const TARGET_DIR
: &str = "/target";
15 const ZPOOL_NAME
: &str = "rpool";
17 /// Helper tool to prepare eveything to `chroot` into an installation
18 #[derive(Parser, Debug)]
19 #[command(author, version, about, long_about = None)]
21 #[command(subcommand)]
25 #[derive(Subcommand, Debug)]
27 Prepare(CommandPrepare
),
28 Cleanup(CommandCleanup
),
31 /// Mount the root file system and bind mounts in preparation to chroot into the installation
32 #[derive(Args, Debug)]
33 struct CommandPrepare
{
34 /// Filesystem used for the installation. Will try to automatically detect it after a
35 /// successful installation.
36 #[arg(short, long, value_enum)]
37 filesystem
: Option
<Filesystems
>,
39 /// Numerical ID of `rpool` ZFS pool to import. Needed if multiple pools of name `rpool` are present.
41 rpool_id
: Option
<u64>,
43 /// UUID of the BTRFS file system to mount. Needed if multiple BTRFS file systems are present.
45 btrfs_uuid
: Option
<String
>,
48 /// Unmount everything. Use once done with chroot.
49 #[derive(Args, Debug)]
50 struct CommandCleanup
{
51 /// Filesystem used for the installation. Will try to automatically detect it by default.
52 #[arg(short, long, value_enum)]
53 filesystem
: Option
<Filesystems
>,
56 #[derive(Copy, Clone, Debug, ValueEnum)]
64 impl From
<FsType
> for Filesystems
{
65 fn from(fs
: FsType
) -> Self {
67 FsType
::Xfs
=> Self::Xfs
,
68 FsType
::Ext4
=> Self::Ext4
,
69 FsType
::Zfs(_
) => Self::Zfs
,
70 FsType
::Btrfs(_
) => Self::Btrfs
,
76 let args
= Cli
::parse();
77 let res
= match &args
.command
{
78 Commands
::Prepare(args
) => prepare(args
),
79 Commands
::Cleanup(args
) => cleanup(args
),
81 if let Err(err
) = res
{
83 std
::process
::exit(1);
87 fn prepare(args
: &CommandPrepare
) -> Result
<()> {
88 let fs
= get_fs(args
.filesystem
)?
;
90 fs
::create_dir_all(TARGET_DIR
)?
;
93 Filesystems
::Zfs
=> mount_zpool(args
.rpool_id
)?
,
94 Filesystems
::Xfs
=> mount_fs()?
,
95 Filesystems
::Ext4
=> mount_fs()?
,
96 Filesystems
::Btrfs
=> mount_btrfs(args
.btrfs_uuid
.clone())?
,
99 if let Err(e
) = bindmount() {
103 println
!("Done. You can now use 'chroot /target /bin/bash'!");
107 fn cleanup(args
: &CommandCleanup
) -> Result
<()> {
108 let fs
= get_fs(args
.filesystem
)?
;
110 if let Err(e
) = bind_umount() {
115 Filesystems
::Zfs
=> umount_zpool(),
116 Filesystems
::Xfs
=> umount_fs()?
,
117 Filesystems
::Ext4
=> umount_fs()?
,
121 println
!("Chroot cleanup done. You can now reboot or leave the shell.");
125 fn get_fs(filesystem
: Option
<Filesystems
>) -> Result
<Filesystems
> {
126 let fs
= match filesystem
{
128 let low_level_config
= match get_low_level_config() {
130 Err(_
) => bail
!("Could not fetch config from previous installation. Please specify file system with -f."),
132 Filesystems
::from(low_level_config
.filesys
)
140 fn get_low_level_config() -> Result
<InstallConfig
> {
141 let file
= fs
::File
::open("/tmp/low-level-config.json")?
;
142 let reader
= io
::BufReader
::new(file
);
143 let config
: InstallConfig
= serde_json
::from_reader(reader
)?
;
147 fn get_iso_info() -> Result
<SetupInfo
> {
148 let file
= fs
::File
::open("/run/proxmox-installer/iso-info.json")?
;
149 let reader
= io
::BufReader
::new(file
);
150 let setup_info
: SetupInfo
= serde_json
::from_reader(reader
)?
;
154 fn mount_zpool(pool_id
: Option
<u64>) -> Result
<()> {
155 println
!("importing ZFS pool to {TARGET_DIR}");
156 let mut import
= Command
::new("zpool");
157 import
.arg("import").args(["-R", TARGET_DIR
]);
160 import
.arg(ZPOOL_NAME
);
163 import
.arg(id
.to_string());
166 match import
.status() {
167 Ok(s
) if !s
.success() => bail
!("Could not import ZFS pool. Abort!"),
170 println
!("successfully imported ZFS pool to {TARGET_DIR}");
175 match Command
::new("zpool").arg("export").arg(ZPOOL_NAME
).status() {
176 Ok(s
) if !s
.success() => println
!("failure on exporting {ZPOOL_NAME}"),
181 fn mount_fs() -> Result
<()> {
182 let iso_info
= get_iso_info()?
;
183 let product
= iso_info
.config
.product
;
185 println
!("Activating VG '{product}'");
186 let res
= Command
::new("vgchange")
188 .arg(product
.to_string())
191 Err(e
) => bail
!("{e}"),
193 if output
.status
.success() {
195 "successfully activated VG '{product}': {}",
196 String
::from_utf8(output
.stdout
)?
200 "activation of VG '{product}' failed: {}",
201 String
::from_utf8(output
.stderr
)?
207 match Command
::new("mount")
208 .arg(format
!("/dev/mapper/{product}-root"))
212 Err(e
) => bail
!("{e}"),
214 if output
.status
.success() {
215 println
!("mounted root file system successfully");
218 "mounting of root file system failed: {}",
219 String
::from_utf8(output
.stderr
)?
228 fn umount_fs() -> Result
<()> {
233 fn mount_btrfs(btrfs_uuid
: Option
<String
>) -> Result
<()> {
234 let uuid
= match btrfs_uuid
{
236 None
=> get_btrfs_uuid()?
,
239 match Command
::new("mount")
245 Err(e
) => bail
!("{e}"),
247 if output
.status
.success() {
248 println
!("mounted BTRFS root file system successfully");
251 "mounting of BTRFS root file system failed: {}",
252 String
::from_utf8(output
.stderr
)?
261 fn get_btrfs_uuid() -> Result
<String
> {
262 let output
= Command
::new("btrfs")
266 if !output
.status
.success() {
268 "Error checking for BTRFS file systems: {}",
269 String
::from_utf8(output
.stderr
)?
272 let out
= String
::from_utf8(output
.stdout
)?
;
273 let mut uuids
= Vec
::new();
276 Regex
::new(r
"uuid: ([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})$")?
;
277 for line
in out
.lines() {
278 if let Some(cap
) = re_uuid
.captures(line
) {
279 if let Some(uuid
) = cap
.get(1) {
280 uuids
.push(uuid
.as_str());
285 0 => bail
!("Could not find any BTRFS UUID"),
287 let uuid_list
= uuids
289 .fold(String
::new(), |acc
, &arg
| format
!("{acc}\n{arg}"));
290 bail
!("Found {i} UUIDs:{uuid_list}\nPlease specify the UUID to use with the --btrfs-uuid parameter")
297 fn bindmount() -> Result
<()> {
298 println
!("Bind mounting");
299 // https://github.com/nix-rust/nix/blob/7badbee1e388618457ed0d725c1091359f253012/test/test_mount.rs#L19
300 // https://github.com/nix-rust/nix/blob/7badbee1e388618457ed0d725c1091359f253012/test/test_mount.rs#L146
301 const NONE
: Option
<&'
static [u8]> = None
;
303 let flags
= MsFlags
::MS_BIND
;
304 for item
in BINDMOUNTS
{
305 let source
= path
::Path
::new("/").join(item
);
306 let target
= path
::Path
::new(TARGET_DIR
).join(item
);
308 println
!("Bindmount {source:?} to {target:?}");
309 mount(Some(source
.as_path()), target
.as_path(), NONE
, flags
, NONE
)?
;
312 let answer_path
= path
::Path
::new("/mnt").join(ANSWER_MP
);
313 if answer_path
.exists() {
314 let target
= path
::Path
::new(TARGET_DIR
).join("mnt").join(ANSWER_MP
);
316 println
!("Create dir {target:?}");
317 fs
::create_dir_all(&target
)?
;
319 println
!("Bindmount {answer_path:?} to {target:?}");
321 Some(answer_path
.as_path()),
331 fn bind_umount() -> Result
<()> {
332 for item
in BINDMOUNTS
{
333 let target
= path
::Path
::new(TARGET_DIR
).join(item
);
334 println
!("Unmounting {target:?}");
335 if let Err(e
) = umount(target
.as_path()) {
340 let answer_target
= path
::Path
::new(TARGET_DIR
).join("mnt").join(ANSWER_MP
);
341 if answer_target
.exists() {
342 println
!("Unmounting and removing answer mountpoint");
343 if let Err(e
) = umount(answer_target
.as_os_str()) {
346 if let Err(e
) = fs
::remove_dir(answer_target
) {