]>
Commit | Line | Data |
---|---|---|
9143507d AL |
1 | use anyhow::{bail, Result}; |
2 | use clap::{Args, Parser, Subcommand, ValueEnum}; | |
3 | use glob::Pattern; | |
4 | use regex::Regex; | |
9b9754a5 | 5 | use serde::Serialize; |
01470aae AL |
6 | use std::{ |
7 | collections::BTreeMap, | |
8 | fs, | |
ca8b8ace | 9 | io::{self, Read}, |
01470aae AL |
10 | path::{Path, PathBuf}, |
11 | process::{Command, Stdio}, | |
12 | }; | |
9143507d AL |
13 | |
14 | use proxmox_auto_installer::{ | |
15 | answer::Answer, | |
16 | answer::FilterMatch, | |
d4c43e9d | 17 | sysinfo::SysInfo, |
01470aae | 18 | utils::{ |
d2c9b9fd | 19 | get_matched_udev_indexes, get_nic_list, get_single_udev_index, AutoInstMode, |
01470aae AL |
20 | AutoInstSettings, |
21 | }, | |
9143507d AL |
22 | }; |
23 | ||
95be2375 | 24 | static PROXMOX_ISO_FLAG: &str = "/auto-installer-capable"; |
01470aae AL |
25 | |
26 | /// This tool can be used to prepare a Proxmox installation ISO for automated installations. | |
27 | /// Additional uses are to validate the format of an answer file or to test match filters and | |
9143507d AL |
28 | /// print information on the properties to match against for the current hardware. |
29 | #[derive(Parser, Debug)] | |
30 | #[command(author, version, about, long_about = None)] | |
31 | struct Cli { | |
32 | #[command(subcommand)] | |
33 | command: Commands, | |
34 | } | |
35 | ||
36 | #[derive(Subcommand, Debug)] | |
37 | enum Commands { | |
01470aae | 38 | PrepareIso(CommandPrepareISO), |
9143507d | 39 | ValidateAnswer(CommandValidateAnswer), |
9b9754a5 AL |
40 | DeviceMatch(CommandDeviceMatch), |
41 | DeviceInfo(CommandDeviceInfo), | |
0a733567 | 42 | SystemInfo(CommandSystemInfo), |
9143507d AL |
43 | } |
44 | ||
45 | /// Show device information that can be used for filters | |
46 | #[derive(Args, Debug)] | |
9b9754a5 | 47 | struct CommandDeviceInfo { |
9143507d AL |
48 | /// For which device type information should be shown |
49 | #[arg(name="type", short, long, value_enum, default_value_t=AllDeviceTypes::All)] | |
50 | device: AllDeviceTypes, | |
51 | } | |
52 | ||
53 | /// Test which devices the given filter matches against | |
54 | /// | |
55 | /// Filters support the following syntax: | |
56 | /// ? Match a single character | |
57 | /// * Match any number of characters | |
58 | /// [a], [0-9] Specifc character or range of characters | |
59 | /// [!a] Negate a specific character of range | |
60 | /// | |
61 | /// To avoid globbing characters being interpreted by the shell, use single quotes. | |
62 | /// Multiple filters can be defined. | |
63 | /// | |
64 | /// Examples: | |
65 | /// Match disks against the serial number and device name, both must match: | |
66 | /// | |
95be2375 | 67 | /// proxmox-auto-install-assistant match --filter-match all disk 'ID_SERIAL_SHORT=*2222*' 'DEVNAME=*nvme*' |
9143507d AL |
68 | #[derive(Args, Debug)] |
69 | #[command(verbatim_doc_comment)] | |
9b9754a5 | 70 | struct CommandDeviceMatch { |
9143507d AL |
71 | /// Device type to match the filter against |
72 | r#type: Devicetype, | |
73 | ||
74 | /// Filter in the format KEY=VALUE where the key is the UDEV key and VALUE the filter string. | |
75 | /// Multiple filters are possible, separated by a space. | |
76 | filter: Vec<String>, | |
77 | ||
78 | /// Defines if any filter or all filters must match. | |
79 | #[arg(long, value_enum, default_value_t=FilterMatch::Any)] | |
80 | filter_match: FilterMatch, | |
81 | } | |
82 | ||
83 | /// Validate if an answer file is formatted correctly. | |
84 | #[derive(Args, Debug)] | |
85 | struct CommandValidateAnswer { | |
86 | /// Path to the answer file | |
87 | path: PathBuf, | |
88 | #[arg(short, long, default_value_t = false)] | |
89 | debug: bool, | |
90 | } | |
91 | ||
01470aae AL |
92 | /// Prepare an ISO for automated installation. |
93 | /// | |
94 | /// The final ISO will try to fetch an answer file automatically. It will first search for a | |
528d1268 | 95 | /// partition / file-system called "PROXMOX-INST-SRC" (or lowercase) and a file in the root named |
01470aae AL |
96 | /// "answer.toml". |
97 | /// | |
98 | /// If that is not found, it will try to fetch an answer file via an HTTP Post request. The URL for | |
9aa27fa4 TL |
99 | /// it can be defined for the ISO with the '--url' argument. If not present, it will try to get a |
100 | /// URL from a DHCP option (250, TXT) or by querying a DNS TXT record at | |
528d1268 | 101 | /// 'proxmox-auto-installer.{search domain}'. |
01470aae | 102 | /// |
9aa27fa4 TL |
103 | /// The TLS certificate fingerprint can either be defined via the '--cert-fingerprint' argument or |
104 | /// alternatively via the custom DHCP option (251, TXT) or in a DNS TXT record located at | |
105 | /// 'proxmox-auto-installer-cert-fingerprint.{search domain}'. | |
01470aae AL |
106 | /// |
107 | /// The latter options to provide the TLS fingerprint will only be used if the same method was used | |
108 | /// to retrieve the URL. For example, the DNS TXT record for the fingerprint will only be used, if | |
109 | /// no one was configured with the '--cert-fingerprint' parameter and if the URL was retrieved via | |
110 | /// the DNS TXT record. | |
111 | /// | |
9aa27fa4 | 112 | /// The behavior of how to fetch an answer file can be overridden with the '--fetch-from', |
01470aae | 113 | /// parameter. The answer file can be{n} |
9aa27fa4 | 114 | /// * integrated into the ISO itself ('iso'){n} |
528d1268 TL |
115 | /// * needs to be present in a partition / file-system with the label 'PROXMOX-INST-SRC' |
116 | /// ('partition'){n} | |
117 | /// * get requested via an HTTP Post request ('http'). | |
01470aae AL |
118 | #[derive(Args, Debug)] |
119 | struct CommandPrepareISO { | |
9aa27fa4 TL |
120 | /// Path to the source ISO to prepare |
121 | input: PathBuf, | |
01470aae | 122 | |
9aa27fa4 | 123 | /// Path to store the final ISO to, defaults to auto-generated depending on mode. |
01470aae | 124 | #[arg(short, long)] |
9aa27fa4 | 125 | output: Option<PathBuf>, |
01470aae | 126 | |
9aa27fa4 TL |
127 | /// Where the automatic installer should fetch the answer file from. |
128 | #[arg(long, value_enum)] | |
129 | fetch_from: AutoInstMode, | |
01470aae | 130 | |
9aa27fa4 TL |
131 | /// Include the specified answer file in the ISO. Requires the '--fetch-from' parameter |
132 | /// to be set to 'iso'. | |
133 | #[arg(long)] | |
01470aae AL |
134 | answer_file: Option<PathBuf>, |
135 | ||
136 | /// Specify URL for fetching the answer file via HTTP | |
9aa27fa4 | 137 | #[arg(long)] |
01470aae AL |
138 | url: Option<String>, |
139 | ||
140 | /// Pin the ISO to the specified SHA256 TLS certificate fingerprint. | |
9aa27fa4 | 141 | #[arg(long)] |
01470aae AL |
142 | cert_fingerprint: Option<String>, |
143 | ||
9aa27fa4 TL |
144 | /// Staging directory to use for preparing the new ISO file. Defaults to the directory of the |
145 | /// input ISO file. | |
01470aae AL |
146 | #[arg(long)] |
147 | tmp: Option<String>, | |
148 | } | |
149 | ||
0a733567 TL |
150 | /// Show the system information that can be used to identify a host. |
151 | /// | |
152 | /// The shown information is sent as POST HTTP request when fetching the answer file for the | |
153 | /// automatic installation through HTTP, You can, for example, use this to return a dynamically | |
154 | /// assembled answer file. | |
9b9754a5 | 155 | #[derive(Args, Debug)] |
0a733567 | 156 | struct CommandSystemInfo {} |
9b9754a5 | 157 | |
9143507d AL |
158 | #[derive(Args, Debug)] |
159 | struct GlobalOpts { | |
160 | /// Output format | |
161 | #[arg(long, short, value_enum)] | |
162 | format: OutputFormat, | |
163 | } | |
164 | ||
165 | #[derive(Clone, Debug, ValueEnum, PartialEq)] | |
166 | enum AllDeviceTypes { | |
167 | All, | |
168 | Network, | |
169 | Disk, | |
170 | } | |
171 | ||
172 | #[derive(Clone, Debug, ValueEnum)] | |
173 | enum Devicetype { | |
174 | Network, | |
175 | Disk, | |
176 | } | |
177 | ||
178 | #[derive(Clone, Debug, ValueEnum)] | |
179 | enum OutputFormat { | |
180 | Pretty, | |
181 | Json, | |
182 | } | |
183 | ||
184 | #[derive(Serialize)] | |
185 | struct Devs { | |
186 | disks: Option<BTreeMap<String, BTreeMap<String, String>>>, | |
187 | nics: Option<BTreeMap<String, BTreeMap<String, String>>>, | |
188 | } | |
189 | ||
190 | fn main() { | |
191 | let args = Cli::parse(); | |
192 | let res = match &args.command { | |
01470aae | 193 | Commands::PrepareIso(args) => prepare_iso(args), |
9143507d | 194 | Commands::ValidateAnswer(args) => validate_answer(args), |
9b9754a5 AL |
195 | Commands::DeviceInfo(args) => info(args), |
196 | Commands::DeviceMatch(args) => match_filter(args), | |
0a733567 | 197 | Commands::SystemInfo(args) => show_system_info(args), |
9143507d AL |
198 | }; |
199 | if let Err(err) = res { | |
200 | eprintln!("{err}"); | |
201 | std::process::exit(1); | |
202 | } | |
203 | } | |
204 | ||
9b9754a5 | 205 | fn info(args: &CommandDeviceInfo) -> Result<()> { |
9143507d AL |
206 | let mut devs = Devs { |
207 | disks: None, | |
208 | nics: None, | |
209 | }; | |
210 | ||
211 | if args.device == AllDeviceTypes::Network || args.device == AllDeviceTypes::All { | |
212 | match get_nics() { | |
213 | Ok(res) => devs.nics = Some(res), | |
214 | Err(err) => bail!("Error getting NIC data: {err}"), | |
215 | } | |
216 | } | |
217 | if args.device == AllDeviceTypes::Disk || args.device == AllDeviceTypes::All { | |
218 | match get_disks() { | |
219 | Ok(res) => devs.disks = Some(res), | |
220 | Err(err) => bail!("Error getting disk data: {err}"), | |
221 | } | |
222 | } | |
223 | println!("{}", serde_json::to_string_pretty(&devs).unwrap()); | |
224 | Ok(()) | |
225 | } | |
226 | ||
9b9754a5 | 227 | fn match_filter(args: &CommandDeviceMatch) -> Result<()> { |
9143507d AL |
228 | let devs: BTreeMap<String, BTreeMap<String, String>> = match args.r#type { |
229 | Devicetype::Disk => get_disks().unwrap(), | |
230 | Devicetype::Network => get_nics().unwrap(), | |
231 | }; | |
232 | // parse filters | |
233 | ||
234 | let mut filters: BTreeMap<String, String> = BTreeMap::new(); | |
235 | ||
236 | for f in &args.filter { | |
237 | match f.split_once('=') { | |
238 | Some((key, value)) => { | |
239 | if key.is_empty() || value.is_empty() { | |
240 | bail!("Filter key or value is empty in filter: '{f}'"); | |
241 | } | |
242 | filters.insert(String::from(key), String::from(value)); | |
243 | } | |
244 | None => { | |
245 | bail!("Could not find separator '=' in filter: '{f}'"); | |
246 | } | |
247 | } | |
248 | } | |
249 | ||
250 | // align return values | |
251 | let result = match args.r#type { | |
252 | Devicetype::Disk => { | |
2a777841 | 253 | get_matched_udev_indexes(&filters, &devs, args.filter_match == FilterMatch::All) |
9143507d | 254 | } |
2a777841 | 255 | Devicetype::Network => get_single_udev_index(&filters, &devs).map(|r| vec![r]), |
9143507d AL |
256 | }; |
257 | ||
258 | match result { | |
259 | Ok(result) => println!("{}", serde_json::to_string_pretty(&result).unwrap()), | |
260 | Err(err) => bail!("Error matching filters: {err}"), | |
261 | } | |
262 | Ok(()) | |
263 | } | |
264 | ||
265 | fn validate_answer(args: &CommandValidateAnswer) -> Result<()> { | |
01470aae | 266 | let answer = parse_answer(&args.path)?; |
9143507d AL |
267 | if args.debug { |
268 | println!("Parsed data from answer file:\n{:#?}", answer); | |
269 | } | |
270 | Ok(()) | |
271 | } | |
272 | ||
0a733567 | 273 | fn show_system_info(_args: &CommandSystemInfo) -> Result<()> { |
d4c43e9d | 274 | match SysInfo::as_json_pretty() { |
9b9754a5 | 275 | Ok(res) => println!("{res}"), |
0a733567 | 276 | Err(err) => eprintln!("Error fetching system info: {err}"), |
9b9754a5 AL |
277 | } |
278 | Ok(()) | |
279 | } | |
280 | ||
01470aae AL |
281 | fn prepare_iso(args: &CommandPrepareISO) -> Result<()> { |
282 | check_prepare_requirements(args)?; | |
283 | ||
9aa27fa4 | 284 | if args.fetch_from == AutoInstMode::Included { |
01470aae AL |
285 | if args.answer_file.is_none() { |
286 | bail!("Missing path to answer file needed for 'direct' install mode."); | |
287 | } | |
288 | if args.cert_fingerprint.is_some() { | |
289 | bail!("No certificate fingerprint needed for direct install mode. Drop the parameter!"); | |
290 | } | |
291 | if args.url.is_some() { | |
292 | bail!("No URL needed for direct install mode. Drop the parameter!"); | |
293 | } | |
9aa27fa4 | 294 | } else if args.fetch_from == AutoInstMode::Partition { |
01470aae AL |
295 | if args.cert_fingerprint.is_some() { |
296 | bail!( | |
297 | "No certificate fingerprint needed for partition install mode. Drop the parameter!" | |
298 | ); | |
299 | } | |
300 | if args.url.is_some() { | |
301 | bail!("No URL needed for partition install mode. Drop the parameter!"); | |
302 | } | |
303 | } | |
9aa27fa4 | 304 | if args.answer_file.is_some() && args.fetch_from != AutoInstMode::Included { |
01470aae AL |
305 | bail!("Set '-i', '--install-mode' to 'included' to place the answer file directly in the ISO."); |
306 | } | |
307 | ||
308 | if let Some(file) = &args.answer_file { | |
309 | println!("Checking provided answer file..."); | |
310 | parse_answer(file)?; | |
311 | } | |
312 | ||
11f2e83f TL |
313 | let iso_target = final_iso_location(args); |
314 | let iso_target_file_name = match iso_target.file_name() { | |
315 | None => bail!("no base filename in target ISO path found"), | |
316 | Some(source_file_name) => source_file_name.to_string_lossy(), | |
317 | }; | |
318 | ||
01470aae | 319 | let mut tmp_base = PathBuf::new(); |
a14a9348 TL |
320 | match args.tmp.as_ref() { |
321 | Some(tmp_dir) => tmp_base.push(tmp_dir), | |
11f2e83f | 322 | None => tmp_base.push(iso_target.parent().unwrap()), |
01470aae | 323 | } |
a14a9348 | 324 | |
01470aae | 325 | let mut tmp_iso = tmp_base.clone(); |
a14a9348 TL |
326 | tmp_iso.push(format!("{iso_target_file_name}.tmp",)); |
327 | ||
01470aae | 328 | println!("Copying source ISO to temporary location..."); |
9aa27fa4 | 329 | fs::copy(&args.input, &tmp_iso)?; |
01470aae AL |
330 | |
331 | println!("Preparing ISO..."); | |
9aa27fa4 TL |
332 | let config = AutoInstSettings { |
333 | mode: args.fetch_from.clone(), | |
01470aae AL |
334 | http_url: args.url.clone(), |
335 | cert_fingerprint: args.cert_fingerprint.clone(), | |
336 | }; | |
337 | let mut instmode_file_tmp = tmp_base.clone(); | |
95be2375 | 338 | instmode_file_tmp.push("auto-installer-mode.toml"); |
9aa27fa4 | 339 | fs::write(&instmode_file_tmp, toml::to_string_pretty(&config)?)?; |
01470aae | 340 | |
95be2375 | 341 | inject_file_to_iso(&tmp_iso, &instmode_file_tmp, "/auto-installer-mode.toml")?; |
01470aae | 342 | |
938726d5 | 343 | if let Some(answer_file) = &args.answer_file { |
810c860d | 344 | inject_file_to_iso(&tmp_iso, answer_file, "/answer.toml")?; |
01470aae AL |
345 | } |
346 | ||
a14a9348 | 347 | println!("Moving prepared ISO to target location..."); |
01470aae | 348 | fs::rename(&tmp_iso, &iso_target)?; |
a14a9348 | 349 | println!("Final ISO is available at {iso_target:?}."); |
01470aae AL |
350 | |
351 | Ok(()) | |
352 | } | |
353 | ||
354 | fn final_iso_location(args: &CommandPrepareISO) -> PathBuf { | |
9aa27fa4 | 355 | if let Some(specified) = args.output.clone() { |
01470aae AL |
356 | return specified; |
357 | } | |
9aa27fa4 | 358 | let mut suffix: String = match args.fetch_from { |
d2c9b9fd TL |
359 | AutoInstMode::Http => "auto-http", |
360 | AutoInstMode::Included => "auto-answer-included", | |
361 | AutoInstMode::Partition => "auto-part", | |
f59910eb TL |
362 | } |
363 | .into(); | |
01470aae AL |
364 | |
365 | if args.url.is_some() { | |
366 | suffix.push_str("-url"); | |
367 | } | |
368 | if args.cert_fingerprint.is_some() { | |
369 | suffix.push_str("-fp"); | |
370 | } | |
371 | ||
9aa27fa4 TL |
372 | let base = args.input.parent().unwrap(); |
373 | let iso = args.input.file_stem().unwrap(); | |
01470aae AL |
374 | |
375 | let mut target = base.to_path_buf(); | |
376 | target.push(format!("{}-{}.iso", iso.to_str().unwrap(), suffix)); | |
377 | ||
378 | target.to_path_buf() | |
379 | } | |
380 | ||
381 | fn inject_file_to_iso(iso: &PathBuf, file: &PathBuf, location: &str) -> Result<()> { | |
382 | let result = Command::new("xorriso") | |
383 | .arg("--boot_image") | |
384 | .arg("any") | |
385 | .arg("keep") | |
386 | .arg("-dev") | |
387 | .arg(iso) | |
388 | .arg("-map") | |
389 | .arg(file) | |
390 | .arg(location) | |
391 | .output()?; | |
392 | if !result.status.success() { | |
393 | bail!( | |
f59910eb TL |
394 | "Error injecting {file:?} into {iso:?}: {}", |
395 | String::from_utf8_lossy(&result.stderr) | |
01470aae AL |
396 | ); |
397 | } | |
398 | Ok(()) | |
399 | } | |
400 | ||
9143507d AL |
401 | fn get_disks() -> Result<BTreeMap<String, BTreeMap<String, String>>> { |
402 | let unwantend_block_devs = vec![ | |
403 | "ram[0-9]*", | |
404 | "loop[0-9]*", | |
405 | "md[0-9]*", | |
406 | "dm-*", | |
407 | "fd[0-9]*", | |
408 | "sr[0-9]*", | |
409 | ]; | |
410 | ||
411 | // compile Regex here once and not inside the loop | |
412 | let re_disk = Regex::new(r"(?m)^E: DEVTYPE=disk")?; | |
413 | let re_cdrom = Regex::new(r"(?m)^E: ID_CDROM")?; | |
414 | let re_iso9660 = Regex::new(r"(?m)^E: ID_FS_TYPE=iso9660")?; | |
415 | ||
416 | let re_name = Regex::new(r"(?m)^N: (.*)$")?; | |
a7edd237 | 417 | let re_props = Regex::new(r"(?m)^E: ([^=]+)=(.*)$")?; |
9143507d AL |
418 | |
419 | let mut disks: BTreeMap<String, BTreeMap<String, String>> = BTreeMap::new(); | |
420 | ||
421 | 'outer: for entry in fs::read_dir("/sys/block")? { | |
422 | let entry = entry.unwrap(); | |
423 | let filename = entry.file_name().into_string().unwrap(); | |
424 | ||
425 | for p in &unwantend_block_devs { | |
426 | if Pattern::new(p)?.matches(&filename) { | |
427 | continue 'outer; | |
428 | } | |
429 | } | |
430 | ||
431 | let output = match get_udev_properties(&entry.path()) { | |
432 | Ok(output) => output, | |
433 | Err(err) => { | |
434 | eprint!("{err}"); | |
435 | continue 'outer; | |
436 | } | |
437 | }; | |
438 | ||
439 | if !re_disk.is_match(&output) { | |
440 | continue 'outer; | |
441 | }; | |
442 | if re_cdrom.is_match(&output) { | |
443 | continue 'outer; | |
444 | }; | |
445 | if re_iso9660.is_match(&output) { | |
446 | continue 'outer; | |
447 | }; | |
448 | ||
449 | let mut name = filename; | |
450 | if let Some(cap) = re_name.captures(&output) { | |
451 | if let Some(res) = cap.get(1) { | |
452 | name = String::from(res.as_str()); | |
453 | } | |
454 | } | |
455 | ||
456 | let mut udev_props: BTreeMap<String, String> = BTreeMap::new(); | |
457 | ||
458 | for line in output.lines() { | |
459 | if let Some(caps) = re_props.captures(line) { | |
460 | let key = String::from(caps.get(1).unwrap().as_str()); | |
461 | let value = String::from(caps.get(2).unwrap().as_str()); | |
462 | udev_props.insert(key, value); | |
463 | } | |
464 | } | |
465 | ||
466 | disks.insert(name, udev_props); | |
467 | } | |
468 | Ok(disks) | |
469 | } | |
470 | ||
9143507d AL |
471 | fn get_nics() -> Result<BTreeMap<String, BTreeMap<String, String>>> { |
472 | let re_props = Regex::new(r"(?m)^E: (.*)=(.*)$")?; | |
473 | let mut nics: BTreeMap<String, BTreeMap<String, String>> = BTreeMap::new(); | |
474 | ||
9b9754a5 | 475 | let links = get_nic_list()?; |
9143507d AL |
476 | for link in links { |
477 | let path = format!("/sys/class/net/{link}"); | |
478 | ||
479 | let output = match get_udev_properties(&PathBuf::from(path)) { | |
480 | Ok(output) => output, | |
481 | Err(err) => { | |
482 | eprint!("{err}"); | |
483 | continue; | |
484 | } | |
485 | }; | |
486 | ||
487 | let mut udev_props: BTreeMap<String, String> = BTreeMap::new(); | |
488 | ||
489 | for line in output.lines() { | |
490 | if let Some(caps) = re_props.captures(line) { | |
491 | let key = String::from(caps.get(1).unwrap().as_str()); | |
492 | let value = String::from(caps.get(2).unwrap().as_str()); | |
493 | udev_props.insert(key, value); | |
494 | } | |
495 | } | |
496 | ||
497 | nics.insert(link, udev_props); | |
498 | } | |
499 | Ok(nics) | |
500 | } | |
501 | ||
502 | fn get_udev_properties(path: &PathBuf) -> Result<String> { | |
503 | let udev_output = Command::new("udevadm") | |
504 | .arg("info") | |
505 | .arg("--path") | |
506 | .arg(path) | |
507 | .arg("--query") | |
508 | .arg("all") | |
509 | .output()?; | |
510 | if !udev_output.status.success() { | |
f59910eb | 511 | bail!("could not run udevadm successfully for {path:?}"); |
9143507d AL |
512 | } |
513 | Ok(String::from_utf8(udev_output.stdout)?) | |
514 | } | |
01470aae AL |
515 | |
516 | fn parse_answer(path: &PathBuf) -> Result<Answer> { | |
517 | let mut file = match fs::File::open(path) { | |
518 | Ok(file) => file, | |
f59910eb | 519 | Err(err) => bail!("Opening answer file {path:?} failed: {err}"), |
01470aae AL |
520 | }; |
521 | let mut contents = String::new(); | |
522 | if let Err(err) = file.read_to_string(&mut contents) { | |
f59910eb | 523 | bail!("Reading from file {path:?} failed: {err}"); |
01470aae AL |
524 | } |
525 | match toml::from_str(&contents) { | |
526 | Ok(answer) => { | |
527 | println!("The file was parsed successfully, no syntax errors found!"); | |
528 | Ok(answer) | |
529 | } | |
530 | Err(err) => bail!("Error parsing answer file: {err}"), | |
531 | } | |
532 | } | |
533 | ||
534 | fn check_prepare_requirements(args: &CommandPrepareISO) -> Result<()> { | |
9aa27fa4 | 535 | match Path::try_exists(&args.input) { |
01470aae AL |
536 | Ok(true) => (), |
537 | Ok(false) => bail!("Source file does not exist."), | |
538 | Err(_) => bail!("Source file does not exist."), | |
539 | } | |
540 | ||
541 | match Command::new("xorriso") | |
542 | .arg("-dev") | |
9aa27fa4 | 543 | .arg(&args.input) |
01470aae AL |
544 | .arg("-find") |
545 | .arg(PROXMOX_ISO_FLAG) | |
546 | .stderr(Stdio::null()) | |
547 | .stdout(Stdio::null()) | |
548 | .status() | |
549 | { | |
550 | Ok(v) => { | |
551 | if !v.success() { | |
552 | bail!("The source ISO file is not able to be installed automatically. Please try a more current one."); | |
553 | } | |
554 | } | |
ca8b8ace TL |
555 | Err(err) if err.kind() == io::ErrorKind::NotFound => { |
556 | bail!("Could not find the 'xorriso' binary. Please install it.") | |
557 | } | |
558 | Err(err) => bail!("unexpected error when trying to execute 'xorriso' - {err}"), | |
01470aae AL |
559 | }; |
560 | ||
561 | Ok(()) | |
562 | } |