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