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