]>
Commit | Line | Data |
---|---|---|
4e4363f2 | 1 | use std::fmt::Display; |
9ecde319 FG |
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 | ||
8b267808 | 12 | use proxmox_offline_mirror::helpers::tty::{ |
9ecde319 FG |
13 | read_bool_from_tty, read_selection_from_tty, read_string_from_tty, |
14 | }; | |
8b267808 | 15 | use 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 |
21 | mod proxmox_offline_mirror_cmds; |
22 | use proxmox_offline_mirror_cmds::*; | |
9ecde319 | 23 | |
4e4363f2 FG |
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 | ||
1f7ac6f6 FG |
95 | fn 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 | 144 | fn 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 | 388 | use_subscription: None, |
96a80415 | 389 | ignore_errors: false, |
1f7ac6f6 FG |
390 | }); |
391 | } | |
392 | } | |
393 | ||
394 | let main_config = MirrorConfig { | |
9ecde319 FG |
395 | id, |
396 | repository, | |
397 | architectures, | |
398 | key_path, | |
399 | verify, | |
400 | sync, | |
c598cb15 | 401 | base_dir, |
8b267808 | 402 | use_subscription, |
96a80415 | 403 | ignore_errors: false, |
1f7ac6f6 FG |
404 | }; |
405 | ||
406 | configs.push(main_config); | |
407 | Ok(configs) | |
9ecde319 FG |
408 | } |
409 | ||
410 | fn action_add_medium(config: &SectionConfigData) -> Result<MediaConfig, Error> { | |
9e725bc3 | 411 | let id = loop { |
f03a1f2b | 412 | let id = read_string_from_tty("Enter new medium ID", None)?; |
5471a418 | 413 | if let Err(err) = MEDIA_ID_SCHEMA.parse_simple_value(&id) { |
9e725bc3 | 414 | eprintln!("Not a valid medium ID: {err}"); |
5471a418 | 415 | continue; |
9e725bc3 FG |
416 | } |
417 | ||
418 | if config.sections.contains_key(&id) { | |
419 | eprintln!("Config entry '{id}' already exists!"); | |
420 | continue; | |
421 | } | |
422 | ||
423 | break id; | |
424 | }; | |
425 | ||
9ecde319 | 426 | let mountpoint = loop { |
38b29068 FG |
427 | let path = read_string_from_tty("Enter (absolute) path where medium is mounted", None)?; |
428 | if !path.starts_with('/') { | |
429 | eprintln!("Path must start with '/'"); | |
430 | continue; | |
431 | } | |
432 | ||
9ecde319 FG |
433 | let mountpoint = Path::new(&path); |
434 | if !mountpoint.exists() { | |
435 | eprintln!("Path doesn't exist."); | |
436 | } else { | |
437 | let mut statefile = mountpoint.to_path_buf(); | |
438 | statefile.push(".mirror-state"); | |
439 | if !statefile.exists() | |
440 | || read_bool_from_tty( | |
441 | &format!("Found existing statefile at {statefile:?} - proceed?"), | |
442 | Some(false), | |
443 | )? | |
444 | { | |
445 | break path; | |
446 | } | |
447 | } | |
448 | }; | |
449 | ||
450 | let mirrors: Vec<MirrorConfig> = config.convert_to_typed_array("mirror")?; | |
451 | let mut available_mirrors: Vec<String> = Vec::new(); | |
452 | for mirror_config in mirrors { | |
453 | available_mirrors.push(mirror_config.id); | |
454 | } | |
455 | ||
456 | let mut selected_mirrors: Vec<String> = Vec::new(); | |
457 | ||
458 | enum Action { | |
459 | SelectMirror, | |
fc9b351a | 460 | SelectAllMirrors, |
9ecde319 | 461 | DeselectMirror, |
fc9b351a | 462 | DeselectAllMirrors, |
9ecde319 FG |
463 | Proceed, |
464 | } | |
9ecde319 FG |
465 | |
466 | loop { | |
467 | println!(); | |
9d5bafe6 | 468 | let actions = if selected_mirrors.is_empty() { |
38b29068 | 469 | println!("No mirrors selected for inclusion on medium so far."); |
9d5bafe6 FG |
470 | vec![ |
471 | (Action::SelectMirror, "Add mirror to selection."), | |
fc9b351a | 472 | (Action::SelectAllMirrors, "Add all mirrors to selection."), |
9d5bafe6 FG |
473 | (Action::Proceed, "Proceed"), |
474 | ] | |
9ecde319 | 475 | } else { |
38b29068 | 476 | println!("Mirrors selected for inclusion on medium:"); |
9ecde319 FG |
477 | for id in &selected_mirrors { |
478 | println!("\t- {id}"); | |
479 | } | |
9d5bafe6 FG |
480 | println!(); |
481 | if available_mirrors.is_empty() { | |
482 | println!("No more mirrors available for selection!"); | |
483 | vec![ | |
484 | (Action::DeselectMirror, "Remove mirror from selection."), | |
fc9b351a FG |
485 | ( |
486 | Action::DeselectAllMirrors, | |
487 | "Remove all mirrors from selection.", | |
488 | ), | |
9d5bafe6 FG |
489 | (Action::Proceed, "Proceed"), |
490 | ] | |
491 | } else { | |
492 | vec![ | |
493 | (Action::SelectMirror, "Add mirror to selection."), | |
fc9b351a | 494 | (Action::SelectAllMirrors, "Add all mirrors to selection."), |
9d5bafe6 | 495 | (Action::DeselectMirror, "Remove mirror from selection."), |
fc9b351a FG |
496 | ( |
497 | Action::DeselectAllMirrors, | |
498 | "Remove all mirrors from selection.", | |
499 | ), | |
9d5bafe6 FG |
500 | (Action::Proceed, "Proceed"), |
501 | ] | |
502 | } | |
503 | }; | |
504 | ||
9ecde319 FG |
505 | println!(); |
506 | ||
9d5bafe6 | 507 | let action = read_selection_from_tty("Select action", &actions, Some(0))?; |
9ecde319 FG |
508 | println!(); |
509 | ||
510 | match action { | |
511 | Action::SelectMirror => { | |
512 | if available_mirrors.is_empty() { | |
38b29068 | 513 | println!("No (more) unselected mirrors available."); |
9ecde319 FG |
514 | continue; |
515 | } | |
516 | ||
517 | let mirrors: Vec<(&str, &str)> = available_mirrors | |
518 | .iter() | |
519 | .map(|v| (v.as_ref(), v.as_ref())) | |
520 | .collect(); | |
521 | ||
522 | let selected = | |
523 | read_selection_from_tty("Select a mirror to add", &mirrors, None)?.to_string(); | |
5ce9ab44 | 524 | available_mirrors.retain(|v| *v != selected); |
9ecde319 FG |
525 | selected_mirrors.push(selected); |
526 | } | |
fc9b351a FG |
527 | Action::SelectAllMirrors => { |
528 | selected_mirrors.extend_from_slice(&available_mirrors); | |
529 | available_mirrors.truncate(0); | |
530 | } | |
9ecde319 FG |
531 | Action::DeselectMirror => { |
532 | if selected_mirrors.is_empty() { | |
38b29068 | 533 | println!("No mirrors selected (yet)."); |
9ecde319 FG |
534 | continue; |
535 | } | |
536 | ||
537 | let mirrors: Vec<(&str, &str)> = selected_mirrors | |
538 | .iter() | |
539 | .map(|v| (v.as_ref(), v.as_ref())) | |
540 | .collect(); | |
541 | ||
542 | let selected = | |
543 | read_selection_from_tty("Select a mirror to remove", &mirrors, None)? | |
544 | .to_string(); | |
5ce9ab44 | 545 | selected_mirrors.retain(|v| *v != selected); |
9ecde319 FG |
546 | available_mirrors.push(selected); |
547 | } | |
fc9b351a FG |
548 | Action::DeselectAllMirrors => { |
549 | available_mirrors.extend_from_slice(&selected_mirrors); | |
550 | selected_mirrors.truncate(0); | |
551 | } | |
9ecde319 FG |
552 | Action::Proceed => { |
553 | break; | |
554 | } | |
555 | } | |
556 | } | |
557 | ||
558 | let verify = read_bool_from_tty( | |
559 | "Should mirrored files be re-verified when updating the medium? (io-intensive!)", | |
560 | Some(true), | |
561 | )?; | |
562 | let sync = read_bool_from_tty("Should newly written files be written using FSYNC to ensure crash-consistency? (io-intensive!)", Some(true))?; | |
563 | ||
9ecde319 FG |
564 | Ok(MediaConfig { |
565 | id, | |
566 | mountpoint, | |
567 | mirrors: selected_mirrors, | |
568 | verify, | |
569 | sync, | |
570 | }) | |
571 | } | |
572 | ||
573 | #[api( | |
574 | input: { | |
575 | properties: { | |
6ba920ff FG |
576 | config: { |
577 | type: String, | |
578 | optional: true, | |
579 | description: "Path to mirroring config file.", | |
580 | }, | |
9ecde319 FG |
581 | }, |
582 | }, | |
583 | )] | |
584 | /// Interactive setup wizard. | |
6ba920ff | 585 | async fn setup(config: Option<String>, _param: Value) -> Result<(), Error> { |
9ecde319 FG |
586 | if !tty::stdin_isatty() { |
587 | bail!("Setup wizard can only run interactively."); | |
588 | } | |
589 | ||
54c83977 | 590 | let config_file = config.unwrap_or_else(get_config_path); |
6ba920ff | 591 | |
8b267808 | 592 | let _lock = proxmox_offline_mirror::config::lock_config(&config_file)?; |
9ecde319 | 593 | |
8b267808 | 594 | let (mut config, _digest) = proxmox_offline_mirror::config::config(&config_file)?; |
9ecde319 FG |
595 | |
596 | if config.sections.is_empty() { | |
597 | println!("Initializing new config."); | |
598 | } else { | |
599 | println!("Loaded existing config."); | |
600 | } | |
601 | ||
602 | enum Action { | |
603 | AddMirror, | |
604 | AddMedium, | |
605 | Quit, | |
606 | } | |
607 | ||
9ecde319 FG |
608 | loop { |
609 | println!(); | |
9d5bafe6 | 610 | let mut mirror_defined = false; |
9ecde319 FG |
611 | if !config.sections.is_empty() { |
612 | println!("Existing config entries:"); | |
613 | for (section, (section_type, _)) in config.sections.iter() { | |
9d5bafe6 FG |
614 | if section_type == "mirror" { |
615 | mirror_defined = true; | |
616 | } | |
9ecde319 FG |
617 | println!("{section_type} '{section}'"); |
618 | } | |
619 | println!(); | |
620 | } | |
621 | ||
9d5bafe6 FG |
622 | let actions = if mirror_defined { |
623 | vec![ | |
624 | (Action::AddMirror, "Add new mirror entry"), | |
625 | (Action::AddMedium, "Add new medium entry"), | |
626 | (Action::Quit, "Quit"), | |
627 | ] | |
628 | } else { | |
629 | vec![ | |
630 | (Action::AddMirror, "Add new mirror entry"), | |
631 | (Action::Quit, "Quit"), | |
632 | ] | |
633 | }; | |
634 | ||
635 | match read_selection_from_tty("Select Action:", &actions, Some(0))? { | |
9ecde319 FG |
636 | Action::Quit => break, |
637 | Action::AddMirror => { | |
1f7ac6f6 FG |
638 | for mirror_config in action_add_mirror(&config)? { |
639 | let id = mirror_config.id.clone(); | |
640 | mirror::init(&mirror_config)?; | |
641 | config.set_data(&id, "mirror", mirror_config)?; | |
642 | save_config(&config_file, &config)?; | |
643 | println!("Config entry '{id}' added"); | |
5d9224ed | 644 | println!("Run \"proxmox-offline-mirror mirror snapshot create --config '{config_file}' '{id}'\" to create a new mirror snapshot."); |
1f7ac6f6 | 645 | } |
9ecde319 FG |
646 | } |
647 | Action::AddMedium => { | |
648 | let media_config = action_add_medium(&config)?; | |
649 | let id = media_config.id.clone(); | |
650 | config.set_data(&id, "medium", media_config)?; | |
651 | save_config(&config_file, &config)?; | |
652 | println!("Config entry '{id}' added"); | |
5d9224ed | 653 | println!("Run \"proxmox-offline-mirror medium sync --config '{config_file}' '{id}'\" to sync mirror snapshots to medium."); |
9ecde319 FG |
654 | } |
655 | } | |
656 | } | |
657 | ||
658 | Ok(()) | |
659 | } | |
660 | fn main() { | |
661 | let rpcenv = CliEnvironment::new(); | |
662 | ||
663 | let cmd_def = CliCommandMap::new() | |
664 | .insert("setup", CliCommand::new(&API_METHOD_SETUP)) | |
665 | .insert("config", config_commands()) | |
8b267808 | 666 | .insert("key", key_commands()) |
9ecde319 FG |
667 | .insert("medium", medium_commands()) |
668 | .insert("mirror", mirror_commands()); | |
669 | ||
670 | run_cli_command( | |
671 | cmd_def, | |
672 | rpcenv, | |
673 | Some(|future| proxmox_async::runtime::main(future)), | |
674 | ); | |
675 | } |