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