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