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