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
--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
.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
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
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
+++ /dev/null
-
-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.
--- /dev/null
+
+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
[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"]
+++ /dev/null
-# 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
--- /dev/null
+debian/proxmox-offline-mirror-helper.bc proxmox-offline-mirror-helper
--- /dev/null
+# 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
debian/proxmox-offline-mirror.bc proxmox-offline-mirror
-debian/proxmox-apt-repo.bc proxmox-apt-repo
# 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
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
.. 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
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),
]
``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.
-----------
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.
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``.
+++ /dev/null
-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
+++ /dev/null
-================
-proxmox-apt-repo
-================
-
-Synopsis
-==========
-
-.. include:: synopsis.rst
-
-Description
-============
-
-.. include:: description.rst
-
-
-.. include:: ../pom-copyright.rst
--- /dev/null
+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
--- /dev/null
+=============================
+proxmox-offline-mirror-helper
+=============================
+
+Synopsis
+==========
+
+.. include:: synopsis.rst
+
+Description
+============
+
+.. include:: description.rst
+
+
+.. include:: ../pom-copyright.rst
+++ /dev/null
-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)),
- );
-}
--- /dev/null
+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)),
+ );
+}
//! 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