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