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