]> git.proxmox.com Git - proxmox-offline-mirror.git/commitdiff
rename proxmox-apt-repo to proxmox-offline-mirror-helper
authorFabian Grünbichler <f.gruenbichler@proxmox.com>
Fri, 9 Sep 2022 11:26:40 +0000 (13:26 +0200)
committerFabian Grünbichler <f.gruenbichler@proxmox.com>
Fri, 9 Sep 2022 12:04:29 +0000 (14:04 +0200)
and move it into its own package

Signed-off-by: Fabian Grünbichler <f.gruenbichler@proxmox.com>
23 files changed:
Makefile
debian/control
debian/control.docs [deleted file]
debian/control.extra [new file with mode: 0644]
debian/debcargo.toml
debian/proxmox-apt-repo.bc [deleted file]
debian/proxmox-offline-mirror-helper.bash-completion [new file with mode: 0644]
debian/proxmox-offline-mirror-helper.bc [new file with mode: 0644]
debian/proxmox-offline-mirror.bash-completion
debian/rules
docs/Makefile
docs/command-syntax.rst
docs/conf.py
docs/introduction.rst
docs/offline-keys.rst
docs/offline-media.rst
docs/proxmox-apt-repo/description.rst [deleted file]
docs/proxmox-apt-repo/man1.rst [deleted file]
docs/proxmox-offline-mirror-helper/description.rst [new file with mode: 0644]
docs/proxmox-offline-mirror-helper/man1.rst [new file with mode: 0644]
src/bin/proxmox-apt-repo.rs [deleted file]
src/bin/proxmox-offline-mirror-helper.rs [new file with mode: 0644]
src/lib.rs

index 48279dc326e046f9b46dad83368c66b35e64d776..ff195aee9132e9fef5bfd216ef514cdb434f6463 100644 (file)
--- a/Makefile
+++ b/Makefile
@@ -9,8 +9,10 @@ BUILDDIR_TMP ?= $(BUILDDIR).tmp
 SUBDIRS := docs
 
 DEB=$(PACKAGE)_$(DEB_VERSION_UPSTREAM_REVISION)_$(DEB_BUILD_ARCH).deb
+HELPER_DEB=$(PACKAGE)-helper_$(DEB_VERSION_UPSTREAM_REVISION)_$(DEB_BUILD_ARCH).deb
 LIB_DEB=librust-$(PACKAGE)-dev_$(DEB_VERSION_UPSTREAM_REVISION)_$(DEB_BUILD_ARCH).deb
 DBG_DEB=$(PACKAGE)-dbgsym_$(DEB_VERSION_UPSTREAM_REVISION)_$(DEB_BUILD_ARCH).deb
+HELPER_DBG_DEB=$(PACKAGE)-helper-dbgsym_$(DEB_VERSION_UPSTREAM_REVISION)_$(DEB_BUILD_ARCH).deb
 DOC_DEB=$(PACKAGE)-docs_$(DEB_VERSION_UPSTREAM_REVISION)_all.deb
 DSC=rust-$(PACKAGE)_$(DEB_VERSION_UPSTREAM_REVISION).dsc
 
@@ -42,7 +44,7 @@ build:
          --directory $(BUILDDIR_TMP) \
          $(PACKAGE) \
          $(shell dpkg-parsechangelog -l debian/changelog -SVersion | sed -e 's/-.*//')
-       cat $(BUILDDIR_TMP)/debian/control debian/control.docs > debian/control
+       cat $(BUILDDIR_TMP)/debian/control debian/control.extra > debian/control
        cp -a debian/control $(BUILDDIR_TMP)/debian/control
        rm -f $(BUILDDIR_TMP)/Cargo.lock
        find $(BUILDDIR_TMP)/debian -name "*.hint" -delete
@@ -66,7 +68,7 @@ dinstall: $(DEB)
 
 .PHONY: upload
 upload: $(DEB)
-       tar cf - $(DEB) $(DBG_DEB) $(DOC_DEB) | ssh -X repoman@repo.proxmox.com -- upload --product pve,pmg,pbs,pbs-client --dist bullseye --arch $(DEB_BUILD_ARCH)
+       tar cf - $(DEB) $(HELPER_DEB) $(DBG_DEB) $(HELPER_DBG_DEB) $(DOC_DEB) | ssh -X repoman@repo.proxmox.com -- upload --product pve,pmg,pbs,pbs-client --dist bullseye --arch $(DEB_BUILD_ARCH)
 
 .PHONY: distclean
 distclean: clean
index 3cd9641e246421ff2440d51095efea2b3aa4558a..23c3e5b62d6339803cace8477f25ddad7303da1d 100644 (file)
@@ -108,10 +108,9 @@ Provides:
 Built-Using: ${cargo:Built-Using}
 XB-X-Cargo-Built-Using: ${cargo:X-Cargo-Built-Using}
 Description: Proxmox offline repository mirror and subscription key manager
- This package contains the following binaries for managing Proxmox offline APT
- repositories and subscription keys:
-  - proxmox-offline-mirror (binary for the mirror host with internet access)
-  - proxmox-apt-repo (binary for the Proxmox host without internet access)
+ This package contains the proxmox-offline-mirror tool for managing Proxmox
+ offline APT
+ repositories and subscription keys.
 
 Package: proxmox-offline-mirror-docs
 Architecture: all
