]> git.proxmox.com Git - pve-installer.git/blob - proxmox-chroot/src/main.rs
print paths directly with debug, not display
[pve-installer.git] / proxmox-chroot / src / main.rs
1 use std::{fs, io, path, process::Command};
2
3 use anyhow::{bail, Result};
4 use clap::{Args, Parser, Subcommand, ValueEnum};
5 use nix::mount::{mount, umount, MsFlags};
6 use proxmox_installer_common::{
7 options::FsType,
8 setup::{InstallConfig, SetupInfo},
9 };
10 use regex::Regex;
11
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";
16
17 /// Helper tool to prepare eveything to `chroot` into an installation
18 #[derive(Parser, Debug)]
19 #[command(author, version, about, long_about = None)]
20 struct Cli {
21 #[command(subcommand)]
22 command: Commands,
23 }
24
25 #[derive(Subcommand, Debug)]
26 enum Commands {
27 Prepare(CommandPrepare),
28 Cleanup(CommandCleanup),
29 }
30
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>,
38
39 /// Numerical ID of `rpool` ZFS pool to import. Needed if multiple pools of name `rpool` are present.
40 #[arg(long)]
41 rpool_id: Option<u64>,
42
43 /// UUID of the BTRFS file system to mount. Needed if multiple BTRFS file systems are present.
44 #[arg(long)]
45 btrfs_uuid: Option<String>,
46 }
47
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>,
54 }
55
56 #[derive(Copy, Clone, Debug, ValueEnum)]
57 enum Filesystems {
58 Zfs,
59 Ext4,
60 Xfs,
61 Btrfs,
62 }
63
64 impl From<FsType> for Filesystems {
65 fn from(fs: FsType) -> Self {
66 match fs {
67 FsType::Xfs => Self::Xfs,
68 FsType::Ext4 => Self::Ext4,
69 FsType::Zfs(_) => Self::Zfs,
70 FsType::Btrfs(_) => Self::Btrfs,
71 }
72 }
73 }
74
75 fn main() {
76 let args = Cli::parse();
77 let res = match &args.command {
78 Commands::Prepare(args) => prepare(args),
79 Commands::Cleanup(args) => cleanup(args),
80 };
81 if let Err(err) = res {
82 eprintln!("{err}");
83 std::process::exit(1);
84 }
85 }
86
87 fn prepare(args: &CommandPrepare) -> Result<()> {
88 let fs = get_fs(args.filesystem)?;
89
90 fs::create_dir_all(TARGET_DIR)?;
91
92 match fs {
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())?,
97 }
98
99 if let Err(e) = bindmount() {
100 eprintln!("{e}")
101 }
102
103 println!("Done. You can now use 'chroot /target /bin/bash'!");
104 Ok(())
105 }
106
107 fn cleanup(args: &CommandCleanup) -> Result<()> {
108 let fs = get_fs(args.filesystem)?;
109
110 if let Err(e) = bind_umount() {
111 eprintln!("{e}")
112 }
113
114 match fs {
115 Filesystems::Zfs => umount_zpool(),
116 Filesystems::Xfs => umount_fs()?,
117 Filesystems::Ext4 => umount_fs()?,
118 _ => (),
119 }
120
121 println!("Chroot cleanup done. You can now reboot or leave the shell.");
122 Ok(())
123 }
124
125 fn get_fs(filesystem: Option<Filesystems>) -> Result<Filesystems> {
126 let fs = match filesystem {
127 None => {
128 let low_level_config = match get_low_level_config() {
129 Ok(c) => c,
130 Err(_) => bail!("Could not fetch config from previous installation. Please specify file system with -f."),
131 };
132 Filesystems::from(low_level_config.filesys)
133 }
134 Some(fs) => fs,
135 };
136
137 Ok(fs)
138 }
139
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)?;
144 Ok(config)
145 }
146
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)?;
151 Ok(setup_info)
152 }
153
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]);
158 match pool_id {
159 None => {
160 import.arg(ZPOOL_NAME);
161 }
162 Some(id) => {
163 import.arg(id.to_string());
164 }
165 }
166 match import.status() {
167 Ok(s) if !s.success() => bail!("Could not import ZFS pool. Abort!"),
168 _ => (),
169 }
170 println!("successfully imported ZFS pool to {TARGET_DIR}");
171 Ok(())
172 }
173
174 fn umount_zpool() {
175 match Command::new("zpool").arg("export").arg(ZPOOL_NAME).status() {
176 Ok(s) if !s.success() => println!("failure on exporting {ZPOOL_NAME}"),
177 _ => (),
178 }
179 }
180
181 fn mount_fs() -> Result<()> {
182 let iso_info = get_iso_info()?;
183 let product = iso_info.config.product;
184
185 println!("Activating VG '{product}'");
186 let res = Command::new("vgchange")
187 .arg("-ay")
188 .arg(product.to_string())
189 .output();
190 match res {
191 Err(e) => bail!("{e}"),
192 Ok(output) => {
193 if output.status.success() {
194 println!(
195 "successfully activated VG '{product}': {}",
196 String::from_utf8(output.stdout)?
197 );
198 } else {
199 bail!(
200 "activation of VG '{product}' failed: {}",
201 String::from_utf8(output.stderr)?
202 )
203 }
204 }
205 }
206
207 match Command::new("mount")
208 .arg(format!("/dev/mapper/{product}-root"))
209 .arg("/target")
210 .output()
211 {
212 Err(e) => bail!("{e}"),
213 Ok(output) => {
214 if output.status.success() {
215 println!("mounted root file system successfully");
216 } else {
217 bail!(
218 "mounting of root file system failed: {}",
219 String::from_utf8(output.stderr)?
220 )
221 }
222 }
223 }
224
225 Ok(())
226 }
227
228 fn umount_fs() -> Result<()> {
229 umount(TARGET_DIR)?;
230 Ok(())
231 }
232
233 fn mount_btrfs(btrfs_uuid: Option<String>) -> Result<()> {
234 let uuid = match btrfs_uuid {
235 Some(uuid) => uuid,
236 None => get_btrfs_uuid()?,
237 };
238
239 match Command::new("mount")
240 .arg("--uuid")
241 .arg(uuid)
242 .arg("/target")
243 .output()
244 {
245 Err(e) => bail!("{e}"),
246 Ok(output) => {
247 if output.status.success() {
248 println!("mounted BTRFS root file system successfully");
249 } else {
250 bail!(
251 "mounting of BTRFS root file system failed: {}",
252 String::from_utf8(output.stderr)?
253 )
254 }
255 }
256 }
257
258 Ok(())
259 }
260
261 fn get_btrfs_uuid() -> Result<String> {
262 let output = Command::new("btrfs")
263 .arg("filesystem")
264 .arg("show")
265 .output()?;
266 if !output.status.success() {
267 bail!(
268 "Error checking for BTRFS file systems: {}",
269 String::from_utf8(output.stderr)?
270 );
271 }
272 let out = String::from_utf8(output.stdout)?;
273 let mut uuids = Vec::new();
274
275 let re_uuid =
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());
281 }
282 }
283 }
284 match uuids.len() {
285 0 => bail!("Could not find any BTRFS UUID"),
286 i if i > 1 => {
287 let uuid_list = uuids
288 .iter()
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")
291 }
292 _ => (),
293 }
294 Ok(uuids[0].into())
295 }
296
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;
302
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);
307
308 println!("Bindmount {source:?} to {target:?}");
309 mount(Some(source.as_path()), target.as_path(), NONE, flags, NONE)?;
310 }
311
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);
315
316 println!("Create dir {target:?}");
317 fs::create_dir_all(&target)?;
318
319 println!("Bindmount {answer_path:?} to {target:?}");
320 mount(
321 Some(answer_path.as_path()),
322 target.as_path(),
323 NONE,
324 flags,
325 NONE,
326 )?;
327 }
328 Ok(())
329 }
330
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()) {
336 eprintln!("{e}");
337 }
338 }
339
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()) {
344 eprintln!("{e}");
345 }
346 if let Err(e) = fs::remove_dir(answer_target) {
347 eprintln!("{e}");
348 }
349 }
350
351 Ok(())
352 }