]>
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::{ |
e79308e6 | 16 | config::{save_config, MediaConfig, MirrorConfig, SkipConfig}, |
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, | |
f907fd5e FG |
99 | ) -> Result<(String, String, String, SkipConfig), Error> { |
100 | println!("Configure filters for Debian mirror {release} / {variant}:"); | |
101 | let skip_sections = match read_string_from_tty("\tEnter list of package sections to be skipped ('-' for None):", Some("debug,games"))?.as_str() { | |
102 | "-" => None, | |
103 | list => Some(list.split(',').map(|v| v.trim().to_owned()).collect::<Vec<String>>()), | |
104 | }; | |
105 | let skip_packages = match read_string_from_tty("\tEnter list of package names/name globs to be skipped ('-' for None):", None)?.as_str() { | |
106 | "-" => None, | |
107 | list => Some(list.split(',').map(|v| v.trim().to_owned()).collect::<Vec<String>>()), | |
108 | }; | |
109 | let filters = SkipConfig { | |
110 | skip_packages, | |
111 | skip_sections, | |
112 | }; | |
54b47e30 | 113 | let url = match (release, variant) { |
1f7ac6f6 | 114 | (Release::Bullseye, DebianVariant::Main) => "http://deb.debian.org/debian bullseye", |
54b47e30 FG |
115 | (Release::Bullseye, DebianVariant::Security) => { |
116 | "http://deb.debian.org/debian-security bullseye-security" | |
117 | } | |
118 | (Release::Bullseye, DebianVariant::Updates) => { | |
119 | "http://deb.debian.org/debian bullseye-updates" | |
120 | } | |
121 | (Release::Bullseye, DebianVariant::Backports) => { | |
122 | "http://deb.debian.org/debian bullseye-backports" | |
123 | } | |
124 | (Release::Bullseye, DebianVariant::Debug) => { | |
125 | "http://deb.debian.org/debian-debug bullseye-debug" | |
126 | } | |
127 | (Release::Buster, DebianVariant::Main) => "http://deb.debian.org/debian buster", | |
128 | (Release::Buster, DebianVariant::Security) => { | |
129 | "http://deb.debian.org/debian-security buster/updates" | |
130 | } | |
1f7ac6f6 | 131 | (Release::Buster, DebianVariant::Updates) => "http://deb.debian.org/debian buster-updates", |
54b47e30 FG |
132 | (Release::Buster, DebianVariant::Backports) => { |
133 | "http://deb.debian.org/debian buster-backports" | |
134 | } | |
135 | (Release::Buster, DebianVariant::Debug) => { | |
136 | "http://deb.debian.org/debian-debug buster-debug" | |
137 | } | |
138 | }; | |
139 | ||
140 | let url = format!("{url} {components}"); | |
141 | let key = match (release, variant) { | |
142 | (Release::Bullseye, DebianVariant::Security) => { | |
143 | "/usr/share/keyrings/debian-archive-bullseye-security-automatic.gpg" | |
144 | } | |
1f7ac6f6 | 145 | (Release::Bullseye, _) => "/usr/share/keyrings/debian-archive-bullseye-automatic.gpg", |
54b47e30 FG |
146 | (Release::Buster, DebianVariant::Security) => { |
147 | "/usr/share/keyrings/debian-archive-buster-security-automatic.gpg" | |
148 | } | |
149 | (Release::Buster, _) => "/usr/share/keyrings/debian-archive-buster-stable.gpg", | |
150 | }; | |
151 | ||
152 | let suggested_id = format!("debian_{release}_{variant}"); | |
153 | ||
f907fd5e | 154 | Ok((url, key.to_string(), suggested_id, filters)) |
54b47e30 FG |
155 | } |
156 | ||
1f7ac6f6 | 157 | fn action_add_mirror(config: &SectionConfigData) -> Result<Vec<MirrorConfig>, Error> { |
8b267808 | 158 | let mut use_subscription = None; |
1f7ac6f6 | 159 | let mut extra_repos = Vec::new(); |
8b267808 | 160 | |
f907fd5e | 161 | let (repository, key_path, architectures, suggested_id, skip) = if read_bool_from_tty( |
4e4363f2 FG |
162 | "Guided Setup", |
163 | Some(true), | |
164 | )? { | |
9ecde319 | 165 | let distros = &[ |
34ccf5f6 | 166 | (Distro::Pve, "Proxmox VE"), |
9ecde319 FG |
167 | (Distro::Pbs, "Proxmox Backup Server"), |
168 | (Distro::Pmg, "Proxmox Mail Gateway"), | |
34ccf5f6 FG |
169 | (Distro::PveCeph, "Proxmox Ceph"), |
170 | (Distro::Debian, "Debian"), | |
9ecde319 FG |
171 | ]; |
172 | let dist = read_selection_from_tty("Select distro to mirror", distros, None)?; | |
173 | ||
9ecde319 FG |
174 | let releases = &[(Release::Bullseye, "Bullseye"), (Release::Buster, "Buster")]; |
175 | let release = read_selection_from_tty("Select release", releases, Some(0))?; | |
176 | ||
1f7ac6f6 FG |
177 | let mut add_debian_repo = false; |
178 | ||
f907fd5e | 179 | let (url, key_path, suggested_id, skip) = match dist { |
9ecde319 | 180 | Distro::Debian => { |
9ecde319 FG |
181 | let variants = &[ |
182 | (DebianVariant::Main, "Main repository"), | |
183 | (DebianVariant::Security, "Security"), | |
184 | (DebianVariant::Updates, "Updates"), | |
185 | (DebianVariant::Backports, "Backports"), | |
186 | (DebianVariant::Debug, "Debug Information"), | |
187 | ]; | |
188 | let variant = | |
189 | read_selection_from_tty("Select repository variant", variants, Some(0))?; | |
190 | let components = read_string_from_tty( | |
191 | "Enter repository components", | |
192 | Some("main contrib non-free"), | |
193 | )?; | |
194 | ||
f907fd5e | 195 | derive_debian_repo(release, variant, &components)? |
9ecde319 FG |
196 | } |
197 | Distro::PveCeph => { | |
198 | enum CephRelease { | |
199 | Luminous, | |
200 | Nautilus, | |
201 | Octopus, | |
202 | Pacific, | |
203 | } | |
204 | ||
205 | let releases = match release { | |
206 | Release::Bullseye => { | |
207 | vec![ | |
208 | (CephRelease::Octopus, "Octopus (15.x)"), | |
209 | (CephRelease::Pacific, "Pacific (16.x)"), | |
210 | ] | |
211 | } | |
212 | Release::Buster => { | |
213 | vec![ | |
214 | (CephRelease::Luminous, "Luminous (12.x)"), | |
215 | (CephRelease::Nautilus, "Nautilus (14.x)"), | |
216 | (CephRelease::Octopus, "Octopus (15.x)"), | |
217 | ] | |
218 | } | |
219 | }; | |
220 | ||
221 | let ceph_release = read_selection_from_tty( | |
222 | "Select Ceph release", | |
223 | &releases, | |
224 | Some(releases.len() - 1), | |
225 | )?; | |
226 | ||
227 | let components = | |
228 | read_string_from_tty("Enter repository components", Some("main test"))?; | |
229 | ||
230 | let key = match release { | |
231 | Release::Bullseye => "/etc/apt/trusted.gpg.d/proxmox-release-bullseye.gpg", | |
232 | Release::Buster => "/etc/apt/trusted.gpg.d/proxmox-release-buster.gpg", | |
233 | }; | |
234 | ||
9ecde319 FG |
235 | let ceph_release = match ceph_release { |
236 | CephRelease::Luminous => "luminous", | |
237 | CephRelease::Nautilus => "nautilus", | |
238 | CephRelease::Octopus => "octopus", | |
239 | CephRelease::Pacific => "pacific", | |
240 | }; | |
241 | ||
242 | let url = format!( | |
243 | "http://download.proxmox.com/debian/ceph-{ceph_release} {release} {components}" | |
244 | ); | |
4e4363f2 | 245 | let suggested_id = format!("ceph_{ceph_release}_{release}"); |
9ecde319 | 246 | |
f907fd5e | 247 | (url, key.to_string(), suggested_id, SkipConfig::default()) |
9ecde319 | 248 | } |
4e4363f2 | 249 | product => { |
9ecde319 FG |
250 | let variants = &[ |
251 | (ProxmoxVariant::Enterprise, "Enterprise repository"), | |
252 | (ProxmoxVariant::NoSubscription, "No-Subscription repository"), | |
253 | (ProxmoxVariant::Test, "Test repository"), | |
254 | ]; | |
255 | ||
256 | let variant = | |
257 | read_selection_from_tty("Select repository variant", variants, Some(0))?; | |
258 | ||
9ecde319 FG |
259 | // TODO enterprise query for key! |
260 | let url = match (release, variant) { | |
261 | (Release::Bullseye, ProxmoxVariant::Enterprise) => format!("https://enterprise.proxmox.com/debian/{product} bullseye {product}-enterprise"), | |
262 | (Release::Bullseye, ProxmoxVariant::NoSubscription) => format!("http://download.proxmox.com/debian/{product} bullseye {product}-no-subscription"), | |
263 | (Release::Bullseye, ProxmoxVariant::Test) => format!("http://download.proxmox.com/debian/{product} bullseye {product}test"), | |
264 | (Release::Buster, ProxmoxVariant::Enterprise) => format!("https://enterprise.proxmox.com/debian/{product} buster {product}-enterprise"), | |
265 | (Release::Buster, ProxmoxVariant::NoSubscription) => format!("http://download.proxmox.com/debian/{product} buster {product}-no-subscription"), | |
266 | (Release::Buster, ProxmoxVariant::Test) => format!("http://download.proxmox.com/debian/{product} buster {product}test"), | |
267 | }; | |
268 | ||
4e4363f2 | 269 | use_subscription = match (product, variant) { |
8b267808 FG |
270 | (Distro::Pbs, &ProxmoxVariant::Enterprise) => Some(ProductType::Pbs), |
271 | (Distro::Pmg, &ProxmoxVariant::Enterprise) => Some(ProductType::Pmg), | |
272 | (Distro::Pve, &ProxmoxVariant::Enterprise) => Some(ProductType::Pve), | |
273 | _ => None, | |
274 | }; | |
275 | ||
9ecde319 FG |
276 | let key = match release { |
277 | Release::Bullseye => "/etc/apt/trusted.gpg.d/proxmox-release-bullseye.gpg", | |
278 | Release::Buster => "/etc/apt/trusted.gpg.d/proxmox-release-buster.gpg", | |
279 | }; | |
280 | ||
4e4363f2 FG |
281 | let suggested_id = format!("{product}_{release}_{variant}"); |
282 | ||
1f7ac6f6 FG |
283 | add_debian_repo = read_bool_from_tty( |
284 | "Should missing Debian mirrors for the selected product be auto-added", | |
285 | Some(true), | |
286 | )?; | |
287 | ||
f907fd5e | 288 | (url, key.to_string(), suggested_id, SkipConfig::default()) |
9ecde319 FG |
289 | } |
290 | }; | |
291 | ||
292 | let architectures = vec!["amd64".to_string(), "all".to_string()]; | |
1f7ac6f6 FG |
293 | |
294 | if add_debian_repo { | |
295 | extra_repos.push(derive_debian_repo( | |
296 | release, | |
297 | &DebianVariant::Main, | |
298 | "main contrib", | |
f907fd5e | 299 | )?); |
1f7ac6f6 FG |
300 | extra_repos.push(derive_debian_repo( |
301 | release, | |
302 | &DebianVariant::Updates, | |
303 | "main contrib", | |
f907fd5e | 304 | )?); |
1f7ac6f6 FG |
305 | extra_repos.push(derive_debian_repo( |
306 | release, | |
307 | &DebianVariant::Security, | |
308 | "main contrib", | |
f907fd5e | 309 | )?); |
1f7ac6f6 FG |
310 | } |
311 | ( | |
312 | format!("deb {url}"), | |
313 | key_path, | |
314 | architectures, | |
315 | Some(suggested_id), | |
f907fd5e | 316 | skip, |
1f7ac6f6 | 317 | ) |
9ecde319 FG |
318 | } else { |
319 | let repo = read_string_from_tty("Enter repository line in sources.list format", None)?; | |
38b29068 | 320 | let key_path = read_string_from_tty("Enter (absolute) path to repository key file", None)?; |
9ecde319 FG |
321 | let architectures = |
322 | read_string_from_tty("Enter list of architectures to mirror", Some("amd64,all"))?; | |
323 | let architectures: Vec<String> = architectures | |
324 | .split(|c: char| c == ',' || c.is_ascii_whitespace()) | |
325 | .filter_map(|value| { | |
326 | if value.is_empty() { | |
327 | None | |
328 | } else { | |
329 | Some(value.to_owned()) | |
330 | } | |
331 | }) | |
332 | .collect(); | |
8b267808 FG |
333 | let subscription_products = &[ |
334 | (Some(ProductType::Pve), "PVE"), | |
335 | (Some(ProductType::Pbs), "PBS"), | |
336 | (Some(ProductType::Pmg), "PMG"), | |
337 | (None, "None"), | |
338 | ]; | |
339 | use_subscription = read_selection_from_tty( | |
340 | "Does this repository require a valid Proxmox subscription key", | |
341 | subscription_products, | |
342 | None, | |
343 | )? | |
344 | .clone(); | |
345 | ||
f907fd5e | 346 | (repo, key_path, architectures, None, SkipConfig::default()) |
9ecde319 FG |
347 | }; |
348 | ||
349 | if !Path::new(&key_path).exists() { | |
350 | eprintln!("Keyfile '{key_path}' doesn't exist - make sure to install relevant keyring packages or update config to provide correct path!"); | |
351 | } | |
352 | ||
353 | let id = loop { | |
4e4363f2 | 354 | let mut id = read_string_from_tty("Enter mirror ID", suggested_id.as_deref())?; |
9ecde319 FG |
355 | while let Err(err) = MIRROR_ID_SCHEMA.parse_simple_value(&id) { |
356 | eprintln!("Not a valid mirror ID: {err}"); | |
357 | id = read_string_from_tty("Enter mirror ID", None)?; | |
358 | } | |
359 | ||
360 | if config.sections.contains_key(&id) { | |
361 | eprintln!("Config entry '{id}' already exists!"); | |
362 | continue; | |
363 | } | |
364 | ||
365 | break id; | |
366 | }; | |
367 | ||
a085d187 FG |
368 | let base_dir = loop { |
369 | let path = read_string_from_tty( | |
370 | "Enter (absolute) base path where mirrored repositories will be stored", | |
371 | Some("/var/lib/proxmox-offline-mirror/mirrors/"), | |
372 | )?; | |
c76900c9 | 373 | if !path.starts_with('/') { |
38b29068 | 374 | eprintln!("Path must start with '/'"); |
9ecde319 FG |
375 | } else { |
376 | break path; | |
377 | } | |
378 | }; | |
379 | ||
9ecde319 FG |
380 | let verify = read_bool_from_tty( |
381 | "Should already mirrored files be re-verified when updating the mirror? (io-intensive!)", | |
382 | Some(true), | |
383 | )?; | |
384 | let sync = read_bool_from_tty("Should newly written files be written using FSYNC to ensure crash-consistency? (io-intensive!)", Some(true))?; | |
385 | ||
1f7ac6f6 FG |
386 | let mut configs = Vec::with_capacity(extra_repos.len() + 1); |
387 | ||
f907fd5e | 388 | for (url, key_path, suggested_id, skip) in extra_repos { |
1f7ac6f6 FG |
389 | if config.sections.contains_key(&suggested_id) { |
390 | eprintln!("config section '{suggested_id}' already exists, skipping.."); | |
391 | } else { | |
392 | let repository = format!("deb {url}"); | |
a085d187 | 393 | |
1f7ac6f6 FG |
394 | configs.push(MirrorConfig { |
395 | id: suggested_id, | |
396 | repository, | |
397 | architectures: architectures.clone(), | |
398 | key_path, | |
399 | verify, | |
400 | sync, | |
c598cb15 | 401 | base_dir: base_dir.clone(), |
1f7ac6f6 | 402 | use_subscription: None, |
96a80415 | 403 | ignore_errors: false, |
f907fd5e | 404 | skip, |
1f7ac6f6 FG |
405 | }); |
406 | } | |
407 | } | |
408 | ||
409 | let main_config = MirrorConfig { | |
9ecde319 FG |
410 | id, |
411 | repository, | |
412 | architectures, | |
413 | key_path, | |
414 | verify, | |
415 | sync, | |
c598cb15 | 416 | base_dir, |
8b267808 | 417 | use_subscription, |
96a80415 | 418 | ignore_errors: false, |
f907fd5e | 419 | skip, |
1f7ac6f6 FG |
420 | }; |
421 | ||
422 | configs.push(main_config); | |
423 | Ok(configs) | |
9ecde319 FG |
424 | } |
425 | ||
426 | fn action_add_medium(config: &SectionConfigData) -> Result<MediaConfig, Error> { | |
9e725bc3 | 427 | let id = loop { |
f03a1f2b | 428 | let id = read_string_from_tty("Enter new medium ID", None)?; |
5471a418 | 429 | if let Err(err) = MEDIA_ID_SCHEMA.parse_simple_value(&id) { |
9e725bc3 | 430 | eprintln!("Not a valid medium ID: {err}"); |
5471a418 | 431 | continue; |
9e725bc3 FG |
432 | } |
433 | ||
434 | if config.sections.contains_key(&id) { | |
435 | eprintln!("Config entry '{id}' already exists!"); | |
436 | continue; | |
437 | } | |
438 | ||
439 | break id; | |
440 | }; | |
441 | ||
9ecde319 | 442 | let mountpoint = loop { |
38b29068 FG |
443 | let path = read_string_from_tty("Enter (absolute) path where medium is mounted", None)?; |
444 | if !path.starts_with('/') { | |
445 | eprintln!("Path must start with '/'"); | |
446 | continue; | |
447 | } | |
448 | ||
9ecde319 FG |
449 | let mountpoint = Path::new(&path); |
450 | if !mountpoint.exists() { | |
451 | eprintln!("Path doesn't exist."); | |
452 | } else { | |
453 | let mut statefile = mountpoint.to_path_buf(); | |
454 | statefile.push(".mirror-state"); | |
455 | if !statefile.exists() | |
456 | || read_bool_from_tty( | |
457 | &format!("Found existing statefile at {statefile:?} - proceed?"), | |
458 | Some(false), | |
459 | )? | |
460 | { | |
461 | break path; | |
462 | } | |
463 | } | |
464 | }; | |
465 | ||
466 | let mirrors: Vec<MirrorConfig> = config.convert_to_typed_array("mirror")?; | |
467 | let mut available_mirrors: Vec<String> = Vec::new(); | |
468 | for mirror_config in mirrors { | |
469 | available_mirrors.push(mirror_config.id); | |
470 | } | |
471 | ||
472 | let mut selected_mirrors: Vec<String> = Vec::new(); | |
473 | ||
474 | enum Action { | |
475 | SelectMirror, | |
fc9b351a | 476 | SelectAllMirrors, |
9ecde319 | 477 | DeselectMirror, |
fc9b351a | 478 | DeselectAllMirrors, |
9ecde319 FG |
479 | Proceed, |
480 | } | |
9ecde319 FG |
481 | |
482 | loop { | |
483 | println!(); | |
9d5bafe6 | 484 | let actions = if selected_mirrors.is_empty() { |
38b29068 | 485 | println!("No mirrors selected for inclusion on medium so far."); |
9d5bafe6 FG |
486 | vec![ |
487 | (Action::SelectMirror, "Add mirror to selection."), | |
fc9b351a | 488 | (Action::SelectAllMirrors, "Add all mirrors to selection."), |
9d5bafe6 FG |
489 | (Action::Proceed, "Proceed"), |
490 | ] | |
9ecde319 | 491 | } else { |
38b29068 | 492 | println!("Mirrors selected for inclusion on medium:"); |
9ecde319 FG |
493 | for id in &selected_mirrors { |
494 | println!("\t- {id}"); | |
495 | } | |
9d5bafe6 FG |
496 | println!(); |
497 | if available_mirrors.is_empty() { | |
498 | println!("No more mirrors available for selection!"); | |
499 | vec![ | |
500 | (Action::DeselectMirror, "Remove mirror from selection."), | |
fc9b351a FG |
501 | ( |
502 | Action::DeselectAllMirrors, | |
503 | "Remove all mirrors from selection.", | |
504 | ), | |
9d5bafe6 FG |
505 | (Action::Proceed, "Proceed"), |
506 | ] | |
507 | } else { | |
508 | vec![ | |
509 | (Action::SelectMirror, "Add mirror to selection."), | |
fc9b351a | 510 | (Action::SelectAllMirrors, "Add all mirrors to selection."), |
9d5bafe6 | 511 | (Action::DeselectMirror, "Remove mirror from selection."), |
fc9b351a FG |
512 | ( |
513 | Action::DeselectAllMirrors, | |
514 | "Remove all mirrors from selection.", | |
515 | ), | |
9d5bafe6 FG |
516 | (Action::Proceed, "Proceed"), |
517 | ] | |
518 | } | |
519 | }; | |
520 | ||
9ecde319 FG |
521 | println!(); |
522 | ||
9d5bafe6 | 523 | let action = read_selection_from_tty("Select action", &actions, Some(0))?; |
9ecde319 FG |
524 | println!(); |
525 | ||
526 | match action { | |
527 | Action::SelectMirror => { | |
528 | if available_mirrors.is_empty() { | |
38b29068 | 529 | println!("No (more) unselected mirrors available."); |
9ecde319 FG |
530 | continue; |
531 | } | |
532 | ||
533 | let mirrors: Vec<(&str, &str)> = available_mirrors | |
534 | .iter() | |
535 | .map(|v| (v.as_ref(), v.as_ref())) | |
536 | .collect(); | |
537 | ||
538 | let selected = | |
539 | read_selection_from_tty("Select a mirror to add", &mirrors, None)?.to_string(); | |
5ce9ab44 | 540 | available_mirrors.retain(|v| *v != selected); |
9ecde319 FG |
541 | selected_mirrors.push(selected); |
542 | } | |
fc9b351a FG |
543 | Action::SelectAllMirrors => { |
544 | selected_mirrors.extend_from_slice(&available_mirrors); | |
545 | available_mirrors.truncate(0); | |
546 | } | |
9ecde319 FG |
547 | Action::DeselectMirror => { |
548 | if selected_mirrors.is_empty() { | |
38b29068 | 549 | println!("No mirrors selected (yet)."); |
9ecde319 FG |
550 | continue; |
551 | } | |
552 | ||
553 | let mirrors: Vec<(&str, &str)> = selected_mirrors | |
554 | .iter() | |
555 | .map(|v| (v.as_ref(), v.as_ref())) | |
556 | .collect(); | |
557 | ||
558 | let selected = | |
559 | read_selection_from_tty("Select a mirror to remove", &mirrors, None)? | |
560 | .to_string(); | |
5ce9ab44 | 561 | selected_mirrors.retain(|v| *v != selected); |
9ecde319 FG |
562 | available_mirrors.push(selected); |
563 | } | |
fc9b351a FG |
564 | Action::DeselectAllMirrors => { |
565 | available_mirrors.extend_from_slice(&selected_mirrors); | |
566 | selected_mirrors.truncate(0); | |
567 | } | |
9ecde319 FG |
568 | Action::Proceed => { |
569 | break; | |
570 | } | |
571 | } | |
572 | } | |
573 | ||
574 | let verify = read_bool_from_tty( | |
575 | "Should mirrored files be re-verified when updating the medium? (io-intensive!)", | |
576 | Some(true), | |
577 | )?; | |
578 | let sync = read_bool_from_tty("Should newly written files be written using FSYNC to ensure crash-consistency? (io-intensive!)", Some(true))?; | |
579 | ||
9ecde319 FG |
580 | Ok(MediaConfig { |
581 | id, | |
582 | mountpoint, | |
583 | mirrors: selected_mirrors, | |
584 | verify, | |
585 | sync, | |
586 | }) | |
587 | } | |
588 | ||
589 | #[api( | |
590 | input: { | |
591 | properties: { | |
6ba920ff FG |
592 | config: { |
593 | type: String, | |
594 | optional: true, | |
595 | description: "Path to mirroring config file.", | |
596 | }, | |
9ecde319 FG |
597 | }, |
598 | }, | |
599 | )] | |
600 | /// Interactive setup wizard. | |
6ba920ff | 601 | async fn setup(config: Option<String>, _param: Value) -> Result<(), Error> { |
9ecde319 FG |
602 | if !tty::stdin_isatty() { |
603 | bail!("Setup wizard can only run interactively."); | |
604 | } | |
605 | ||
54c83977 | 606 | let config_file = config.unwrap_or_else(get_config_path); |
6ba920ff | 607 | |
8b267808 | 608 | let _lock = proxmox_offline_mirror::config::lock_config(&config_file)?; |
9ecde319 | 609 | |
8b267808 | 610 | let (mut config, _digest) = proxmox_offline_mirror::config::config(&config_file)?; |
9ecde319 FG |
611 | |
612 | if config.sections.is_empty() { | |
613 | println!("Initializing new config."); | |
614 | } else { | |
615 | println!("Loaded existing config."); | |
616 | } | |
617 | ||
618 | enum Action { | |
619 | AddMirror, | |
620 | AddMedium, | |
621 | Quit, | |
622 | } | |
623 | ||
9ecde319 FG |
624 | loop { |
625 | println!(); | |
9d5bafe6 | 626 | let mut mirror_defined = false; |
9ecde319 FG |
627 | if !config.sections.is_empty() { |
628 | println!("Existing config entries:"); | |
629 | for (section, (section_type, _)) in config.sections.iter() { | |
9d5bafe6 FG |
630 | if section_type == "mirror" { |
631 | mirror_defined = true; | |
632 | } | |
9ecde319 FG |
633 | println!("{section_type} '{section}'"); |
634 | } | |
635 | println!(); | |
636 | } | |
637 | ||
9d5bafe6 FG |
638 | let actions = if mirror_defined { |
639 | vec![ | |
640 | (Action::AddMirror, "Add new mirror entry"), | |
641 | (Action::AddMedium, "Add new medium entry"), | |
642 | (Action::Quit, "Quit"), | |
643 | ] | |
644 | } else { | |
645 | vec![ | |
646 | (Action::AddMirror, "Add new mirror entry"), | |
647 | (Action::Quit, "Quit"), | |
648 | ] | |
649 | }; | |
650 | ||
651 | match read_selection_from_tty("Select Action:", &actions, Some(0))? { | |
9ecde319 FG |
652 | Action::Quit => break, |
653 | Action::AddMirror => { | |
1f7ac6f6 FG |
654 | for mirror_config in action_add_mirror(&config)? { |
655 | let id = mirror_config.id.clone(); | |
656 | mirror::init(&mirror_config)?; | |
657 | config.set_data(&id, "mirror", mirror_config)?; | |
658 | save_config(&config_file, &config)?; | |
659 | println!("Config entry '{id}' added"); | |
5d9224ed | 660 | println!("Run \"proxmox-offline-mirror mirror snapshot create --config '{config_file}' '{id}'\" to create a new mirror snapshot."); |
1f7ac6f6 | 661 | } |
9ecde319 FG |
662 | } |
663 | Action::AddMedium => { | |
664 | let media_config = action_add_medium(&config)?; | |
665 | let id = media_config.id.clone(); | |
666 | config.set_data(&id, "medium", media_config)?; | |
667 | save_config(&config_file, &config)?; | |
668 | println!("Config entry '{id}' added"); | |
5d9224ed | 669 | println!("Run \"proxmox-offline-mirror medium sync --config '{config_file}' '{id}'\" to sync mirror snapshots to medium."); |
9ecde319 FG |
670 | } |
671 | } | |
672 | } | |
673 | ||
674 | Ok(()) | |
675 | } | |
676 | fn main() { | |
677 | let rpcenv = CliEnvironment::new(); | |
678 | ||
679 | let cmd_def = CliCommandMap::new() | |
680 | .insert("setup", CliCommand::new(&API_METHOD_SETUP)) | |
681 | .insert("config", config_commands()) | |
8b267808 | 682 | .insert("key", key_commands()) |
9ecde319 FG |
683 | .insert("medium", medium_commands()) |
684 | .insert("mirror", mirror_commands()); | |
685 | ||
686 | run_cli_command( | |
687 | cmd_def, | |
688 | rpcenv, | |
689 | Some(|future| proxmox_async::runtime::main(future)), | |
690 | ); | |
691 | } |