@@ -120,3 +119,25 @@ Breaks: proxmox-offline-mirror (<< 0.2.0~)
 Replaces: proxmox-offline-mirror (<< 0.2.0~)
 Description: Proxmox offline repository mirror and subscription key manager
  This package contains the documentation for proxmox-offline-mirror.
+
+Package: proxmox-offline-mirror-helper
+Architecture: any
+Multi-Arch: allowed
+Section: admin
+Depends:
+ ${misc:Depends},
+ ${shlibs:Depends},
+ ${cargo:Depends},
+ proxmox-offline-mirror-docs,
+ proxmox-archive-keyring
+Recommends:
+ ${cargo:Recommends}
+Suggests:
+ ${cargo:Suggests}
+Provides:
+ ${cargo:Provides}
+Built-Using: ${cargo:Built-Using}
+XB-X-Cargo-Built-Using: ${cargo:X-Cargo-Built-Using}
+Description: Proxmox offline repository mirror and subscription key manager helper
+ This package contains the proxmox-offline-mirror-helper binary for managing Proxmox offline APT
+ repositories and subscription keys on Proxmox offline systems.
\ No newline at end of file
diff --git a/debian/control.docs b/debian/control.docs
deleted file mode 100644 (file)
index 683b706..0000000
+++ /dev/null
@@ -1,8 +0,0 @@
-
-Package: proxmox-offline-mirror-docs
-Architecture: all
-Section: admin
-Breaks: proxmox-offline-mirror (<< 0.2.0~)
-Replaces: proxmox-offline-mirror (<< 0.2.0~)
-Description: Proxmox offline repository mirror and subscription key manager
- This package contains the documentation for proxmox-offline-mirror.
diff --git a/debian/control.extra b/debian/control.extra
new file mode 100644 (file)
index 0000000..e813aa9
--- /dev/null
@@ -0,0 +1,30 @@
+
+Package: proxmox-offline-mirror-docs
+Architecture: all
+Section: admin
+Breaks: proxmox-offline-mirror (<< 0.2.0~)
+Replaces: proxmox-offline-mirror (<< 0.2.0~)
+Description: Proxmox offline repository mirror and subscription key manager
+ This package contains the documentation for proxmox-offline-mirror.
+
+Package: proxmox-offline-mirror-helper
+Architecture: any
+Multi-Arch: allowed
+Section: admin
+Depends:
+ ${misc:Depends},
+ ${shlibs:Depends},
+ ${cargo:Depends},
+ proxmox-offline-mirror-docs,
+ proxmox-archive-keyring
+Recommends:
+ ${cargo:Recommends}
+Suggests:
+ ${cargo:Suggests}
+Provides:
+ ${cargo:Provides}
+Built-Using: ${cargo:Built-Using}
+XB-X-Cargo-Built-Using: ${cargo:X-Cargo-Built-Using}
+Description: Proxmox offline repository mirror and subscription key manager helper
+ This package contains the proxmox-offline-mirror-helper binary for managing Proxmox offline APT
+ repositories and subscription keys on Proxmox offline systems.
\ No newline at end of file
index aeda7d9b7b94c32ee70c80833e41c922f0ea1659..769d5381c11457b3d6f7973c7f97a0d67b304168 100644 (file)
@@ -9,9 +9,7 @@ vcs_browser = "https://git.proxmox.com/?p=proxmox-offline-mirror.git"
 [packages.bin]
 section = "admin"
 description = """
-This package contains the following binaries for managing Proxmox offline APT
-repositories and subscription keys:
-- proxmox-offline-mirror (binary for the mirror host with internet access)
-- proxmox-apt-repo (binary for the Proxmox host without internet access)
+This package contains the proxmox-offline-mirror tool for managing Proxmox offline APT
+repositories and subscription keys.
 """
 depends = ["proxmox-offline-mirror-docs", "proxmox-archive-keyring"]
