]> git.proxmox.com Git - proxmox-offline-mirror.git/blob - src/bin/proxmox-offline-mirror.rs
wizard: extract debian release/variant/.. handling
[proxmox-offline-mirror.git] / src / bin / proxmox-offline-mirror.rs
1 use std::fmt::Display;
2 use std::path::Path;
3
4 use anyhow::{bail, Error};
5 use serde_json::Value;
6
7 use proxmox_router::cli::{run_cli_command, CliCommand, CliCommandMap, CliEnvironment};
8 use proxmox_schema::api;
9 use proxmox_section_config::SectionConfigData;
10 use proxmox_sys::linux::tty;
11
12 use proxmox_offline_mirror::helpers::tty::{
13 read_bool_from_tty, read_selection_from_tty, read_string_from_tty,
14 };
15 use proxmox_offline_mirror::{
16 config::{save_config, MediaConfig, MirrorConfig},
17 mirror,
18 types::{ProductType, MEDIA_ID_SCHEMA, MIRROR_ID_SCHEMA},
19 };
20
21 mod proxmox_offline_mirror_cmds;
22 use proxmox_offline_mirror_cmds::*;
23
24 enum Distro {
25 Debian,
26 Pbs,
27 Pmg,
28 Pve,
29 PveCeph,
30 }
31
32 impl 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
44 enum Release {
45 Bullseye,
46 Buster,
47 }
48
49 impl 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
58 enum DebianVariant {
59 Main,
60 Security,
61 Updates,
62 Backports,
63 Debug,
64 }
65
66 impl 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)]
79 enum ProxmoxVariant {
80 Enterprise,
81 NoSubscription,
82 Test,
83 }
84
85 impl 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
95 fn derive_debian_repo(release: &Release, variant: &DebianVariant, components: &str) -> (String, String, Option<String>) {
96 let url = match (release, variant) {
97 (Release::Bullseye, DebianVariant::Main) => {
98 "http://deb.debian.org/debian bullseye"
99 }
100 (Release::Bullseye, DebianVariant::Security) => {
101 "http://deb.debian.org/debian-security bullseye-security"
102 }
103 (Release::Bullseye, DebianVariant::Updates) => {
104 "http://deb.debian.org/debian bullseye-updates"
105 }
106 (Release::Bullseye, DebianVariant::Backports) => {
107 "http://deb.debian.org/debian bullseye-backports"
108 }
109 (Release::Bullseye, DebianVariant::Debug) => {
110 "http://deb.debian.org/debian-debug bullseye-debug"
111 }
112 (Release::Buster, DebianVariant::Main) => "http://deb.debian.org/debian buster",
113 (Release::Buster, DebianVariant::Security) => {
114 "http://deb.debian.org/debian-security buster/updates"
115 }
116 (Release::Buster, DebianVariant::Updates) => {
117 "http://deb.debian.org/debian buster-updates"
118 }
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 }
132 (Release::Bullseye, _) => {
133 "/usr/share/keyrings/debian-archive-bullseye-automatic.gpg"
134 }
135 (Release::Buster, DebianVariant::Security) => {
136 "/usr/share/keyrings/debian-archive-buster-security-automatic.gpg"
137 }
138 (Release::Buster, _) => "/usr/share/keyrings/debian-archive-buster-stable.gpg",
139 };
140
141 let suggested_id = format!("debian_{release}_{variant}");
142
143 (url, key.to_string(), Some(suggested_id))
144 }
145
146 fn action_add_mirror(config: &SectionConfigData) -> Result<MirrorConfig, Error> {
147 let mut use_subscription = None;
148
149 let (repository, key_path, architectures, suggested_id) = if read_bool_from_tty(
150 "Guided Setup",
151 Some(true),
152 )? {
153 let distros = &[
154 (Distro::Pve, "Proxmox VE"),
155 (Distro::Pbs, "Proxmox Backup Server"),
156 (Distro::Pmg, "Proxmox Mail Gateway"),
157 (Distro::PveCeph, "Proxmox Ceph"),
158 (Distro::Debian, "Debian"),
159
160 ];
161 let dist = read_selection_from_tty("Select distro to mirror", distros, None)?;
162
163 let releases = &[(Release::Bullseye, "Bullseye"), (Release::Buster, "Buster")];
164 let release = read_selection_from_tty("Select release", releases, Some(0))?;
165
166 let (url, key_path, suggested_id) = match dist {
167 Distro::Debian => {
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
182 derive_debian_repo(release, variant, &components)
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
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 );
232 let suggested_id = format!("ceph_{ceph_release}_{release}");
233
234 (url, key.to_string(), Some(suggested_id))
235 }
236 product => {
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
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
256 use_subscription = match (product, variant) {
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
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
268 let suggested_id = format!("{product}_{release}_{variant}");
269
270 (url, key.to_string(), Some(suggested_id))
271 }
272 };
273
274 let architectures = vec!["amd64".to_string(), "all".to_string()];
275 (format!("deb {url}"), key_path, architectures, suggested_id)
276 } else {
277 let repo = read_string_from_tty("Enter repository line in sources.list format", None)?;
278 let key_path = read_string_from_tty("Enter (absolute) path to repository key file", None)?;
279 let architectures =
280 read_string_from_tty("Enter list of architectures to mirror", Some("amd64,all"))?;
281 let architectures: Vec<String> = architectures
282 .split(|c: char| c == ',' || c.is_ascii_whitespace())
283 .filter_map(|value| {
284 if value.is_empty() {
285 None
286 } else {
287 Some(value.to_owned())
288 }
289 })
290 .collect();
291 let subscription_products = &[
292 (Some(ProductType::Pve), "PVE"),
293 (Some(ProductType::Pbs), "PBS"),
294 (Some(ProductType::Pmg), "PMG"),
295 (None, "None"),
296 ];
297 use_subscription = read_selection_from_tty(
298 "Does this repository require a valid Proxmox subscription key",
299 subscription_products,
300 None,
301 )?
302 .clone();
303
304 (repo, key_path, architectures, None)
305 };
306
307 if !Path::new(&key_path).exists() {
308 eprintln!("Keyfile '{key_path}' doesn't exist - make sure to install relevant keyring packages or update config to provide correct path!");
309 }
310
311 let id = loop {
312 let mut id = read_string_from_tty("Enter mirror ID", suggested_id.as_deref())?;
313 while let Err(err) = MIRROR_ID_SCHEMA.parse_simple_value(&id) {
314 eprintln!("Not a valid mirror ID: {err}");
315 id = read_string_from_tty("Enter mirror ID", None)?;
316 }
317
318 if config.sections.contains_key(&id) {
319 eprintln!("Config entry '{id}' already exists!");
320 continue;
321 }
322
323 break id;
324 };
325
326 let dir = loop {
327 let path = read_string_from_tty(
328 "Enter (absolute) path where mirrored repository will be stored",
329 Some("/var/lib/proxmox-offline-mirror/mirrors/{id}"),
330 )?;
331 if !path.starts_with('/') {
332 eprintln!("Path must start with '/'");
333 } else if Path::new(&path).exists() {
334 eprintln!("Path already exists.");
335 } else {
336 break path;
337 }
338 };
339
340 let verify = read_bool_from_tty(
341 "Should already mirrored files be re-verified when updating the mirror? (io-intensive!)",
342 Some(true),
343 )?;
344 let sync = read_bool_from_tty("Should newly written files be written using FSYNC to ensure crash-consistency? (io-intensive!)", Some(true))?;
345
346 Ok(MirrorConfig {
347 id,
348 repository,
349 architectures,
350 key_path,
351 verify,
352 sync,
353 dir,
354 use_subscription,
355 })
356 }
357
358 fn action_add_medium(config: &SectionConfigData) -> Result<MediaConfig, Error> {
359 let id = loop {
360 let mut id = read_string_from_tty("Enter medium ID", None)?;
361 while let Err(err) = MEDIA_ID_SCHEMA.parse_simple_value(&id) {
362 eprintln!("Not a valid medium ID: {err}");
363 id = read_string_from_tty("Enter medium ID", None)?;
364 }
365
366 if config.sections.contains_key(&id) {
367 eprintln!("Config entry '{id}' already exists!");
368 continue;
369 }
370
371 break id;
372 };
373
374 let mountpoint = loop {
375 let path = read_string_from_tty("Enter (absolute) path where medium is mounted", None)?;
376 if !path.starts_with('/') {
377 eprintln!("Path must start with '/'");
378 continue;
379 }
380
381 let mountpoint = Path::new(&path);
382 if !mountpoint.exists() {
383 eprintln!("Path doesn't exist.");
384 } else {
385 let mut statefile = mountpoint.to_path_buf();
386 statefile.push(".mirror-state");
387 if !statefile.exists()
388 || read_bool_from_tty(
389 &format!("Found existing statefile at {statefile:?} - proceed?"),
390 Some(false),
391 )?
392 {
393 break path;
394 }
395 }
396 };
397
398 let mirrors: Vec<MirrorConfig> = config.convert_to_typed_array("mirror")?;
399 let mut available_mirrors: Vec<String> = Vec::new();
400 for mirror_config in mirrors {
401 available_mirrors.push(mirror_config.id);
402 }
403
404 let mut selected_mirrors: Vec<String> = Vec::new();
405
406 enum Action {
407 SelectMirror,
408 DeselectMirror,
409 Proceed,
410 }
411
412 loop {
413 println!();
414 let actions = if selected_mirrors.is_empty() {
415 println!("No mirrors selected for inclusion on medium so far.");
416 vec![
417 (Action::SelectMirror, "Add mirror to selection."),
418 (Action::Proceed, "Proceed"),
419 ]
420 } else {
421 println!("Mirrors selected for inclusion on medium:");
422 for id in &selected_mirrors {
423 println!("\t- {id}");
424 }
425 println!();
426 if available_mirrors.is_empty() {
427 println!("No more mirrors available for selection!");
428 vec![
429 (Action::DeselectMirror, "Remove mirror from selection."),
430 (Action::Proceed, "Proceed"),
431 ]
432 } else {
433 vec![
434 (Action::SelectMirror, "Add mirror to selection."),
435 (Action::DeselectMirror, "Remove mirror from selection."),
436 (Action::Proceed, "Proceed"),
437 ]
438 }
439 };
440
441 println!();
442
443 let action = read_selection_from_tty("Select action", &actions, Some(0))?;
444 println!();
445
446 match action {
447 Action::SelectMirror => {
448 if available_mirrors.is_empty() {
449 println!("No (more) unselected mirrors available.");
450 continue;
451 }
452
453 let mirrors: Vec<(&str, &str)> = available_mirrors
454 .iter()
455 .map(|v| (v.as_ref(), v.as_ref()))
456 .collect();
457
458 let selected =
459 read_selection_from_tty("Select a mirror to add", &mirrors, None)?.to_string();
460 available_mirrors = available_mirrors
461 .into_iter()
462 .filter(|v| *v != selected)
463 .collect();
464 selected_mirrors.push(selected);
465 }
466 Action::DeselectMirror => {
467 if selected_mirrors.is_empty() {
468 println!("No mirrors selected (yet).");
469 continue;
470 }
471
472 let mirrors: Vec<(&str, &str)> = selected_mirrors
473 .iter()
474 .map(|v| (v.as_ref(), v.as_ref()))
475 .collect();
476
477 let selected =
478 read_selection_from_tty("Select a mirror to remove", &mirrors, None)?
479 .to_string();
480 selected_mirrors = selected_mirrors
481 .into_iter()
482 .filter(|v| *v != selected)
483 .collect();
484 available_mirrors.push(selected);
485 }
486 Action::Proceed => {
487 break;
488 }
489 }
490 }
491
492 let verify = read_bool_from_tty(
493 "Should mirrored files be re-verified when updating the medium? (io-intensive!)",
494 Some(true),
495 )?;
496 let sync = read_bool_from_tty("Should newly written files be written using FSYNC to ensure crash-consistency? (io-intensive!)", Some(true))?;
497
498 Ok(MediaConfig {
499 id,
500 mountpoint,
501 mirrors: selected_mirrors,
502 verify,
503 sync,
504 })
505 }
506
507 #[api(
508 input: {
509 properties: {
510 },
511 },
512 )]
513 /// Interactive setup wizard.
514 async fn setup(_param: Value) -> Result<(), Error> {
515 if !tty::stdin_isatty() {
516 bail!("Setup wizard can only run interactively.");
517 }
518
519 let config_file = read_string_from_tty("Mirror config file", Some(DEFAULT_CONFIG_PATH))?;
520 let _lock = proxmox_offline_mirror::config::lock_config(&config_file)?;
521
522 let (mut config, _digest) = proxmox_offline_mirror::config::config(&config_file)?;
523
524 if config.sections.is_empty() {
525 println!("Initializing new config.");
526 } else {
527 println!("Loaded existing config.");
528 }
529
530 enum Action {
531 AddMirror,
532 AddMedium,
533 Quit,
534 }
535
536 loop {
537 println!();
538 let mut mirror_defined = false;
539 if !config.sections.is_empty() {
540 println!("Existing config entries:");
541 for (section, (section_type, _)) in config.sections.iter() {
542 if section_type == "mirror" {
543 mirror_defined = true;
544 }
545 println!("{section_type} '{section}'");
546 }
547 println!();
548 }
549
550 let actions = if mirror_defined {
551 vec![
552 (Action::AddMirror, "Add new mirror entry"),
553 (Action::AddMedium, "Add new medium entry"),
554 (Action::Quit, "Quit"),
555 ]
556 } else {
557 vec![
558 (Action::AddMirror, "Add new mirror entry"),
559 (Action::Quit, "Quit"),
560 ]
561 };
562
563 match read_selection_from_tty("Select Action:", &actions, Some(0))? {
564 Action::Quit => break,
565 Action::AddMirror => {
566 let mirror_config = action_add_mirror(&config)?;
567 let id = mirror_config.id.clone();
568 mirror::init(&mirror_config)?;
569 config.set_data(&id, "mirror", mirror_config)?;
570 save_config(&config_file, &config)?;
571 println!("Config entry '{id}' added");
572 println!("Run \"proxmox-offline-mirror mirror snapshot create --config '{config_file}' --id '{id}'\" to create a new mirror snapshot.");
573 }
574 Action::AddMedium => {
575 let media_config = action_add_medium(&config)?;
576 let id = media_config.id.clone();
577 config.set_data(&id, "medium", media_config)?;
578 save_config(&config_file, &config)?;
579 println!("Config entry '{id}' added");
580 println!("Run \"proxmox-offline-mirror medium sync --config '{config_file}' --id '{id}'\" to sync mirror snapshots to medium.");
581 }
582 }
583 }
584
585 Ok(())
586 }
587 fn main() {
588 let rpcenv = CliEnvironment::new();
589
590 let cmd_def = CliCommandMap::new()
591 .insert("setup", CliCommand::new(&API_METHOD_SETUP))
592 .insert("config", config_commands())
593 .insert("key", key_commands())
594 .insert("medium", medium_commands())
595 .insert("mirror", mirror_commands());
596
597 run_cli_command(
598 cmd_def,
599 rpcenv,
600 Some(|future| proxmox_async::runtime::main(future)),
601 );
602 }