4 use anyhow
::{bail, Error}
;
7 use proxmox_router
::cli
::{run_cli_command, CliCommand, CliCommandMap, CliEnvironment}
;
8 use proxmox_schema
::api
;
9 use proxmox_section_config
::SectionConfigData
;
10 use proxmox_sys
::linux
::tty
;
12 use proxmox_offline_mirror
::helpers
::tty
::{
13 read_bool_from_tty
, read_selection_from_tty
, read_string_from_tty
,
15 use proxmox_offline_mirror
::{
16 config
::{save_config, MediaConfig, MirrorConfig}
,
18 types
::{ProductType, MEDIA_ID_SCHEMA, MIRROR_ID_SCHEMA}
,
21 mod proxmox_offline_mirror_cmds
;
22 use proxmox_offline_mirror_cmds
::*;
32 impl Display
for Distro
{
33 fn fmt(&self, f
: &mut std
::fmt
::Formatter
<'_
>) -> std
::fmt
::Result
{
35 Distro
::Debian
=> write
!(f
, "debian"),
36 Distro
::Pbs
=> write
!(f
, "pbs"),
37 Distro
::Pmg
=> write
!(f
, "pmg"),
38 Distro
::Pve
=> write
!(f
, "pve"),
39 Distro
::PveCeph
=> write
!(f
, "ceph"),
49 impl Display
for Release
{
50 fn fmt(&self, f
: &mut std
::fmt
::Formatter
<'_
>) -> std
::fmt
::Result
{
52 Release
::Bullseye
=> write
!(f
, "bullseye"),
53 Release
::Buster
=> write
!(f
, "buster"),
66 impl Display
for DebianVariant
{
67 fn fmt(&self, f
: &mut std
::fmt
::Formatter
<'_
>) -> std
::fmt
::Result
{
69 DebianVariant
::Main
=> write
!(f
, "main"),
70 DebianVariant
::Security
=> write
!(f
, "security"),
71 DebianVariant
::Updates
=> write
!(f
, "updates"),
72 DebianVariant
::Backports
=> write
!(f
, "backports"),
73 DebianVariant
::Debug
=> write
!(f
, "debug"),
85 impl Display
for ProxmoxVariant
{
86 fn fmt(&self, f
: &mut std
::fmt
::Formatter
<'_
>) -> std
::fmt
::Result
{
88 ProxmoxVariant
::Enterprise
=> write
!(f
, "enterprise"),
89 ProxmoxVariant
::NoSubscription
=> write
!(f
, "no_subscription"),
90 ProxmoxVariant
::Test
=> write
!(f
, "test"),
95 fn derive_debian_repo(release
: &Release
, variant
: &DebianVariant
, components
: &str) -> (String
, String
, Option
<String
>) {
96 let url
= match (release
, variant
) {
97 (Release
::Bullseye
, DebianVariant
::Main
) => {
98 "http://deb.debian.org/debian bullseye"
100 (Release
::Bullseye
, DebianVariant
::Security
) => {
101 "http://deb.debian.org/debian-security bullseye-security"
103 (Release
::Bullseye
, DebianVariant
::Updates
) => {
104 "http://deb.debian.org/debian bullseye-updates"
106 (Release
::Bullseye
, DebianVariant
::Backports
) => {
107 "http://deb.debian.org/debian bullseye-backports"
109 (Release
::Bullseye
, DebianVariant
::Debug
) => {
110 "http://deb.debian.org/debian-debug bullseye-debug"
112 (Release
::Buster
, DebianVariant
::Main
) => "http://deb.debian.org/debian buster",
113 (Release
::Buster
, DebianVariant
::Security
) => {
114 "http://deb.debian.org/debian-security buster/updates"
116 (Release
::Buster
, DebianVariant
::Updates
) => {
117 "http://deb.debian.org/debian buster-updates"
119 (Release
::Buster
, DebianVariant
::Backports
) => {
120 "http://deb.debian.org/debian buster-backports"
122 (Release
::Buster
, DebianVariant
::Debug
) => {
123 "http://deb.debian.org/debian-debug buster-debug"
127 let url
= format
!("{url} {components}");
128 let key
= match (release
, variant
) {
129 (Release
::Bullseye
, DebianVariant
::Security
) => {
130 "/usr/share/keyrings/debian-archive-bullseye-security-automatic.gpg"
132 (Release
::Bullseye
, _
) => {
133 "/usr/share/keyrings/debian-archive-bullseye-automatic.gpg"
135 (Release
::Buster
, DebianVariant
::Security
) => {
136 "/usr/share/keyrings/debian-archive-buster-security-automatic.gpg"
138 (Release
::Buster
, _
) => "/usr/share/keyrings/debian-archive-buster-stable.gpg",
141 let suggested_id
= format
!("debian_{release}_{variant}");
143 (url
, key
.to_string(), Some(suggested_id
))
146 fn action_add_mirror(config
: &SectionConfigData
) -> Result
<MirrorConfig
, Error
> {
147 let mut use_subscription
= None
;
149 let (repository
, key_path
, architectures
, suggested_id
) = if read_bool_from_tty(
154 (Distro
::Pve
, "Proxmox VE"),
155 (Distro
::Pbs
, "Proxmox Backup Server"),
156 (Distro
::Pmg
, "Proxmox Mail Gateway"),
157 (Distro
::PveCeph
, "Proxmox Ceph"),
158 (Distro
::Debian
, "Debian"),
161 let dist
= read_selection_from_tty("Select distro to mirror", distros
, None
)?
;
163 let releases
= &[(Release
::Bullseye
, "Bullseye"), (Release
::Buster
, "Buster")];
164 let release
= read_selection_from_tty("Select release", releases
, Some(0))?
;
166 let (url
, key_path
, suggested_id
) = match dist
{
169 (DebianVariant
::Main
, "Main repository"),
170 (DebianVariant
::Security
, "Security"),
171 (DebianVariant
::Updates
, "Updates"),
172 (DebianVariant
::Backports
, "Backports"),
173 (DebianVariant
::Debug
, "Debug Information"),
176 read_selection_from_tty("Select repository variant", variants
, Some(0))?
;
177 let components
= read_string_from_tty(
178 "Enter repository components",
179 Some("main contrib non-free"),
182 derive_debian_repo(release
, variant
, &components
)
192 let releases
= match release
{
193 Release
::Bullseye
=> {
195 (CephRelease
::Octopus
, "Octopus (15.x)"),
196 (CephRelease
::Pacific
, "Pacific (16.x)"),
201 (CephRelease
::Luminous
, "Luminous (12.x)"),
202 (CephRelease
::Nautilus
, "Nautilus (14.x)"),
203 (CephRelease
::Octopus
, "Octopus (15.x)"),
208 let ceph_release
= read_selection_from_tty(
209 "Select Ceph release",
211 Some(releases
.len() - 1),
215 read_string_from_tty("Enter repository components", Some("main test"))?
;
217 let key
= match release
{
218 Release
::Bullseye
=> "/etc/apt/trusted.gpg.d/proxmox-release-bullseye.gpg",
219 Release
::Buster
=> "/etc/apt/trusted.gpg.d/proxmox-release-buster.gpg",
222 let ceph_release
= match ceph_release
{
223 CephRelease
::Luminous
=> "luminous",
224 CephRelease
::Nautilus
=> "nautilus",
225 CephRelease
::Octopus
=> "octopus",
226 CephRelease
::Pacific
=> "pacific",
230 "http://download.proxmox.com/debian/ceph-{ceph_release} {release} {components}"
232 let suggested_id
= format
!("ceph_{ceph_release}_{release}");
234 (url
, key
.to_string(), Some(suggested_id
))
238 (ProxmoxVariant
::Enterprise
, "Enterprise repository"),
239 (ProxmoxVariant
::NoSubscription
, "No-Subscription repository"),
240 (ProxmoxVariant
::Test
, "Test repository"),
244 read_selection_from_tty("Select repository variant", variants
, Some(0))?
;
246 // TODO enterprise query for key!
247 let url
= match (release
, variant
) {
248 (Release
::Bullseye
, ProxmoxVariant
::Enterprise
) => format
!("https://enterprise.proxmox.com/debian/{product} bullseye {product}-enterprise"),
249 (Release
::Bullseye
, ProxmoxVariant
::NoSubscription
) => format
!("http://download.proxmox.com/debian/{product} bullseye {product}-no-subscription"),
250 (Release
::Bullseye
, ProxmoxVariant
::Test
) => format
!("http://download.proxmox.com/debian/{product} bullseye {product}test"),
251 (Release
::Buster
, ProxmoxVariant
::Enterprise
) => format
!("https://enterprise.proxmox.com/debian/{product} buster {product}-enterprise"),
252 (Release
::Buster
, ProxmoxVariant
::NoSubscription
) => format
!("http://download.proxmox.com/debian/{product} buster {product}-no-subscription"),
253 (Release
::Buster
, ProxmoxVariant
::Test
) => format
!("http://download.proxmox.com/debian/{product} buster {product}test"),
256 use_subscription
= match (product
, variant
) {
257 (Distro
::Pbs
, &ProxmoxVariant
::Enterprise
) => Some(ProductType
::Pbs
),
258 (Distro
::Pmg
, &ProxmoxVariant
::Enterprise
) => Some(ProductType
::Pmg
),
259 (Distro
::Pve
, &ProxmoxVariant
::Enterprise
) => Some(ProductType
::Pve
),
263 let key
= match release
{
264 Release
::Bullseye
=> "/etc/apt/trusted.gpg.d/proxmox-release-bullseye.gpg",
265 Release
::Buster
=> "/etc/apt/trusted.gpg.d/proxmox-release-buster.gpg",
268 let suggested_id
= format
!("{product}_{release}_{variant}");
270 (url
, key
.to_string(), Some(suggested_id
))
274 let architectures
= vec
!["amd64".to_string(), "all".to_string()];
275 (format
!("deb {url}"), key_path
, architectures
, suggested_id
)
277 let repo
= read_string_from_tty("Enter repository line in sources.list format", None
)?
;
278 let key_path
= read_string_from_tty("Enter (absolute) path to repository key file", None
)?
;
280 read_string_from_tty("Enter list of architectures to mirror", Some("amd64,all"))?
;
281 let architectures
: Vec
<String
> = architectures
282 .split(|c
: char| c
== '
,'
|| c
.is_ascii_whitespace())
283 .filter_map(|value
| {
284 if value
.is_empty() {
287 Some(value
.to_owned())
291 let subscription_products
= &[
292 (Some(ProductType
::Pve
), "PVE"),
293 (Some(ProductType
::Pbs
), "PBS"),
294 (Some(ProductType
::Pmg
), "PMG"),
297 use_subscription
= read_selection_from_tty(
298 "Does this repository require a valid Proxmox subscription key",
299 subscription_products
,
304 (repo
, key_path
, architectures
, None
)
307 if !Path
::new(&key_path
).exists() {
308 eprintln
!("Keyfile '{key_path}' doesn't exist - make sure to install relevant keyring packages or update config to provide correct path!");
312 let mut id
= read_string_from_tty("Enter mirror ID", suggested_id
.as_deref())?
;
313 while let Err(err
) = MIRROR_ID_SCHEMA
.parse_simple_value(&id
) {
314 eprintln
!("Not a valid mirror ID: {err}");
315 id
= read_string_from_tty("Enter mirror ID", None
)?
;
318 if config
.sections
.contains_key(&id
) {
319 eprintln
!("Config entry '{id}' already exists!");
327 let path
= read_string_from_tty(
328 "Enter (absolute) path where mirrored repository will be stored",
329 Some("/var/lib/proxmox-offline-mirror/mirrors/{id}"),
331 if !path
.starts_with('
/'
) {
332 eprintln
!("Path must start with '/'");
333 } else if Path
::new(&path
).exists() {
334 eprintln
!("Path already exists.");
340 let verify
= read_bool_from_tty(
341 "Should already mirrored files be re-verified when updating the mirror? (io-intensive!)",
344 let sync
= read_bool_from_tty("Should newly written files be written using FSYNC to ensure crash-consistency? (io-intensive!)", Some(true))?
;
358 fn action_add_medium(config
: &SectionConfigData
) -> Result
<MediaConfig
, Error
> {
360 let mut id
= read_string_from_tty("Enter medium ID", None
)?
;
361 while let Err(err
) = MEDIA_ID_SCHEMA
.parse_simple_value(&id
) {
362 eprintln
!("Not a valid medium ID: {err}");
363 id
= read_string_from_tty("Enter medium ID", None
)?
;
366 if config
.sections
.contains_key(&id
) {
367 eprintln
!("Config entry '{id}' already exists!");
374 let mountpoint
= loop {
375 let path
= read_string_from_tty("Enter (absolute) path where medium is mounted", None
)?
;
376 if !path
.starts_with('
/'
) {
377 eprintln
!("Path must start with '/'");
381 let mountpoint
= Path
::new(&path
);
382 if !mountpoint
.exists() {
383 eprintln
!("Path doesn't exist.");
385 let mut statefile
= mountpoint
.to_path_buf();
386 statefile
.push(".mirror-state");
387 if !statefile
.exists()
388 || read_bool_from_tty(
389 &format
!("Found existing statefile at {statefile:?} - proceed?"),
398 let mirrors
: Vec
<MirrorConfig
> = config
.convert_to_typed_array("mirror")?
;
399 let mut available_mirrors
: Vec
<String
> = Vec
::new();
400 for mirror_config
in mirrors
{
401 available_mirrors
.push(mirror_config
.id
);
404 let mut selected_mirrors
: Vec
<String
> = Vec
::new();
414 let actions
= if selected_mirrors
.is_empty() {
415 println
!("No mirrors selected for inclusion on medium so far.");
417 (Action
::SelectMirror
, "Add mirror to selection."),
418 (Action
::Proceed
, "Proceed"),
421 println
!("Mirrors selected for inclusion on medium:");
422 for id
in &selected_mirrors
{
423 println
!("\t- {id}");
426 if available_mirrors
.is_empty() {
427 println
!("No more mirrors available for selection!");
429 (Action
::DeselectMirror
, "Remove mirror from selection."),
430 (Action
::Proceed
, "Proceed"),
434 (Action
::SelectMirror
, "Add mirror to selection."),
435 (Action
::DeselectMirror
, "Remove mirror from selection."),
436 (Action
::Proceed
, "Proceed"),
443 let action
= read_selection_from_tty("Select action", &actions
, Some(0))?
;
447 Action
::SelectMirror
=> {
448 if available_mirrors
.is_empty() {
449 println
!("No (more) unselected mirrors available.");
453 let mirrors
: Vec
<(&str, &str)> = available_mirrors
455 .map(|v
| (v
.as_ref(), v
.as_ref()))
459 read_selection_from_tty("Select a mirror to add", &mirrors
, None
)?
.to_string();
460 available_mirrors
= available_mirrors
462 .filter(|v
| *v
!= selected
)
464 selected_mirrors
.push(selected
);
466 Action
::DeselectMirror
=> {
467 if selected_mirrors
.is_empty() {
468 println
!("No mirrors selected (yet).");
472 let mirrors
: Vec
<(&str, &str)> = selected_mirrors
474 .map(|v
| (v
.as_ref(), v
.as_ref()))
478 read_selection_from_tty("Select a mirror to remove", &mirrors
, None
)?
480 selected_mirrors
= selected_mirrors
482 .filter(|v
| *v
!= selected
)
484 available_mirrors
.push(selected
);
492 let verify
= read_bool_from_tty(
493 "Should mirrored files be re-verified when updating the medium? (io-intensive!)",
496 let sync
= read_bool_from_tty("Should newly written files be written using FSYNC to ensure crash-consistency? (io-intensive!)", Some(true))?
;
501 mirrors
: selected_mirrors
,
513 /// Interactive setup wizard.
514 async
fn setup(_param
: Value
) -> Result
<(), Error
> {
515 if !tty
::stdin_isatty() {
516 bail
!("Setup wizard can only run interactively.");
519 let config_file
= read_string_from_tty("Mirror config file", Some(DEFAULT_CONFIG_PATH
))?
;
520 let _lock
= proxmox_offline_mirror
::config
::lock_config(&config_file
)?
;
522 let (mut config
, _digest
) = proxmox_offline_mirror
::config
::config(&config_file
)?
;
524 if config
.sections
.is_empty() {
525 println
!("Initializing new config.");
527 println
!("Loaded existing config.");
538 let mut mirror_defined
= false;
539 if !config
.sections
.is_empty() {
540 println
!("Existing config entries:");
541 for (section
, (section_type
, _
)) in config
.sections
.iter() {
542 if section_type
== "mirror" {
543 mirror_defined
= true;
545 println
!("{section_type} '{section}'");
550 let actions
= if mirror_defined
{
552 (Action
::AddMirror
, "Add new mirror entry"),
553 (Action
::AddMedium
, "Add new medium entry"),
554 (Action
::Quit
, "Quit"),
558 (Action
::AddMirror
, "Add new mirror entry"),
559 (Action
::Quit
, "Quit"),
563 match read_selection_from_tty("Select Action:", &actions
, Some(0))?
{
564 Action
::Quit
=> break,
565 Action
::AddMirror
=> {
566 let mirror_config
= action_add_mirror(&config
)?
;
567 let id
= mirror_config
.id
.clone();
568 mirror
::init(&mirror_config
)?
;
569 config
.set_data(&id
, "mirror", mirror_config
)?
;
570 save_config(&config_file
, &config
)?
;
571 println
!("Config entry '{id}' added");
572 println
!("Run \"proxmox-offline-mirror mirror snapshot create --config '{config_file}' --id '{id}'\" to create a new mirror snapshot.");
574 Action
::AddMedium
=> {
575 let media_config
= action_add_medium(&config
)?
;
576 let id
= media_config
.id
.clone();
577 config
.set_data(&id
, "medium", media_config
)?
;
578 save_config(&config_file
, &config
)?
;
579 println
!("Config entry '{id}' added");
580 println
!("Run \"proxmox-offline-mirror medium sync --config '{config_file}' --id '{id}'\" to sync mirror snapshots to medium.");
588 let rpcenv
= CliEnvironment
::new();
590 let cmd_def
= CliCommandMap
::new()
591 .insert("setup", CliCommand
::new(&API_METHOD_SETUP
))
592 .insert("config", config_commands())
593 .insert("key", key_commands())
594 .insert("medium", medium_commands())
595 .insert("mirror", mirror_commands());
600 Some(|future
| proxmox_async
::runtime
::main(future
)),