diff --git a/debian/proxmox-apt-repo.bc b/debian/proxmox-apt-repo.bc
deleted file mode 100644 (file)
index 96ddd86..0000000
+++ /dev/null
@@ -1,8 +0,0 @@
-# proxmox-apt-repo completion
-
-# see http://tiswww.case.edu/php/chet/bash/FAQ
-# and __ltrim_colon_completions() in /usr/share/bash-completion/bash_completion
-# this modifies global var, but I found no better way
-COMP_WORDBREAKS=${COMP_WORDBREAKS//:}
-
-complete -C 'proxmox-apt-repo bashcomplete' proxmox-apt-repo
diff --git a/debian/proxmox-offline-mirror-helper.bash-completion b/debian/proxmox-offline-mirror-helper.bash-completion
new file mode 100644 (file)
index 0000000..383d800
--- /dev/null
@@ -0,0 +1 @@
+debian/proxmox-offline-mirror-helper.bc proxmox-offline-mirror-helper
diff --git a/debian/proxmox-offline-mirror-helper.bc b/debian/proxmox-offline-mirror-helper.bc
new file mode 100644 (file)
index 0000000..b91be2d
--- /dev/null
@@ -0,0 +1,8 @@
+# proxmox-offline-mirror-helper completion
+
+# see http://tiswww.case.edu/php/chet/bash/FAQ
+# and __ltrim_colon_completions() in /usr/share/bash-completion/bash_completion
+# this modifies global var, but I found no better way
+COMP_WORDBREAKS=${COMP_WORDBREAKS//:}
+
+complete -C 'proxmox-offline-mirror-helper bashcomplete' proxmox-offline-mirror-helper
index c04c2a18fc2d71dad22f9ca6bfc9c602cbf3e47e..8239e29a679788fb5f34744a7492d8fd2fa3714c 100644 (file)
@@ -1,2 +1 @@
 debian/proxmox-offline-mirror.bc proxmox-offline-mirror
-debian/proxmox-apt-repo.bc proxmox-apt-repo
index 561c381a373fc5d94e3019232c100e7ea1036457..a45da527bd338505953160cafedf588f7b74a084 100644 (file)
@@ -12,9 +12,12 @@ override_dh_auto_test:
        # dh_auto_test -- test --all
 
 override_dh_auto_install:
-       dh_auto_install
+       DESTDIR=debian/proxmox-offline-mirror dh_auto_install
        DESTDIR=../debian/proxmox-offline-mirror-docs make -C docs install
        rm debian/proxmox-offline-mirror/usr/bin/docgen
+       mkdir -p debian/proxmox-offline-mirror-helper/usr/bin
+       mv debian/proxmox-offline-mirror/usr/bin/proxmox-offline-mirror-helper \
+        debian/proxmox-offline-mirror-helper/usr/bin
 
 override_dh_missing:
        dh_missing --fail-missing
index 81931a6414ac8fbfa273301e5ef43ab14ffa598a..6d680a70908076af526a99ed1ebb18334e93879d 100644 (file)
@@ -2,12 +2,12 @@ include ../defines.mk
 
 GENERATED_SYNOPSIS :=                                          \
        proxmox-offline-mirror/synopsis.rst                     \
-       proxmox-apt-repo/synopsis.rst                   \
+       proxmox-offline-mirror-helper/synopsis.rst                      \
        config/mirror/config.rst
 
 MAN1_PAGES :=                          \
        proxmox-offline-mirror.1        \
-       proxmox-apt-repo.1
+       proxmox-offline-mirror-helper.1
 
 MAN5_PAGES :=                          \
        proxmox-offline-mirror.cfg.5
index 7fefc0bb47cfb61e7d8ad4cc6400bf9345fd0fd7..d38921b1dd0e46cace9b4c0af48502aac103ad6c 100644 (file)
@@ -7,7 +7,7 @@ Command Syntax
 .. include:: proxmox-offline-mirror/synopsis.rst
 
 
-``proxmox-apt-repo``
+``proxmox-offline-mirror-helper``
 --------------------
 
-.. include:: proxmox-apt-repo/synopsis.rst
+.. include:: proxmox-offline-mirror-helper/synopsis.rst
index 82a54a0f4904661804e3ec7e68b851eef3b3a382..54410347122e576b6182425b8a7f1c12e44fb471 100644 (file)
@@ -93,7 +93,7 @@ rst_epilog += f"\n..  |pom-copyright| replace:: Copyright (C) {copyright}"
 man_pages = [
     # CLI
     ('proxmox-offline-mirror/man1', 'proxmox-offline-mirror', 'Command line tool for Backup and Restore', [author], 1),
-    ('proxmox-apt-repo/man1', 'proxmox-apt-repo', 'Command line tool to manage and configure the backup server.', [author], 1),
+    ('proxmox-offline-mirror-helper/man1', 'proxmox-offline-mirror-helper', 'Command line tool to manage and configure the backup server.', [author], 1),
     # configs
     ('config/mirror/man5', 'proxmox-offline-mirror.cfg', 'Proxmox Offline Mirror Configuration', [author], 5),
 ]
index 9f526406bdb725c24960d457d0988bb45f439926..d2bbbe73fed6f2ca55932bbb4c184c3e1d03ff76 100644 (file)
@@ -16,7 +16,7 @@ This tool consists of two binaries:
 ``proxmox-offline-mirror``
   The mirror tool to create and manage mirrors and media containing repositories
 
-``proxmox-apt-repo``
+``proxmox-offline-mirror-helper``
   The helper to use the external medium on offline Proxmox VE, Proxmox Mail Gateway or Proxmox
   Backup Server systems as well as managing subscriptions on these systems.
 
index 04d741a32a2114fa6c189bb554dbd10b1759f839..043743b9fd3597dbe766e9ed8d3ecd676ae04c92 100644 (file)
@@ -58,6 +58,6 @@ Deploy Keys
 -----------
 
 The subscription information is transferred to a medium (see :ref:`sync_medium`) and can then be
-activated on the offline system with either ``proxmox-apt-repo offline-key`` or ``proxmox-apt-repo
-setup``. This process must be repeated at least once a year or before the next due date of the
-subscription key is reached, whichever comes first.
+activated on the offline system with either ``proxmox-offline-mirror-helper offline-key`` or
+``proxmox-offline-mirror-helper setup``. This process must be repeated at least once a year or
+before the next due date of the subscription key is reached, whichever comes first.
index 994aef6dbca59b382e419de5f356abac246957e7..70cfff2dcc67b1ebba664e612688e9018d8a14af 100644 (file)
@@ -35,17 +35,17 @@ To sync the local mirrors to a medium, the following command can be used:
   proxmox-offline-mirror medium sync --id pve-bullseye
 
 This command will sync all mirrors linked with this medium to the medium's mount point.
-Additionally, it will sync all offline keys for further processing by ``proxmox-apt-repo`` on the
+Additionally, it will sync all offline keys for further processing by ``proxmox-offline-mirror-helper`` on the
 target system.
 
 Using a Medium
 --------------
 
 After syncing a medium, unmount it and make it accessible on the (offline) target system.  Either
-point `apt` directly at the synced snapshots on the medium or run ``proxmox-apt-repo setup``.  The
+point `apt` directly at the synced snapshots on the medium or run ``proxmox-offline-mirror-helper setup``.  The
 setup will let you select the mirrors and snapshots and can generate a `sources.list.d` snippet.
 This snippet can be saved to the ``/etc/apt/sources.list.d`` directory. The default file name is
 ``offline-mirror.list``.  Don't forget to remove the snippet after the upgrade is done.
 
-To activate or update an offline subscription key, either use ``proxmox-apt-repo offline-key`` or
-``proxmox-apt-repo setup``.
+To activate or update an offline subscription key, either use ``proxmox-offline-mirror-helper offline-key`` or
+``proxmox-offline-mirror-helper setup``.
diff --git a/docs/proxmox-apt-repo/description.rst b/docs/proxmox-apt-repo/description.rst
deleted file mode 100644 (file)
index f0c7185..0000000
+++ /dev/null
@@ -1,2 +0,0 @@
-This tool serves as a helper to streamline usage of a mirrored offline APT
-repository and offline subscription keys.
\ No newline at end of file
diff --git a/docs/proxmox-apt-repo/man1.rst b/docs/proxmox-apt-repo/man1.rst
deleted file mode 100644 (file)
index e68a584..0000000
+++ /dev/null
@@ -1,16 +0,0 @@
-================
-proxmox-apt-repo
-================
-
-Synopsis
-==========
-
-.. include:: synopsis.rst
-
-Description
-============
-
-.. include:: description.rst
-
-
-.. include:: ../pom-copyright.rst
diff --git a/docs/proxmox-offline-mirror-helper/description.rst b/docs/proxmox-offline-mirror-helper/description.rst
new file mode 100644 (file)
index 0000000..f0c7185
--- /dev/null
@@ -0,0 +1,2 @@
+This tool serves as a helper to streamline usage of a mirrored offline APT
+repository and offline subscription keys.
\ No newline at end of file
diff --git a/docs/proxmox-offline-mirror-helper/man1.rst b/docs/proxmox-offline-mirror-helper/man1.rst
new file mode 100644 (file)
index 0000000..fa7e2e5
--- /dev/null
@@ -0,0 +1,16 @@
+=============================
+proxmox-offline-mirror-helper
+=============================
+
+Synopsis
+==========
+
+.. include:: synopsis.rst
+
+Description
+============
+
+.. include:: description.rst
+
+
+.. include:: ../pom-copyright.rst
diff --git a/src/bin/proxmox-apt-repo.rs b/src/bin/proxmox-apt-repo.rs
deleted file mode 100644 (file)
index d70d3fb..0000000
+++ /dev/null
@@ -1,362 +0,0 @@
-use std::path::PathBuf;
-use std::process::Command;
-use std::{collections::HashMap, path::Path};
-
-use anyhow::{bail, format_err, Error};
-
-use proxmox_offline_mirror::types::{ProductType, Snapshot};
-use proxmox_subscription::SubscriptionInfo;
-use proxmox_sys::command::run_command;
-use proxmox_sys::fs::{replace_file, CreateOptions};
-use proxmox_sys::{fs::file_get_contents, linux::tty};
-use proxmox_time::epoch_to_rfc3339_utc;
-use serde_json::Value;
-
-use proxmox_router::cli::{run_cli_command, CliCommand, CliCommandMap, CliEnvironment};
-use proxmox_schema::{api, param_bail};
-
-use proxmox_offline_mirror::helpers::tty::{
-    read_bool_from_tty, read_selection_from_tty, read_string_from_tty,
-};
-use proxmox_offline_mirror::medium::{self, generate_repo_snippet, MediumState};
-
-fn set_subscription_key(
-    product: ProductType,
-    subscription: &SubscriptionInfo,
-) -> Result<String, Error> {
-    let data = base64::encode(serde_json::to_vec(subscription)?);
-
-    let cmd = match product {
-        ProductType::Pve => {
-            let mut cmd = Command::new("pvesubscription");
-            cmd.arg("set-offline-key");
-            cmd.arg(data);
-            cmd
-        }
-        ProductType::Pbs => {
-            let mut cmd = Command::new("proxmox-backup-manager");
-            cmd.arg("subscription");
-            cmd.arg("set-offline-key");
-            cmd.arg(data);
-            cmd
-        }
-        ProductType::Pmg => {
-            let mut cmd = Command::new("pmgsubscription");
-            cmd.arg("set-offline-key");
-            cmd.arg(data);
-            cmd
-        }
-        ProductType::Pom => unreachable!(),
-    };
-
-    run_command(cmd, Some(|v| v == 0))
-}
-
-#[api(
-    input: {
-        properties: {
-        },
-    },
-)]
-/// Interactive setup wizard.
-async fn setup(_param: Value) -> Result<(), Error> {
-    if !tty::stdin_isatty() {
-        bail!("Setup wizard can only run interactively.");
-    }
-
-    let default_dir = std::env::current_exe().map_or_else(
-        |_| None,
-        |mut p| {
-            p.pop();
-            let p = p.to_str();
-            p.map(str::to_string)
-        },
-    );
-
-    let mountpoint = read_string_from_tty("Path to medium mountpoint", default_dir.as_deref())?;
-    let mountpoint = Path::new(&mountpoint);
-    if !mountpoint.exists() {
-        bail!("Medium mountpoint doesn't exist.");
-    }
-
-    let mut statefile = mountpoint.to_path_buf();
-    statefile.push(".mirror-state");
-
-    println!("Loading state from {statefile:?}..");
-    let raw = file_get_contents(&statefile)?;
-    let state: MediumState = serde_json::from_slice(&raw)?;
-    println!(
-        "Last sync timestamp: {}",
-        epoch_to_rfc3339_utc(state.last_sync)?
-    );
-
-    let mut selected_repos = HashMap::new();
-
-    enum Action {
-        SelectMirrorSnapshot,
-        DeselectMirrorSnapshot,
-        GenerateSourcesList,
-        UpdateOfflineSubscription,
-        Quit,
-    }
-    let actions = &[
-        (
-            Action::SelectMirrorSnapshot,
-            "Add mirror & snapshot to selected repositories.",
-        ),
-        (
-            Action::DeselectMirrorSnapshot,
-            "Remove mirror & snapshot from selected repositories.",
-        ),
-        (
-            Action::GenerateSourcesList,
-            "Generate 'sources.list.d' snippet for accessing selected repositories.",
-        ),
-        (
-            Action::UpdateOfflineSubscription,
-            "Update offline subscription key",
-        ),
-        (Action::Quit, "Quit."),
-    ];
-
-    loop {
-        println!();
-        if selected_repos.is_empty() {
-            println!("No repositories selected so far.");
-        } else {
-            println!("Selected repositories:");
-            for (mirror, (_info, snapshot)) in selected_repos.iter() {
-                println!("\t- {mirror}/{snapshot}");
-            }
-        }
-        println!();
-
-        let action = read_selection_from_tty("Select action", actions, Some(0))?;
-        println!();
-
-        match action {
-            Action::SelectMirrorSnapshot => {
-                let mirrors: Vec<(&str, &str)> = state
-                    .mirrors
-                    .keys()
-                    .filter_map(|k| {
-                        if selected_repos.contains_key(k) {
-                            None
-                        } else {
-                            Some((k.as_ref(), k.as_ref()))
-                        }
-                    })
-                    .collect();
-
-                if mirrors.is_empty() {
-                    println!("All mirrors already selected.");
-                    continue;
-                }
-
-                let selected_mirror = read_selection_from_tty("Select mirror", &mirrors, None)?;
-                let snapshots: Vec<(Snapshot, String)> =
-                    medium::list_snapshots(mountpoint, selected_mirror)?
-                        .into_iter()
-                        .map(|s| (s, s.to_string()))
-                        .collect();
-                if snapshots.is_empty() {
-                    println!("Mirror doesn't have any synced snapshots.");
-                    continue;
-                }
-
-                let snapshots: Vec<(&Snapshot, &str)> = snapshots
-                    .iter()
-                    .map(|(snap, string)| (snap, string.as_ref()))
-                    .collect();
-                let selected_snapshot = read_selection_from_tty(
-                    "Select snapshot",
-                    &snapshots,
-                    Some(snapshots.len() - 1),
-                )?;
-
-                selected_repos.insert(
-                    selected_mirror.to_string(),
-                    (
-                        state.mirrors.get(*selected_mirror).unwrap(),
-                        **selected_snapshot,
-                    ),
-                );
-            }
-            Action::DeselectMirrorSnapshot => {
-                let mirrors: Vec<(&str, &str)> = selected_repos
-                    .keys()
-                    .map(|k| (k.as_ref(), k.as_ref()))
-                    .collect();
-
-                let selected_mirror =
-                    read_selection_from_tty("Deselect mirror", &mirrors, None)?.to_string();
-                selected_repos.remove(&selected_mirror);
-            }
-            Action::GenerateSourcesList => {
-                let lines = generate_repo_snippet(mountpoint, &selected_repos)?;
-                println!("Generated sources.list.d snippet:");
-                let data = lines.join("\n");
-                println!();
-                println!("-----8<-----");
-                println!("{data}");
-                println!("----->8-----");
-                if read_bool_from_tty("Configure snippet as repository source", Some(true))? {
-                    let snippet_file_name = loop {
-                        let file = read_string_from_tty(
-                            "Enter filename under '/etc/apt/sources.list.d/' (will be overwritten)",
-                            Some("offline-mirror.list"),
-                        )?;
-                        if file.contains('/') {
-                            eprintln!("Invalid file name.");
-                        } else {
-                            break file;
-                        }
-                    };
-                    let mut file = PathBuf::from("/etc/apt/sources.list.d");
-                    file.push(snippet_file_name);
-                    replace_file(file, data.as_bytes(), CreateOptions::default(), true)?;
-                } else {
-                    println!("Add above snippet to system's repository entries (/etc/apt/sources.list.d/) manually to configure.");
-                }
-
-                println!("Now run 'apt update && apt full-upgrade' to upgrade system.");
-                println!();
-            }
-            Action::UpdateOfflineSubscription => {
-                let server_id = proxmox_subscription::get_hardware_address()?;
-                let subscriptions: Vec<(&SubscriptionInfo, &str)> = state
-                    .subscriptions
-                    .iter()
-                    .filter_map(|s| {
-                        if let Some(key) = s.key.as_ref() {
-                            if let Ok(product) = key[..3].parse::<ProductType>() {
-                                if product == ProductType::Pom {
-                                    return None;
-                                } else {
-                                    return Some((s, key.as_str()));
-                                }
-                            }
-                        }
-                        None
-                    })
-                    .collect();
-
-                if subscriptions.is_empty() {
-                    println!(
-                        "No matching subscription key found for server ID '{}'",
-                        server_id
-                    );
-                } else {
-                    let info = read_selection_from_tty("Select key", &subscriptions, None)?;
-                    // safe unwrap, checked above!
-                    let product: ProductType = info.key.as_ref().unwrap()[..3].parse()?;
-                    set_subscription_key(product, info)?;
-                }
-            }
-            Action::Quit => {
-                break;
-            }
-        }
-    }
-
-    Ok(())
-}
-
-#[api(
-    input: {
-        properties: {
-            mountpoint: {
-                type: String,
-                optional: true,
-                description: "Path to medium mountpoint - defaults to `proxmox-apt-repo` containing directory.",
-            },
-            product: {
-                type: ProductType,
-            },
-        },
-    },
-)]
-/// Configures and offline subscription key
-async fn setup_offline_key(
-    mountpoint: Option<String>,
-    product: ProductType,
-    _param: Value,
-) -> Result<(), Error> {
-    if product == ProductType::Pom {
-        param_bail!(
-            "product",
-            format_err!("Proxmox Offline Mirror does not support offline operations.")
-        );
-    }
-
-    let mountpoint = mountpoint
-        .or_else(|| {
-            std::env::current_exe().map_or_else(
-                |_| None,
-                |mut p| {
-                    p.pop();
-                    let p = p.to_str();
-                    p.map(str::to_string)
-                },
-            )
-        })
-        .ok_or_else(|| {
-            format_err!("Failed to determine fallback mountpoint via executable path.")
-        })?;
-
-    let mountpoint = Path::new(&mountpoint);
-    if !mountpoint.exists() {
-        bail!("Medium mountpoint doesn't exist.");
-    }
-
-    let mut statefile = mountpoint.to_path_buf();
-    statefile.push(".mirror-state");
-
-    println!("Loading state from {statefile:?}..");
-    let raw = file_get_contents(&statefile)?;
-    let state: MediumState = serde_json::from_slice(&raw)?;
-    println!(
-        "Last sync timestamp: {}",
-        epoch_to_rfc3339_utc(state.last_sync)?
-    );
-
-    let server_id = proxmox_subscription::get_hardware_address()?;
-    let subscription = state.subscriptions.iter().find(|s| {
-        if let Some(key) = s.key.as_ref() {
-            if let Ok(found_product) = key[..3].parse::<ProductType>() {
-                return product == found_product;
-            }
-        }
-        false
-    });
-
-    match subscription {
-        Some(subscription) => {
-            eprintln!("Setting offline subscription key for {product}..");
-            match set_subscription_key(product, subscription) {
-                Ok(output) if !output.is_empty() => eprintln!("success: {output}"),
-                Ok(_) => eprintln!("success."),
-                Err(err) => eprintln!("error: {err}"),
-            }
-            Ok(())
-        }
-        None => bail!("No matching subscription key found for product '{product}' and server ID '{server_id}'"),
-    }
-}
-
-fn main() {
-    let rpcenv = CliEnvironment::new();
-
-    let cmd_def = CliCommandMap::new()
-        .insert("setup", CliCommand::new(&API_METHOD_SETUP))
-        .insert(
-            "offline-key",
-            CliCommand::new(&API_METHOD_SETUP_OFFLINE_KEY),
-        );
-
-    run_cli_command(
-        cmd_def,
-        rpcenv,
-        Some(|future| proxmox_async::runtime::main(future)),
-    );
-}
diff --git a/src/bin/proxmox-offline-mirror-helper.rs b/src/bin/proxmox-offline-mirror-helper.rs
new file mode 100644 (file)
index 0000000..af090a5
--- /dev/null
@@ -0,0 +1,362 @@
+use std::path::PathBuf;
+use std::process::Command;
+use std::{collections::HashMap, path::Path};
+
+use anyhow::{bail, format_err, Error};
+
+use proxmox_offline_mirror::types::{ProductType, Snapshot};
+use proxmox_subscription::SubscriptionInfo;
+use proxmox_sys::command::run_command;
+use proxmox_sys::fs::{replace_file, CreateOptions};
+use proxmox_sys::{fs::file_get_contents, linux::tty};
+use proxmox_time::epoch_to_rfc3339_utc;
+use serde_json::Value;
+
+use proxmox_router::cli::{run_cli_command, CliCommand, CliCommandMap, CliEnvironment};
+use proxmox_schema::{api, param_bail};
+
+use proxmox_offline_mirror::helpers::tty::{
+    read_bool_from_tty, read_selection_from_tty, read_string_from_tty,
+};
+use proxmox_offline_mirror::medium::{self, generate_repo_snippet, MediumState};
+
+fn set_subscription_key(
+    product: ProductType,
+    subscription: &SubscriptionInfo,
+) -> Result<String, Error> {
+    let data = base64::encode(serde_json::to_vec(subscription)?);
+
+    let cmd = match product {
+        ProductType::Pve => {
+            let mut cmd = Command::new("pvesubscription");
+            cmd.arg("set-offline-key");
+            cmd.arg(data);
+            cmd
+        }
+        ProductType::Pbs => {
+            let mut cmd = Command::new("proxmox-backup-manager");
+            cmd.arg("subscription");
+            cmd.arg("set-offline-key");
+            cmd.arg(data);
+            cmd
+        }
+        ProductType::Pmg => {
+            let mut cmd = Command::new("pmgsubscription");
+            cmd.arg("set-offline-key");
+            cmd.arg(data);
+            cmd
+        }
+        ProductType::Pom => unreachable!(),
+    };
+
+    run_command(cmd, Some(|v| v == 0))
+}
+
+#[api(
+    input: {
+        properties: {
+        },
+    },
+)]
+/// Interactive setup wizard.
+async fn setup(_param: Value) -> Result<(), Error> {
+    if !tty::stdin_isatty() {
+        bail!("Setup wizard can only run interactively.");
+    }
+
+    let default_dir = std::env::current_exe().map_or_else(
+        |_| None,
+        |mut p| {
+            p.pop();
+            let p = p.to_str();
+            p.map(str::to_string)
+        },
+    );
+
+    let mountpoint = read_string_from_tty("Path to medium mountpoint", default_dir.as_deref())?;
+    let mountpoint = Path::new(&mountpoint);
+    if !mountpoint.exists() {
+        bail!("Medium mountpoint doesn't exist.");
+    }
+
+    let mut statefile = mountpoint.to_path_buf();
+    statefile.push(".mirror-state");
+
+    println!("Loading state from {statefile:?}..");
+    let raw = file_get_contents(&statefile)?;
+    let state: MediumState = serde_json::from_slice(&raw)?;
+    println!(
+        "Last sync timestamp: {}",
+        epoch_to_rfc3339_utc(state.last_sync)?
+    );
+
+    let mut selected_repos = HashMap::new();
+
+    enum Action {
+        SelectMirrorSnapshot,
+        DeselectMirrorSnapshot,
+        GenerateSourcesList,
+        UpdateOfflineSubscription,
+        Quit,
+    }
+    let actions = &[
+        (
+            Action::SelectMirrorSnapshot,
+            "Add mirror & snapshot to selected repositories.",
+        ),
+        (
+            Action::DeselectMirrorSnapshot,
+            "Remove mirror & snapshot from selected repositories.",
+        ),
+        (
+            Action::GenerateSourcesList,
+            "Generate 'sources.list.d' snippet for accessing selected repositories.",
+        ),
+        (
+            Action::UpdateOfflineSubscription,
+            "Update offline subscription key",
+        ),
+        (Action::Quit, "Quit."),
+    ];
+
+    loop {
+        println!();
+        if selected_repos.is_empty() {
+            println!("No repositories selected so far.");
+        } else {
+            println!("Selected repositories:");
+            for (mirror, (_info, snapshot)) in selected_repos.iter() {
+                println!("\t- {mirror}/{snapshot}");
+            }
+        }
+        println!();
+
+        let action = read_selection_from_tty("Select action", actions, Some(0))?;
+        println!();
+
+        match action {
+            Action::SelectMirrorSnapshot => {
+                let mirrors: Vec<(&str, &str)> = state
+                    .mirrors
+                    .keys()
+                    .filter_map(|k| {
+                        if selected_repos.contains_key(k) {
+                            None
+                        } else {
+                            Some((k.as_ref(), k.as_ref()))
+                        }
+                    })
+                    .collect();
+
+                if mirrors.is_empty() {
+                    println!("All mirrors already selected.");
+                    continue;
+                }
+
+                let selected_mirror = read_selection_from_tty("Select mirror", &mirrors, None)?;
+                let snapshots: Vec<(Snapshot, String)> =
+                    medium::list_snapshots(mountpoint, selected_mirror)?
+                        .into_iter()
+                        .map(|s| (s, s.to_string()))
+                        .collect();
+                if snapshots.is_empty() {
+                    println!("Mirror doesn't have any synced snapshots.");
+                    continue;
+                }
+
+                let snapshots: Vec<(&Snapshot, &str)> = snapshots
+                    .iter()
+                    .map(|(snap, string)| (snap, string.as_ref()))
+                    .collect();
+                let selected_snapshot = read_selection_from_tty(
+                    "Select snapshot",
+                    &snapshots,
+                    Some(snapshots.len() - 1),
+                )?;
+
+                selected_repos.insert(
+                    selected_mirror.to_string(),
+                    (
+                        state.mirrors.get(*selected_mirror).unwrap(),
+                        **selected_snapshot,
+                    ),
+                );
+            }
+            Action::DeselectMirrorSnapshot => {
+                let mirrors: Vec<(&str, &str)> = selected_repos
+                    .keys()
+                    .map(|k| (k.as_ref(), k.as_ref()))
+                    .collect();
+
+                let selected_mirror =
+                    read_selection_from_tty("Deselect mirror", &mirrors, None)?.to_string();
+                selected_repos.remove(&selected_mirror);
+            }
+            Action::GenerateSourcesList => {
+                let lines = generate_repo_snippet(mountpoint, &selected_repos)?;
+                println!("Generated sources.list.d snippet:");
+                let data = lines.join("\n");
+                println!();
+                println!("-----8<-----");
+                println!("{data}");
+                println!("----->8-----");
+                if read_bool_from_tty("Configure snippet as repository source", Some(true))? {
+                    let snippet_file_name = loop {
+                        let file = read_string_from_tty(
+                            "Enter filename under '/etc/apt/sources.list.d/' (will be overwritten)",
+                            Some("offline-mirror.list"),
+                        )?;
+                        if file.contains('/') {
+                            eprintln!("Invalid file name.");
+                        } else {
+                            break file;
+                        }
+                    };
+                    let mut file = PathBuf::from("/etc/apt/sources.list.d");
+                    file.push(snippet_file_name);
+                    replace_file(file, data.as_bytes(), CreateOptions::default(), true)?;
+                } else {
+                    println!("Add above snippet to system's repository entries (/etc/apt/sources.list.d/) manually to configure.");
+                }
+
+                println!("Now run 'apt update && apt full-upgrade' to upgrade system.");
+                println!();
+            }
+            Action::UpdateOfflineSubscription => {
+                let server_id = proxmox_subscription::get_hardware_address()?;
+                let subscriptions: Vec<(&SubscriptionInfo, &str)> = state
+                    .subscriptions
+                    .iter()
+                    .filter_map(|s| {
+                        if let Some(key) = s.key.as_ref() {
+                            if let Ok(product) = key[..3].parse::<ProductType>() {
+                                if product == ProductType::Pom {
+                                    return None;
+                                } else {
+                                    return Some((s, key.as_str()));
+                                }
+                            }
+                        }
+                        None
+                    })
+                    .collect();
+
+                if subscriptions.is_empty() {
+                    println!(
+                        "No matching subscription key found for server ID '{}'",
+                        server_id
+                    );
+                } else {
+                    let info = read_selection_from_tty("Select key", &subscriptions, None)?;
+                    // safe unwrap, checked above!
+                    let product: ProductType = info.key.as_ref().unwrap()[..3].parse()?;
+                    set_subscription_key(product, info)?;
+                }
+            }
+            Action::Quit => {
+                break;
+            }
+        }
+    }
+
+    Ok(())
+}
+
+#[api(
+    input: {
+        properties: {
+            mountpoint: {
+                type: String,
+                optional: true,
+                description: "Path to medium mountpoint - defaults to `proxmox-offline-mirror-helper` containing directory.",
+            },
+            product: {
+                type: ProductType,
+            },
+        },
+    },
+)]
+/// Configures and offline subscription key
+async fn setup_offline_key(
+    mountpoint: Option<String>,
+    product: ProductType,
+    _param: Value,
+) -> Result<(), Error> {
+    if product == ProductType::Pom {
+        param_bail!(
+            "product",
+            format_err!("Proxmox Offline Mirror does not support offline operations.")
+        );
+    }
+
+    let mountpoint = mountpoint
+        .or_else(|| {
+            std::env::current_exe().map_or_else(
+                |_| None,
+                |mut p| {
+                    p.pop();
+                    let p = p.to_str();
+                    p.map(str::to_string)
+                },
+            )
+        })
+        .ok_or_else(|| {
+            format_err!("Failed to determine fallback mountpoint via executable path.")
+        })?;
+
+    let mountpoint = Path::new(&mountpoint);
+    if !mountpoint.exists() {
+        bail!("Medium mountpoint doesn't exist.");
+    }
+
+    let mut statefile = mountpoint.to_path_buf();
+    statefile.push(".mirror-state");
+
+    println!("Loading state from {statefile:?}..");
+    let raw = file_get_contents(&statefile)?;
+    let state: MediumState = serde_json::from_slice(&raw)?;
+    println!(
+        "Last sync timestamp: {}",
+        epoch_to_rfc3339_utc(state.last_sync)?
+    );
+
+    let server_id = proxmox_subscription::get_hardware_address()?;
+    let subscription = state.subscriptions.iter().find(|s| {
+        if let Some(key) = s.key.as_ref() {
+            if let Ok(found_product) = key[..3].parse::<ProductType>() {
+                return product == found_product;
+            }
+        }
+        false
+    });
+
+    match subscription {
+        Some(subscription) => {
+            eprintln!("Setting offline subscription key for {product}..");
+            match set_subscription_key(product, subscription) {
+                Ok(output) if !output.is_empty() => eprintln!("success: {output}"),
+                Ok(_) => eprintln!("success."),
+                Err(err) => eprintln!("error: {err}"),
+            }
+            Ok(())
+        }
+        None => bail!("No matching subscription key found for product '{product}' and server ID '{server_id}'"),
+    }
+}
+
+fn main() {
+    let rpcenv = CliEnvironment::new();
+
+    let cmd_def = CliCommandMap::new()
+        .insert("setup", CliCommand::new(&API_METHOD_SETUP))
+        .insert(
+            "offline-key",
+            CliCommand::new(&API_METHOD_SETUP_OFFLINE_KEY),
+        );
+
+    run_cli_command(
+        cmd_def,
+        rpcenv,
+        Some(|future| proxmox_async::runtime::main(future)),
+    );
+}
index 9ad5cabdbbd3e07c083a1cfd2d3d10f8b889e72c..8de1f33e59252734102af56f4c12feecc13605fd 100644 (file)
@@ -1,7 +1,7 @@
 //! Proxmox mirroring tool for APT repositories.
 //!
 //! This library provides the underlying functionality of the `proxmox-offline-mirror` and
-//! `proxmox-apt-repo` binaries.
+//! `proxmox-offline-mirror-helper` binaries.
 //!
 //! It implements the following features:
 //! - local storage in a hardlink-based pool