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