]> git.proxmox.com Git - proxmox-offline-mirror.git/blame - src/bin/proxmox-offline-mirror.rs
cargo fmt
[proxmox-offline-mirror.git] / src / bin / proxmox-offline-mirror.rs
CommitLineData
4e4363f2 1use std::fmt::Display;
9ecde319
FG
2use std::path::Path;
3
23712e9e 4use anyhow::{bail, format_err, Error};
28945c9a
FG
5use proxmox_offline_mirror::config::SubscriptionKey;
6use proxmox_offline_mirror::subscription::{extract_mirror_key, refresh_mirror_key};
9ecde319
FG
7use serde_json::Value;
8
9use proxmox_router::cli::{run_cli_command, CliCommand, CliCommandMap, CliEnvironment};
10use proxmox_schema::api;
11use proxmox_section_config::SectionConfigData;
12use proxmox_sys::linux::tty;
13
8b267808 14use proxmox_offline_mirror::helpers::tty::{
9ecde319
FG
15 read_bool_from_tty, read_selection_from_tty, read_string_from_tty,
16};
8b267808 17use proxmox_offline_mirror::{
e79308e6 18 config::{save_config, MediaConfig, MirrorConfig, SkipConfig},
d035ecb5 19 mirror,
b42cad3b 20 types::{ProductType, MEDIA_ID_SCHEMA, MIRROR_ID_SCHEMA},
9ecde319
FG
21};
22
8b267808
FG
23mod proxmox_offline_mirror_cmds;
24use proxmox_offline_mirror_cmds::*;
9ecde319 25
4e4363f2
FG
26enum Distro {
27 Debian,
28 Pbs,
29 Pmg,
30 Pve,
31 PveCeph,
32}
33
34impl Display for Distro {
35 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
36 match self {
37 Distro::Debian => write!(f, "debian"),
38 Distro::Pbs => write!(f, "pbs"),
39 Distro::Pmg => write!(f, "pmg"),
40 Distro::Pve => write!(f, "pve"),
41 Distro::PveCeph => write!(f, "ceph"),
42 }
43 }
44}
45
46enum Release {
47 Bullseye,
48 Buster,
49}
50
51impl Display for Release {
52 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
53 match self {
54 Release::Bullseye => write!(f, "bullseye"),
55 Release::Buster => write!(f, "buster"),
56 }
57 }
58}
59
60enum DebianVariant {
61 Main,
62 Security,
63 Updates,
64 Backports,
65 Debug,
66}
67
68impl Display for DebianVariant {
69 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
70 match self {
71 DebianVariant::Main => write!(f, "main"),
72 DebianVariant::Security => write!(f, "security"),
73 DebianVariant::Updates => write!(f, "updates"),
74 DebianVariant::Backports => write!(f, "backports"),
75 DebianVariant::Debug => write!(f, "debug"),
76 }
77 }
78}
79
80#[derive(PartialEq)]
81enum ProxmoxVariant {
82 Enterprise,
83 NoSubscription,
84 Test,
85}
86
87impl Display for ProxmoxVariant {
88 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
89 match self {
90 ProxmoxVariant::Enterprise => write!(f, "enterprise"),
91 ProxmoxVariant::NoSubscription => write!(f, "no_subscription"),
92 ProxmoxVariant::Test => write!(f, "test"),
93 }
94 }
95}
96
1f7ac6f6
FG
97fn derive_debian_repo(
98 release: &Release,
99 variant: &DebianVariant,
100 components: &str,
f907fd5e
FG
101) -> Result<(String, String, String, SkipConfig), Error> {
102 println!("Configure filters for Debian mirror {release} / {variant}:");
56ca838b 103 let skip_sections = match read_string_from_tty(
a4a06e8a 104 "\tEnter list of package sections to be skipped ('-' for None)",
56ca838b
WB
105 Some("debug,games"),
106 )?
107 .as_str()
108 {
f907fd5e 109 "-" => None,
56ca838b
WB
110 list => Some(
111 list.split(',')
112 .map(|v| v.trim().to_owned())
113 .collect::<Vec<String>>(),
114 ),
f907fd5e 115 };
56ca838b 116 let skip_packages = match read_string_from_tty(
a4a06e8a 117 "\tEnter list of package names/name globs to be skipped ('-' for None)",
56ca838b
WB
118 None,
119 )?
120 .as_str()
121 {
f907fd5e 122 "-" => None,
56ca838b
WB
123 list => Some(
124 list.split(',')
125 .map(|v| v.trim().to_owned())
126 .collect::<Vec<String>>(),
127 ),
f907fd5e
FG
128 };
129 let filters = SkipConfig {
130 skip_packages,
131 skip_sections,
132 };
54b47e30 133 let url = match (release, variant) {
1f7ac6f6 134 (Release::Bullseye, DebianVariant::Main) => "http://deb.debian.org/debian bullseye",
54b47e30
FG
135 (Release::Bullseye, DebianVariant::Security) => {
136 "http://deb.debian.org/debian-security bullseye-security"
137 }
138 (Release::Bullseye, DebianVariant::Updates) => {
139 "http://deb.debian.org/debian bullseye-updates"
140 }
141 (Release::Bullseye, DebianVariant::Backports) => {
142 "http://deb.debian.org/debian bullseye-backports"
143 }
144 (Release::Bullseye, DebianVariant::Debug) => {
145 "http://deb.debian.org/debian-debug bullseye-debug"
146 }
147 (Release::Buster, DebianVariant::Main) => "http://deb.debian.org/debian buster",
148 (Release::Buster, DebianVariant::Security) => {
149 "http://deb.debian.org/debian-security buster/updates"
150 }
1f7ac6f6 151 (Release::Buster, DebianVariant::Updates) => "http://deb.debian.org/debian buster-updates",
54b47e30
FG
152 (Release::Buster, DebianVariant::Backports) => {
153 "http://deb.debian.org/debian buster-backports"
154 }
155 (Release::Buster, DebianVariant::Debug) => {
156 "http://deb.debian.org/debian-debug buster-debug"
157 }
158 };
159
160 let url = format!("{url} {components}");
161 let key = match (release, variant) {
162 (Release::Bullseye, DebianVariant::Security) => {
163 "/usr/share/keyrings/debian-archive-bullseye-security-automatic.gpg"
164 }
1f7ac6f6 165 (Release::Bullseye, _) => "/usr/share/keyrings/debian-archive-bullseye-automatic.gpg",
54b47e30
FG
166 (Release::Buster, DebianVariant::Security) => {
167 "/usr/share/keyrings/debian-archive-buster-security-automatic.gpg"
168 }
169 (Release::Buster, _) => "/usr/share/keyrings/debian-archive-buster-stable.gpg",
170 };
171
172 let suggested_id = format!("debian_{release}_{variant}");
173
f907fd5e 174 Ok((url, key.to_string(), suggested_id, filters))
54b47e30
FG
175}
176
1f7ac6f6 177fn action_add_mirror(config: &SectionConfigData) -> Result<Vec<MirrorConfig>, Error> {
8b267808 178 let mut use_subscription = None;
1f7ac6f6 179 let mut extra_repos = Vec::new();
8b267808 180
f907fd5e 181 let (repository, key_path, architectures, suggested_id, skip) = if read_bool_from_tty(
4e4363f2
FG
182 "Guided Setup",
183 Some(true),
184 )? {
9ecde319 185 let distros = &[
34ccf5f6 186 (Distro::Pve, "Proxmox VE"),
9ecde319
FG
187 (Distro::Pbs, "Proxmox Backup Server"),
188 (Distro::Pmg, "Proxmox Mail Gateway"),
34ccf5f6
FG
189 (Distro::PveCeph, "Proxmox Ceph"),
190 (Distro::Debian, "Debian"),
9ecde319
FG
191 ];
192 let dist = read_selection_from_tty("Select distro to mirror", distros, None)?;
193
9ecde319
FG
194 let releases = &[(Release::Bullseye, "Bullseye"), (Release::Buster, "Buster")];
195 let release = read_selection_from_tty("Select release", releases, Some(0))?;
196
1f7ac6f6
FG
197 let mut add_debian_repo = false;
198
f907fd5e 199 let (url, key_path, suggested_id, skip) = match dist {
9ecde319 200 Distro::Debian => {
9ecde319
FG
201 let variants = &[
202 (DebianVariant::Main, "Main repository"),
203 (DebianVariant::Security, "Security"),
204 (DebianVariant::Updates, "Updates"),
205 (DebianVariant::Backports, "Backports"),
206 (DebianVariant::Debug, "Debug Information"),
207 ];
208 let variant =
209 read_selection_from_tty("Select repository variant", variants, Some(0))?;
210 let components = read_string_from_tty(
211 "Enter repository components",
212 Some("main contrib non-free"),
213 )?;
214
f907fd5e 215 derive_debian_repo(release, variant, &components)?
9ecde319
FG
216 }
217 Distro::PveCeph => {
218 enum CephRelease {
219 Luminous,
220 Nautilus,
221 Octopus,
222 Pacific,
8b7c7967 223 Quincy,
9ecde319
FG
224 }
225
226 let releases = match release {
227 Release::Bullseye => {
228 vec![
229 (CephRelease::Octopus, "Octopus (15.x)"),
230 (CephRelease::Pacific, "Pacific (16.x)"),
8b7c7967 231 (CephRelease::Quincy, "Quincy (17.x)"),
9ecde319
FG
232 ]
233 }
234 Release::Buster => {
235 vec![
236 (CephRelease::Luminous, "Luminous (12.x)"),
237 (CephRelease::Nautilus, "Nautilus (14.x)"),
238 (CephRelease::Octopus, "Octopus (15.x)"),
239 ]
240 }
241 };
242
243 let ceph_release = read_selection_from_tty(
244 "Select Ceph release",
245 &releases,
246 Some(releases.len() - 1),
247 )?;
248
249 let components =
250 read_string_from_tty("Enter repository components", Some("main test"))?;
251
252 let key = match release {
253 Release::Bullseye => "/etc/apt/trusted.gpg.d/proxmox-release-bullseye.gpg",
254 Release::Buster => "/etc/apt/trusted.gpg.d/proxmox-release-buster.gpg",
255 };
256
9ecde319
FG
257 let ceph_release = match ceph_release {
258 CephRelease::Luminous => "luminous",
259 CephRelease::Nautilus => "nautilus",
260 CephRelease::Octopus => "octopus",
261 CephRelease::Pacific => "pacific",
8b7c7967 262 CephRelease::Quincy => "quincy",
9ecde319
FG
263 };
264
265 let url = format!(
266 "http://download.proxmox.com/debian/ceph-{ceph_release} {release} {components}"
267 );
4e4363f2 268 let suggested_id = format!("ceph_{ceph_release}_{release}");
9ecde319 269
f907fd5e 270 (url, key.to_string(), suggested_id, SkipConfig::default())
9ecde319 271 }
4e4363f2 272 product => {
9ecde319
FG
273 let variants = &[
274 (ProxmoxVariant::Enterprise, "Enterprise repository"),
275 (ProxmoxVariant::NoSubscription, "No-Subscription repository"),
276 (ProxmoxVariant::Test, "Test repository"),
277 ];
278
279 let variant =
280 read_selection_from_tty("Select repository variant", variants, Some(0))?;
281
9ecde319
FG
282 // TODO enterprise query for key!
283 let url = match (release, variant) {
284 (Release::Bullseye, ProxmoxVariant::Enterprise) => format!("https://enterprise.proxmox.com/debian/{product} bullseye {product}-enterprise"),
285 (Release::Bullseye, ProxmoxVariant::NoSubscription) => format!("http://download.proxmox.com/debian/{product} bullseye {product}-no-subscription"),
286 (Release::Bullseye, ProxmoxVariant::Test) => format!("http://download.proxmox.com/debian/{product} bullseye {product}test"),
287 (Release::Buster, ProxmoxVariant::Enterprise) => format!("https://enterprise.proxmox.com/debian/{product} buster {product}-enterprise"),
288 (Release::Buster, ProxmoxVariant::NoSubscription) => format!("http://download.proxmox.com/debian/{product} buster {product}-no-subscription"),
289 (Release::Buster, ProxmoxVariant::Test) => format!("http://download.proxmox.com/debian/{product} buster {product}test"),
290 };
291
4e4363f2 292 use_subscription = match (product, variant) {
8b267808
FG
293 (Distro::Pbs, &ProxmoxVariant::Enterprise) => Some(ProductType::Pbs),
294 (Distro::Pmg, &ProxmoxVariant::Enterprise) => Some(ProductType::Pmg),
295 (Distro::Pve, &ProxmoxVariant::Enterprise) => Some(ProductType::Pve),
296 _ => None,
297 };
298
9ecde319
FG
299 let key = match release {
300 Release::Bullseye => "/etc/apt/trusted.gpg.d/proxmox-release-bullseye.gpg",
301 Release::Buster => "/etc/apt/trusted.gpg.d/proxmox-release-buster.gpg",
302 };
303
4e4363f2
FG
304 let suggested_id = format!("{product}_{release}_{variant}");
305
1f7ac6f6
FG
306 add_debian_repo = read_bool_from_tty(
307 "Should missing Debian mirrors for the selected product be auto-added",
308 Some(true),
309 )?;
310
f907fd5e 311 (url, key.to_string(), suggested_id, SkipConfig::default())
9ecde319
FG
312 }
313 };
314
315 let architectures = vec!["amd64".to_string(), "all".to_string()];
1f7ac6f6
FG
316
317 if add_debian_repo {
318 extra_repos.push(derive_debian_repo(
319 release,
320 &DebianVariant::Main,
321 "main contrib",
f907fd5e 322 )?);
1f7ac6f6
FG
323 extra_repos.push(derive_debian_repo(
324 release,
325 &DebianVariant::Updates,
326 "main contrib",
f907fd5e 327 )?);
1f7ac6f6
FG
328 extra_repos.push(derive_debian_repo(
329 release,
330 &DebianVariant::Security,
331 "main contrib",
f907fd5e 332 )?);
1f7ac6f6
FG
333 }
334 (
335 format!("deb {url}"),
336 key_path,
337 architectures,
338 Some(suggested_id),
f907fd5e 339 skip,
1f7ac6f6 340 )
9ecde319
FG
341 } else {
342 let repo = read_string_from_tty("Enter repository line in sources.list format", None)?;
38b29068 343 let key_path = read_string_from_tty("Enter (absolute) path to repository key file", None)?;
9ecde319
FG
344 let architectures =
345 read_string_from_tty("Enter list of architectures to mirror", Some("amd64,all"))?;
346 let architectures: Vec<String> = architectures
347 .split(|c: char| c == ',' || c.is_ascii_whitespace())
348 .filter_map(|value| {
349 if value.is_empty() {
350 None
351 } else {
352 Some(value.to_owned())
353 }
354 })
355 .collect();
8b267808
FG
356 let subscription_products = &[
357 (Some(ProductType::Pve), "PVE"),
358 (Some(ProductType::Pbs), "PBS"),
359 (Some(ProductType::Pmg), "PMG"),
360 (None, "None"),
361 ];
362 use_subscription = read_selection_from_tty(
363 "Does this repository require a valid Proxmox subscription key",
364 subscription_products,
365 None,
366 )?
367 .clone();
368
f907fd5e 369 (repo, key_path, architectures, None, SkipConfig::default())
9ecde319
FG
370 };
371
372 if !Path::new(&key_path).exists() {
373 eprintln!("Keyfile '{key_path}' doesn't exist - make sure to install relevant keyring packages or update config to provide correct path!");
374 }
375
376 let id = loop {
4e4363f2 377 let mut id = read_string_from_tty("Enter mirror ID", suggested_id.as_deref())?;
9ecde319
FG
378 while let Err(err) = MIRROR_ID_SCHEMA.parse_simple_value(&id) {
379 eprintln!("Not a valid mirror ID: {err}");
380 id = read_string_from_tty("Enter mirror ID", None)?;
381 }
382
383 if config.sections.contains_key(&id) {
384 eprintln!("Config entry '{id}' already exists!");
385 continue;
386 }
387
388 break id;
389 };
390
a085d187
FG
391 let base_dir = loop {
392 let path = read_string_from_tty(
393 "Enter (absolute) base path where mirrored repositories will be stored",
394 Some("/var/lib/proxmox-offline-mirror/mirrors/"),
395 )?;
c76900c9 396 if !path.starts_with('/') {
38b29068 397 eprintln!("Path must start with '/'");
9ecde319
FG
398 } else {
399 break path;
400 }
401 };
402
9ecde319
FG
403 let verify = read_bool_from_tty(
404 "Should already mirrored files be re-verified when updating the mirror? (io-intensive!)",
405 Some(true),
406 )?;
407 let sync = read_bool_from_tty("Should newly written files be written using FSYNC to ensure crash-consistency? (io-intensive!)", Some(true))?;
408
1f7ac6f6
FG
409 let mut configs = Vec::with_capacity(extra_repos.len() + 1);
410
f907fd5e 411 for (url, key_path, suggested_id, skip) in extra_repos {
1f7ac6f6
FG
412 if config.sections.contains_key(&suggested_id) {
413 eprintln!("config section '{suggested_id}' already exists, skipping..");
414 } else {
415 let repository = format!("deb {url}");
a085d187 416
1f7ac6f6
FG
417 configs.push(MirrorConfig {
418 id: suggested_id,
419 repository,
420 architectures: architectures.clone(),
421 key_path,
422 verify,
423 sync,
c598cb15 424 base_dir: base_dir.clone(),
1f7ac6f6 425 use_subscription: None,
96a80415 426 ignore_errors: false,
f907fd5e 427 skip,
fa95f21a 428 weak_crypto: None,
1f7ac6f6
FG
429 });
430 }
431 }
432
433 let main_config = MirrorConfig {
9ecde319
FG
434 id,
435 repository,
436 architectures,
437 key_path,
438 verify,
439 sync,
c598cb15 440 base_dir,
8b267808 441 use_subscription,
96a80415 442 ignore_errors: false,
f907fd5e 443 skip,
fa95f21a 444 weak_crypto: None,
1f7ac6f6
FG
445 };
446
447 configs.push(main_config);
448 Ok(configs)
9ecde319
FG
449}
450
451fn action_add_medium(config: &SectionConfigData) -> Result<MediaConfig, Error> {
9e725bc3 452 let id = loop {
f03a1f2b 453 let id = read_string_from_tty("Enter new medium ID", None)?;
5471a418 454 if let Err(err) = MEDIA_ID_SCHEMA.parse_simple_value(&id) {
9e725bc3 455 eprintln!("Not a valid medium ID: {err}");
5471a418 456 continue;
9e725bc3
FG
457 }
458
459 if config.sections.contains_key(&id) {
460 eprintln!("Config entry '{id}' already exists!");
461 continue;
462 }
463
464 break id;
465 };
466
9ecde319 467 let mountpoint = loop {
38b29068
FG
468 let path = read_string_from_tty("Enter (absolute) path where medium is mounted", None)?;
469 if !path.starts_with('/') {
470 eprintln!("Path must start with '/'");
471 continue;
472 }
473
9ecde319
FG
474 let mountpoint = Path::new(&path);
475 if !mountpoint.exists() {
476 eprintln!("Path doesn't exist.");
477 } else {
478 let mut statefile = mountpoint.to_path_buf();
479 statefile.push(".mirror-state");
480 if !statefile.exists()
481 || read_bool_from_tty(
482 &format!("Found existing statefile at {statefile:?} - proceed?"),
483 Some(false),
484 )?
485 {
486 break path;
487 }
488 }
489 };
490
491 let mirrors: Vec<MirrorConfig> = config.convert_to_typed_array("mirror")?;
492 let mut available_mirrors: Vec<String> = Vec::new();
493 for mirror_config in mirrors {
494 available_mirrors.push(mirror_config.id);
495 }
496
497 let mut selected_mirrors: Vec<String> = Vec::new();
498
499 enum Action {
500 SelectMirror,
fc9b351a 501 SelectAllMirrors,
9ecde319 502 DeselectMirror,
fc9b351a 503 DeselectAllMirrors,
9ecde319
FG
504 Proceed,
505 }
9ecde319
FG
506
507 loop {
508 println!();
9d5bafe6 509 let actions = if selected_mirrors.is_empty() {
38b29068 510 println!("No mirrors selected for inclusion on medium so far.");
9d5bafe6
FG
511 vec![
512 (Action::SelectMirror, "Add mirror to selection."),
fc9b351a 513 (Action::SelectAllMirrors, "Add all mirrors to selection."),
9d5bafe6
FG
514 (Action::Proceed, "Proceed"),
515 ]
9ecde319 516 } else {
38b29068 517 println!("Mirrors selected for inclusion on medium:");
9ecde319
FG
518 for id in &selected_mirrors {
519 println!("\t- {id}");
520 }
9d5bafe6
FG
521 println!();
522 if available_mirrors.is_empty() {
523 println!("No more mirrors available for selection!");
524 vec![
525 (Action::DeselectMirror, "Remove mirror from selection."),
fc9b351a
FG
526 (
527 Action::DeselectAllMirrors,
528 "Remove all mirrors from selection.",
529 ),
9d5bafe6
FG
530 (Action::Proceed, "Proceed"),
531 ]
532 } else {
533 vec![
534 (Action::SelectMirror, "Add mirror to selection."),
fc9b351a 535 (Action::SelectAllMirrors, "Add all mirrors to selection."),
9d5bafe6 536 (Action::DeselectMirror, "Remove mirror from selection."),
fc9b351a
FG
537 (
538 Action::DeselectAllMirrors,
539 "Remove all mirrors from selection.",
540 ),
9d5bafe6
FG
541 (Action::Proceed, "Proceed"),
542 ]
543 }
544 };
545
9ecde319
FG
546 println!();
547
9d5bafe6 548 let action = read_selection_from_tty("Select action", &actions, Some(0))?;
9ecde319
FG
549 println!();
550
551 match action {
552 Action::SelectMirror => {
553 if available_mirrors.is_empty() {
38b29068 554 println!("No (more) unselected mirrors available.");
9ecde319
FG
555 continue;
556 }
557
558 let mirrors: Vec<(&str, &str)> = available_mirrors
559 .iter()
560 .map(|v| (v.as_ref(), v.as_ref()))
561 .collect();
562
563 let selected =
564 read_selection_from_tty("Select a mirror to add", &mirrors, None)?.to_string();
5ce9ab44 565 available_mirrors.retain(|v| *v != selected);
9ecde319
FG
566 selected_mirrors.push(selected);
567 }
fc9b351a
FG
568 Action::SelectAllMirrors => {
569 selected_mirrors.extend_from_slice(&available_mirrors);
570 available_mirrors.truncate(0);
571 }
9ecde319
FG
572 Action::DeselectMirror => {
573 if selected_mirrors.is_empty() {
38b29068 574 println!("No mirrors selected (yet).");
9ecde319
FG
575 continue;
576 }
577
578 let mirrors: Vec<(&str, &str)> = selected_mirrors
579 .iter()
580 .map(|v| (v.as_ref(), v.as_ref()))
581 .collect();
582
583 let selected =
584 read_selection_from_tty("Select a mirror to remove", &mirrors, None)?
585 .to_string();
5ce9ab44 586 selected_mirrors.retain(|v| *v != selected);
9ecde319
FG
587 available_mirrors.push(selected);
588 }
fc9b351a
FG
589 Action::DeselectAllMirrors => {
590 available_mirrors.extend_from_slice(&selected_mirrors);
591 selected_mirrors.truncate(0);
592 }
9ecde319
FG
593 Action::Proceed => {
594 break;
595 }
596 }
597 }
598
599 let verify = read_bool_from_tty(
600 "Should mirrored files be re-verified when updating the medium? (io-intensive!)",
601 Some(true),
602 )?;
603 let sync = read_bool_from_tty("Should newly written files be written using FSYNC to ensure crash-consistency? (io-intensive!)", Some(true))?;
604
9ecde319
FG
605 Ok(MediaConfig {
606 id,
607 mountpoint,
608 mirrors: selected_mirrors,
609 verify,
610 sync,
611 })
612}
613
28945c9a
FG
614fn action_add_key(config: &SectionConfigData) -> Result<SubscriptionKey, Error> {
615 let (product, mirror_key) = if let Ok(mirror_key) =
616 extract_mirror_key(&config.convert_to_typed_array("subscription")?)
617 {
618 let subscription_products = &[
619 (ProductType::Pve, "Proxmox VE"),
620 (ProductType::Pbs, "Proxmox Backup Server"),
621 (ProductType::Pmg, "Proxmox Mail Gateway"),
622 ];
623
624 let product = read_selection_from_tty(
625 "Select Proxmox product for which subscription key should be added",
626 subscription_products,
627 None,
628 )?;
629
630 (product, Some(mirror_key))
631 } else {
632 println!("No mirror key configured yet, forcing mirror key setup first..");
633 (&ProductType::Pom, None)
634 };
635
636 let key = read_string_from_tty("Please enter subscription key", None)?;
637 if config.sections.get(&key).is_some() {
638 bail!("Key entry for '{key}' already exists - please use 'key refresh' or 'key update'!");
639 }
640
641 let server_id = if product == &ProductType::Pom {
642 let server_id = proxmox_subscription::get_hardware_address()?;
643 println!("Server ID of this system is '{server_id}'");
644 server_id
645 } else {
646 read_string_from_tty(
647 "Please enter server ID of offline system using this subscription",
648 None,
649 )?
650 };
651
652 let mut data = SubscriptionKey {
653 key,
654 server_id,
655 description: None,
656 info: None,
657 };
658
659 if data.product() != *product {
660 bail!(
661 "Selected product and product in subscription key don't match: {} != {}",
662 product,
663 data.product()
664 );
665 }
666
667 if read_bool_from_tty("Attempt to refresh key", Some(true))? {
668 let info = if let Some(mirror_key) = mirror_key {
669 if let Err(err) = refresh_mirror_key(mirror_key.clone()) {
670 eprintln!("Failed to refresh mirror_key '{}' - {err}", mirror_key.key);
671 }
672
673 let mut refreshed = proxmox_offline_mirror::subscription::refresh_offline_keys(
674 mirror_key,
675 vec![data.clone()],
676 public_key()?,
677 )?;
678
679 refreshed
680 .pop()
681 .ok_or_else(|| format_err!("Server did not return subscription info.."))?
682 } else {
683 proxmox_offline_mirror::subscription::refresh_mirror_key(data.clone())?
684 };
685
686 println!(
687 "Refreshed subscription info - status: {}, message: {}",
688 info.status,
689 info.message.as_ref().unwrap_or(&"-".to_string())
690 );
691
692 if info.key.as_ref() == Some(&data.key) {
693 data.info = Some(base64::encode(serde_json::to_vec(&info)?));
694 } else {
695 bail!("Server returned subscription info for wrong key.");
696 }
697 }
698
699 Ok(data)
700}
701
9ecde319
FG
702#[api(
703 input: {
704 properties: {
6ba920ff
FG
705 config: {
706 type: String,
707 optional: true,
708 description: "Path to mirroring config file.",
709 },
9ecde319
FG
710 },
711 },
712)]
713/// Interactive setup wizard.
6ba920ff 714async fn setup(config: Option<String>, _param: Value) -> Result<(), Error> {
9ecde319
FG
715 if !tty::stdin_isatty() {
716 bail!("Setup wizard can only run interactively.");
717 }
718
54c83977 719 let config_file = config.unwrap_or_else(get_config_path);
6ba920ff 720
8b267808 721 let _lock = proxmox_offline_mirror::config::lock_config(&config_file)?;
9ecde319 722
8b267808 723 let (mut config, _digest) = proxmox_offline_mirror::config::config(&config_file)?;
9ecde319
FG
724
725 if config.sections.is_empty() {
726 println!("Initializing new config.");
727 } else {
728 println!("Loaded existing config.");
729 }
730
731 enum Action {
28945c9a 732 AddKey,
9ecde319
FG
733 AddMirror,
734 AddMedium,
735 Quit,
736 }
737
9ecde319
FG
738 loop {
739 println!();
9d5bafe6 740 let mut mirror_defined = false;
9ecde319
FG
741 if !config.sections.is_empty() {
742 println!("Existing config entries:");
743 for (section, (section_type, _)) in config.sections.iter() {
9d5bafe6
FG
744 if section_type == "mirror" {
745 mirror_defined = true;
746 }
9ecde319
FG
747 println!("{section_type} '{section}'");
748 }
749 println!();
750 }
751
9d5bafe6
FG
752 let actions = if mirror_defined {
753 vec![
754 (Action::AddMirror, "Add new mirror entry"),
755 (Action::AddMedium, "Add new medium entry"),
28945c9a 756 (Action::AddKey, "Add new subscription key"),
9d5bafe6
FG
757 (Action::Quit, "Quit"),
758 ]
759 } else {
760 vec![
761 (Action::AddMirror, "Add new mirror entry"),
28945c9a 762 (Action::AddKey, "Add new subscription key"),
9d5bafe6
FG
763 (Action::Quit, "Quit"),
764 ]
765 };
766
767 match read_selection_from_tty("Select Action:", &actions, Some(0))? {
9ecde319
FG
768 Action::Quit => break,
769 Action::AddMirror => {
1f7ac6f6
FG
770 for mirror_config in action_add_mirror(&config)? {
771 let id = mirror_config.id.clone();
772 mirror::init(&mirror_config)?;
773 config.set_data(&id, "mirror", mirror_config)?;
774 save_config(&config_file, &config)?;
775 println!("Config entry '{id}' added");
5d9224ed 776 println!("Run \"proxmox-offline-mirror mirror snapshot create --config '{config_file}' '{id}'\" to create a new mirror snapshot.");
1f7ac6f6 777 }
9ecde319
FG
778 }
779 Action::AddMedium => {
780 let media_config = action_add_medium(&config)?;
781 let id = media_config.id.clone();
782 config.set_data(&id, "medium", media_config)?;
783 save_config(&config_file, &config)?;
784 println!("Config entry '{id}' added");
5d9224ed 785 println!("Run \"proxmox-offline-mirror medium sync --config '{config_file}' '{id}'\" to sync mirror snapshots to medium.");
9ecde319 786 }
28945c9a
FG
787 Action::AddKey => {
788 let key = action_add_key(&config)?;
789 let id = key.key.clone();
790 config.set_data(&id, "subscription", &key)?;
791 save_config(&config_file, &config)?;
792 println!("Config entry '{id}' added");
793 println!("Run \"proxmox-offline-mirror key refresh\" to refresh subscription information");
794 }
9ecde319
FG
795 }
796 }
797
798 Ok(())
799}
28945c9a 800
9ecde319
FG
801fn main() {
802 let rpcenv = CliEnvironment::new();
803
804 let cmd_def = CliCommandMap::new()
805 .insert("setup", CliCommand::new(&API_METHOD_SETUP))
806 .insert("config", config_commands())
8b267808 807 .insert("key", key_commands())
9ecde319
FG
808 .insert("medium", medium_commands())
809 .insert("mirror", mirror_commands());
810
811 run_cli_command(
812 cmd_def,
813 rpcenv,
814 Some(|future| proxmox_async::runtime::main(future)),
815 );
816}