]> git.proxmox.com Git - proxmox-offline-mirror.git/blobdiff - src/bin/proxmox-offline-mirror.rs
fix #4259: mirror: add ignore-errors option
[proxmox-offline-mirror.git] / src / bin / proxmox-offline-mirror.rs
index 805e32ddfab10b89e0bf3361e21ae683bfcd8021..222b561b616cd198acaa03bfa9e453f2d05eb107 100644 (file)
@@ -15,7 +15,7 @@ use proxmox_offline_mirror::helpers::tty::{
 use proxmox_offline_mirror::{
     config::{save_config, MediaConfig, MirrorConfig},
     mirror,
-    types::{ProductType, MIRROR_ID_SCHEMA},
+    types::{ProductType, MEDIA_ID_SCHEMA, MIRROR_ID_SCHEMA},
 };
 
 mod proxmox_offline_mirror_cmds;
@@ -92,25 +92,77 @@ impl Display for ProxmoxVariant {
     }
 }
 
-fn action_add_mirror(config: &SectionConfigData) -> Result<MirrorConfig, Error> {
+fn derive_debian_repo(
+    release: &Release,
+    variant: &DebianVariant,
+    components: &str,
+) -> (String, String, String) {
+    let url = match (release, variant) {
+        (Release::Bullseye, DebianVariant::Main) => "http://deb.debian.org/debian bullseye",
+        (Release::Bullseye, DebianVariant::Security) => {
+            "http://deb.debian.org/debian-security bullseye-security"
+        }
+        (Release::Bullseye, DebianVariant::Updates) => {
+            "http://deb.debian.org/debian bullseye-updates"
+        }
+        (Release::Bullseye, DebianVariant::Backports) => {
+            "http://deb.debian.org/debian bullseye-backports"
+        }
+        (Release::Bullseye, DebianVariant::Debug) => {
+            "http://deb.debian.org/debian-debug bullseye-debug"
+        }
+        (Release::Buster, DebianVariant::Main) => "http://deb.debian.org/debian buster",
+        (Release::Buster, DebianVariant::Security) => {
+            "http://deb.debian.org/debian-security buster/updates"
+        }
+        (Release::Buster, DebianVariant::Updates) => "http://deb.debian.org/debian buster-updates",
+        (Release::Buster, DebianVariant::Backports) => {
+            "http://deb.debian.org/debian buster-backports"
+        }
+        (Release::Buster, DebianVariant::Debug) => {
+            "http://deb.debian.org/debian-debug buster-debug"
+        }
+    };
+
+    let url = format!("{url} {components}");
+    let key = match (release, variant) {
+        (Release::Bullseye, DebianVariant::Security) => {
+            "/usr/share/keyrings/debian-archive-bullseye-security-automatic.gpg"
+        }
+        (Release::Bullseye, _) => "/usr/share/keyrings/debian-archive-bullseye-automatic.gpg",
+        (Release::Buster, DebianVariant::Security) => {
+            "/usr/share/keyrings/debian-archive-buster-security-automatic.gpg"
+        }
+        (Release::Buster, _) => "/usr/share/keyrings/debian-archive-buster-stable.gpg",
+    };
+
+    let suggested_id = format!("debian_{release}_{variant}");
+
+    (url, key.to_string(), suggested_id)
+}
+
+fn action_add_mirror(config: &SectionConfigData) -> Result<Vec<MirrorConfig>, Error> {
     let mut use_subscription = None;
+    let mut extra_repos = Vec::new();
 
     let (repository, key_path, architectures, suggested_id) = if read_bool_from_tty(
         "Guided Setup",
         Some(true),
     )? {
         let distros = &[
-            (Distro::Debian, "Debian"),
+            (Distro::Pve, "Proxmox VE"),
             (Distro::Pbs, "Proxmox Backup Server"),
             (Distro::Pmg, "Proxmox Mail Gateway"),
-            (Distro::Pve, "Proxmox VE"),
-            (Distro::PveCeph, "Proxmox VE Ceph"),
+            (Distro::PveCeph, "Proxmox Ceph"),
+            (Distro::Debian, "Debian"),
         ];
         let dist = read_selection_from_tty("Select distro to mirror", distros, None)?;
 
         let releases = &[(Release::Bullseye, "Bullseye"), (Release::Buster, "Buster")];
         let release = read_selection_from_tty("Select release", releases, Some(0))?;
 
+        let mut add_debian_repo = false;
+
         let (url, key_path, suggested_id) = match dist {
             Distro::Debian => {
                 let variants = &[
@@ -127,54 +179,7 @@ fn action_add_mirror(config: &SectionConfigData) -> Result<MirrorConfig, Error>
                     Some("main contrib non-free"),
                 )?;
 
-                let url = match (release, variant) {
-                    (Release::Bullseye, DebianVariant::Main) => {
-                        "http://deb.debian.org/debian bullseye"
-                    }
-                    (Release::Bullseye, DebianVariant::Security) => {
-                        "http://deb.debian.org/debian-security bullseye-security"
-                    }
-                    (Release::Bullseye, DebianVariant::Updates) => {
-                        "http://deb.debian.org/debian bullseye-updates"
-                    }
-                    (Release::Bullseye, DebianVariant::Backports) => {
-                        "http://deb.debian.org/debian bullseye-backports"
-                    }
-                    (Release::Bullseye, DebianVariant::Debug) => {
-                        "http://deb.debian.org/debian-debug bullseye-debug"
-                    }
-                    (Release::Buster, DebianVariant::Main) => "http://deb.debian.org/debian buster",
-                    (Release::Buster, DebianVariant::Security) => {
-                        "http://deb.debian.org/debian-security buster/updates"
-                    }
-                    (Release::Buster, DebianVariant::Updates) => {
-                        "http://deb.debian.org/debian buster-updates"
-                    }
-                    (Release::Buster, DebianVariant::Backports) => {
-                        "http://deb.debian.org/debian buster-backports"
-                    }
-                    (Release::Buster, DebianVariant::Debug) => {
-                        "http://deb.debian.org/debian-debug buster-debug"
-                    }
-                };
-
-                let url = format!("{url} {components}");
-                let key = match (release, variant) {
-                    (Release::Bullseye, DebianVariant::Security) => {
-                        "/usr/share/keyrings/debian-archive-bullseye-security-automatic.gpg"
-                    }
-                    (Release::Bullseye, _) => {
-                        "/usr/share/keyrings/debian-archive-bullseye-automatic.gpg"
-                    }
-                    (Release::Buster, DebianVariant::Security) => {
-                        "/usr/share/keyrings/debian-archive-buster-security-automatic.gpg"
-                    }
-                    (Release::Buster, _) => "/usr/share/keyrings/debian-archive-buster-stable.gpg",
-                };
-
-                let suggested_id = format!("{dist}_{release}_{variant}");
-
-                (url, key.to_string(), Some(suggested_id))
+                derive_debian_repo(release, variant, &components)
             }
             Distro::PveCeph => {
                 enum CephRelease {
@@ -226,7 +231,7 @@ fn action_add_mirror(config: &SectionConfigData) -> Result<MirrorConfig, Error>
                 );
                 let suggested_id = format!("ceph_{ceph_release}_{release}");
 
-                (url, key.to_string(), Some(suggested_id))
+                (url, key.to_string(), suggested_id)
             }
             product => {
                 let variants = &[
@@ -262,15 +267,43 @@ fn action_add_mirror(config: &SectionConfigData) -> Result<MirrorConfig, Error>
 
                 let suggested_id = format!("{product}_{release}_{variant}");
 
-                (url, key.to_string(), Some(suggested_id))
+                add_debian_repo = read_bool_from_tty(
+                    "Should missing Debian mirrors for the selected product be auto-added",
+                    Some(true),
+                )?;
+
+                (url, key.to_string(), suggested_id)
             }
         };
 
         let architectures = vec!["amd64".to_string(), "all".to_string()];
-        (format!("deb {url}"), key_path, architectures, suggested_id)
+
+        if add_debian_repo {
+            extra_repos.push(derive_debian_repo(
+                release,
+                &DebianVariant::Main,
+                "main contrib",
+            ));
+            extra_repos.push(derive_debian_repo(
+                release,
+                &DebianVariant::Updates,
+                "main contrib",
+            ));
+            extra_repos.push(derive_debian_repo(
+                release,
+                &DebianVariant::Security,
+                "main contrib",
+            ));
+        }
+        (
+            format!("deb {url}"),
+            key_path,
+            architectures,
+            Some(suggested_id),
+        )
     } else {
         let repo = read_string_from_tty("Enter repository line in sources.list format", None)?;
-        let key_path = read_string_from_tty("Enter path to repository key file", None)?;
+        let key_path = read_string_from_tty("Enter (absolute) path to repository key file", None)?;
         let architectures =
             read_string_from_tty("Enter list of architectures to mirror", Some("amd64,all"))?;
         let architectures: Vec<String> = architectures
@@ -318,11 +351,13 @@ fn action_add_mirror(config: &SectionConfigData) -> Result<MirrorConfig, Error>
         break id;
     };
 
-    let dir = loop {
-        let path =
-            read_string_from_tty("Enter path where mirrored repository will be stored", None)?;
-        if Path::new(&path).exists() {
-            eprintln!("Path already exists.");
+    let base_dir = loop {
+        let path = read_string_from_tty(
+            "Enter (absolute) base path where mirrored repositories will be stored",
+            Some("/var/lib/proxmox-offline-mirror/mirrors/"),
+        )?;
+        if !path.starts_with('/') {
+            eprintln!("Path must start with '/'");
         } else {
             break path;
         }
@@ -334,21 +369,67 @@ fn action_add_mirror(config: &SectionConfigData) -> Result<MirrorConfig, Error>
     )?;
     let sync = read_bool_from_tty("Should newly written files be written using FSYNC to ensure crash-consistency? (io-intensive!)", Some(true))?;
 
-    Ok(MirrorConfig {
+    let mut configs = Vec::with_capacity(extra_repos.len() + 1);
+
+    for (url, key_path, suggested_id) in extra_repos {
+        if config.sections.contains_key(&suggested_id) {
+            eprintln!("config section '{suggested_id}' already exists, skipping..");
+        } else {
+            let repository = format!("deb {url}");
+
+            configs.push(MirrorConfig {
+                id: suggested_id,
+                repository,
+                architectures: architectures.clone(),
+                key_path,
+                verify,
+                sync,
+                base_dir: base_dir.clone(),
+                use_subscription: None,
+                ignore_errors: false,
+            });
+        }
+    }
+
+    let main_config = MirrorConfig {
         id,
         repository,
         architectures,
         key_path,
         verify,
         sync,
-        dir,
+        base_dir,
         use_subscription,
-    })
+        ignore_errors: false,
+    };
+
+    configs.push(main_config);
+    Ok(configs)
 }
 
 fn action_add_medium(config: &SectionConfigData) -> Result<MediaConfig, Error> {
+    let id = loop {
+        let id = read_string_from_tty("Enter new medium ID", None)?;
+        if let Err(err) = MEDIA_ID_SCHEMA.parse_simple_value(&id) {
+            eprintln!("Not a valid medium ID: {err}");
+            continue;
+        }
+
+        if config.sections.contains_key(&id) {
+            eprintln!("Config entry '{id}' already exists!");
+            continue;
+        }
+
+        break id;
+    };
+
     let mountpoint = loop {
-        let path = read_string_from_tty("Enter path where medium is mounted", None)?;
+        let path = read_string_from_tty("Enter (absolute) path where medium is mounted", None)?;
+        if !path.starts_with('/') {
+            eprintln!("Path must start with '/'");
+            continue;
+        }
+
         let mountpoint = Path::new(&path);
         if !mountpoint.exists() {
             eprintln!("Path doesn't exist.");
@@ -376,37 +457,60 @@ fn action_add_medium(config: &SectionConfigData) -> Result<MediaConfig, Error> {
 
     enum Action {
         SelectMirror,
+        SelectAllMirrors,
         DeselectMirror,
+        DeselectAllMirrors,
         Proceed,
     }
-    let actions = &[
-        (Action::SelectMirror, "Add mirror to selected mirrors."),
-        (
-            Action::DeselectMirror,
-            "Remove mirror from selected mirrors.",
-        ),
-        (Action::Proceed, "Proceed"),
-    ];
 
     loop {
         println!();
-        if selected_mirrors.is_empty() {
-            println!("No mirrors selected so far.");
+        let actions = if selected_mirrors.is_empty() {
+            println!("No mirrors selected for inclusion on medium so far.");
+            vec![
+                (Action::SelectMirror, "Add mirror to selection."),
+                (Action::SelectAllMirrors, "Add all mirrors to selection."),
+                (Action::Proceed, "Proceed"),
+            ]
         } else {
-            println!("Selected mirrors:");
+            println!("Mirrors selected for inclusion on medium:");
             for id in &selected_mirrors {
                 println!("\t- {id}");
             }
-        }
+            println!();
+            if available_mirrors.is_empty() {
+                println!("No more mirrors available for selection!");
+                vec![
+                    (Action::DeselectMirror, "Remove mirror from selection."),
+                    (
+                        Action::DeselectAllMirrors,
+                        "Remove all mirrors from selection.",
+                    ),
+                    (Action::Proceed, "Proceed"),
+                ]
+            } else {
+                vec![
+                    (Action::SelectMirror, "Add mirror to selection."),
+                    (Action::SelectAllMirrors, "Add all mirrors to selection."),
+                    (Action::DeselectMirror, "Remove mirror from selection."),
+                    (
+                        Action::DeselectAllMirrors,
+                        "Remove all mirrors from selection.",
+                    ),
+                    (Action::Proceed, "Proceed"),
+                ]
+            }
+        };
+
         println!();
 
-        let action = read_selection_from_tty("Select action", actions, Some(0))?;
+        let action = read_selection_from_tty("Select action", &actions, Some(0))?;
         println!();
 
         match action {
             Action::SelectMirror => {
                 if available_mirrors.is_empty() {
-                    println!("No unselected mirrors available.");
+                    println!("No (more) unselected mirrors available.");
                     continue;
                 }
 
@@ -423,9 +527,13 @@ fn action_add_medium(config: &SectionConfigData) -> Result<MediaConfig, Error> {
                     .collect();
                 selected_mirrors.push(selected);
             }
+            Action::SelectAllMirrors => {
+                selected_mirrors.extend_from_slice(&available_mirrors);
+                available_mirrors.truncate(0);
+            }
             Action::DeselectMirror => {
                 if selected_mirrors.is_empty() {
-                    println!("No selected mirrors available.");
+                    println!("No mirrors selected (yet).");
                     continue;
                 }
 
@@ -443,6 +551,10 @@ fn action_add_medium(config: &SectionConfigData) -> Result<MediaConfig, Error> {
                     .collect();
                 available_mirrors.push(selected);
             }
+            Action::DeselectAllMirrors => {
+                available_mirrors.extend_from_slice(&selected_mirrors);
+                selected_mirrors.truncate(0);
+            }
             Action::Proceed => {
                 break;
             }
@@ -455,21 +567,6 @@ fn action_add_medium(config: &SectionConfigData) -> Result<MediaConfig, Error> {
     )?;
     let sync = read_bool_from_tty("Should newly written files be written using FSYNC to ensure crash-consistency? (io-intensive!)", Some(true))?;
 
-    let id = loop {
-        let mut id = read_string_from_tty("Enter medium ID", None)?;
-        while let Err(err) = MIRROR_ID_SCHEMA.parse_simple_value(&id) {
-            eprintln!("Not a valid medium ID: {err}");
-            id = read_string_from_tty("Enter medium ID", None)?;
-        }
-
-        if config.sections.contains_key(&id) {
-            eprintln!("Config entry '{id}' already exists!");
-            continue;
-        }
-
-        break id;
-    };
-
     Ok(MediaConfig {
         id,
         mountpoint,
@@ -482,16 +579,22 @@ fn action_add_medium(config: &SectionConfigData) -> Result<MediaConfig, Error> {
 #[api(
     input: {
         properties: {
+            config: {
+                type: String,
+                optional: true,
+                description: "Path to mirroring config file.",
+            },
         },
     },
 )]
 /// Interactive setup wizard.
-async fn setup(_param: Value) -> Result<(), Error> {
+async fn setup(config: Option<String>, _param: Value) -> Result<(), Error> {
     if !tty::stdin_isatty() {
         bail!("Setup wizard can only run interactively.");
     }
 
-    let config_file = read_string_from_tty("Mirror config file", Some(DEFAULT_CONFIG_PATH))?;
+    let config_file = config.unwrap_or_else(get_config_path);
+
     let _lock = proxmox_offline_mirror::config::lock_config(&config_file)?;
 
     let (mut config, _digest) = proxmox_offline_mirror::config::config(&config_file)?;
@@ -508,32 +611,44 @@ async fn setup(_param: Value) -> Result<(), Error> {
         Quit,
     }
 
-    let actions = &[
-        (Action::AddMirror, "Add new mirror entry"),
-        (Action::AddMedium, "Add new medium entry"),
-        (Action::Quit, "Quit"),
-    ];
-
     loop {
         println!();
+        let mut mirror_defined = false;
         if !config.sections.is_empty() {
             println!("Existing config entries:");
             for (section, (section_type, _)) in config.sections.iter() {
+                if section_type == "mirror" {
+                    mirror_defined = true;
+                }
                 println!("{section_type} '{section}'");
             }
             println!();
         }
 
-        match read_selection_from_tty("Select Action:", actions, Some(0))? {
+        let actions = if mirror_defined {
+            vec![
+                (Action::AddMirror, "Add new mirror entry"),
+                (Action::AddMedium, "Add new medium entry"),
+                (Action::Quit, "Quit"),
+            ]
+        } else {
+            vec![
+                (Action::AddMirror, "Add new mirror entry"),
+                (Action::Quit, "Quit"),
+            ]
+        };
+
+        match read_selection_from_tty("Select Action:", &actions, Some(0))? {
             Action::Quit => break,
             Action::AddMirror => {
-                let mirror_config = action_add_mirror(&config)?;
-                let id = mirror_config.id.clone();
-                mirror::init(&mirror_config)?;
-                config.set_data(&id, "mirror", mirror_config)?;
-                save_config(&config_file, &config)?;
-                println!("Config entry '{id}' added");
-                println!("Run \"proxmox-apt-mirror mirror snapshot create --config '{config_file}' --id '{id}'\" to create a new mirror snapshot.");
+                for mirror_config in action_add_mirror(&config)? {
+                    let id = mirror_config.id.clone();
+                    mirror::init(&mirror_config)?;
+                    config.set_data(&id, "mirror", mirror_config)?;
+                    save_config(&config_file, &config)?;
+                    println!("Config entry '{id}' added");
+                    println!("Run \"proxmox-offline-mirror mirror snapshot create --config '{config_file}' '{id}'\" to create a new mirror snapshot.");
+                }
             }
             Action::AddMedium => {
                 let media_config = action_add_medium(&config)?;
@@ -541,7 +656,7 @@ async fn setup(_param: Value) -> Result<(), Error> {
                 config.set_data(&id, "medium", media_config)?;
                 save_config(&config_file, &config)?;
                 println!("Config entry '{id}' added");
-                println!("Run \"proxmox-apt-mirror medium sync --config '{config_file}' --id '{id}'\" to sync mirror snapshots to medium.");
+                println!("Run \"proxmox-offline-mirror medium sync --config '{config_file}' '{id}'\" to sync mirror snapshots to medium.");
             }
         }
     }