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