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