From: Dominik Csapak <d.csapak@proxmox.com>
To: Jakob-Tobias Winter <tobias@stuttgart.de>, pbs-devel@lists.proxmox.com
Subject: Re: [RFC PATCH proxmox-backup 3/4] close #7333: tape: restore: allow using several drives in parallel
Date: Wed, 11 Nov 2026 14:18:03 +0100 [thread overview]
Message-ID: <1584ed85-8fcb-455e-be64-758e93775c1f@proxmox.com> (raw)
In-Reply-To: <20261106133826.1692620-5-tobias@stuttgart.de>
comments inline
On 11/6/26 2:39 PM, Jakob-Tobias Winter wrote:
> Restoring a media set that spans several tapes from a single drive
> takes long, even if other drives of the library are idle. Add the
> optional 'additional-drives' parameter to the restore API and the
> proxmox-tape CLI, for up to three drives in addition to the given one.
> All drives are checked for the Tape.Read privilege, locked and marked
> as in use for the whole task.
>
> A selective restore then reads the media of its first two phases in
> parallel. A full restore cannot simply do the same: a tape backup
> writes the chunks of a snapshot before its snapshot archive, possibly
> to an earlier media, so with several drives a snapshot could be added
> before all of its chunks are restored, and an aborted restore would
> leave it behind with chunks missing. So with several drives, a full
> restore uses the phases of the selective restore for all snapshots,
> which only add the snapshots once all chunks are restored. This needs
> the media set catalog, the snapshots only show up at the end of the
> restore, and each media is loaded twice, once for the small snapshot
> archives and once for the chunks. With a single drive, a full restore
> works as before.
>
> Signed-off-by: Jakob-Tobias Winter <tobias@stuttgart.de>
> ---
> src/api2/tape/restore.rs | 65 +++++++++++++++++++++++++++++++---------
> src/bin/proxmox-tape.rs | 10 +++++++
> 2 files changed, 61 insertions(+), 14 deletions(-)
>
> diff --git a/src/api2/tape/restore.rs b/src/api2/tape/restore.rs
> index ee8906f85..8040a6c21 100644
> --- a/src/api2/tape/restore.rs
> +++ b/src/api2/tape/restore.rs
> @@ -286,6 +286,16 @@ pub const ROUTER: Router = Router::new().post(&API_METHOD_RESTORE);
> drive: {
> schema: DRIVE_NAME_SCHEMA,
> },
> + "additional-drives": {
> + description: "Use these drives in addition to 'drive', to restore from several \
> + media in parallel.",
> + type: Array,
> + optional: true,
> + max_length: 3,
> + items: {
> + schema: DRIVE_NAME_SCHEMA,
> + },
> + },
as said in my answer to the cover letter, i'd prefer 'drives' as a
replacement for drives instead of an additional parameter.
> "media-set": {
> description: "Media set UUID.",
> type: String,
> @@ -318,8 +328,9 @@ pub const ROUTER: Router = Router::new().post(&API_METHOD_RESTORE);
> access: {
> // Note: parameters are no uri parameter, so we need to test inside function body
> description: "The user needs Tape.Read privilege on /tape/pool/{pool} and \
> - /tape/device/{drive}, Datastore.Backup privilege on /datastore/{store}/[{namespace}], \
> - Datastore.Modify privileges to create namespaces (if they don't exist).",
> + /tape/device/{drive} (and each additional drive), Datastore.Backup privilege on \
> + /datastore/{store}/[{namespace}], Datastore.Modify privileges to create namespaces \
> + (if they don't exist).",
> permission: &Permission::Anybody,
> },
> )]
> @@ -328,6 +339,7 @@ pub const ROUTER: Router = Router::new().post(&API_METHOD_RESTORE);
> pub fn restore(
> store: String,
> drive: String,
> + additional_drives: Option<Vec<String>>,
> namespaces: Option<Vec<String>>,
> media_set: String,
> notify_user: Option<Userid>,
> @@ -374,7 +386,16 @@ pub fn restore(
> }
> }
> }
> - user_info.check_privs(&auth_id, &["tape", "device", &drive], PRIV_TAPE_READ, false)?;
> + let mut drives = vec![drive];
> + for drive in additional_drives.unwrap_or_default() {
> + if drives.contains(&drive) {
> + bail!("drive '{drive}' given more than once");
nit: if we want to enforce that here, i think a hashset is a better way,
since we have quadratic check cost here (shouldn't matter
that much though)
> + }
> + drives.push(drive);
> + }
> + for drive in drives.iter() {
> + user_info.check_privs(&auth_id, &["tape", "device", drive], PRIV_TAPE_READ, false)?;
> + }
>
> let media_set_uuid = media_set.parse()?;
>
> @@ -388,7 +409,10 @@ pub fn restore(
> let (drive_config, _digest) = pbs_config::drive::config()?;
>
> // early check/lock before starting worker
> - let drive_lock = lock_tape_device(&drive_config, &drive)?;
> + let drive_locks = drives
> + .iter()
> + .map(|drive| lock_tape_device(&drive_config, drive))
> + .collect::<Result<Vec<_>, _>>()?;
>
> let to_stdout = rpcenv.env_type() == RpcEnvironmentType::CLI;
>
> @@ -404,23 +428,28 @@ pub fn restore(
> auth_id.to_string(),
> to_stdout,
> move |worker| {
> - let _drive_lock = drive_lock; // keep lock guard
> + let _drive_locks = drive_locks; // keep lock guards
>
> - set_tape_device_state(&drive, &worker.upid().to_string())?;
> + for drive in drives.iter() {
> + set_tape_device_state(drive, &worker.upid().to_string())?;
> + }
>
> let restore_owner = owner.as_ref().unwrap_or(&auth_id);
>
> info!("Mediaset '{media_set}'");
> info!("Pool: {pool}");
>
> - let res = if snapshots.is_some() || namespaces {
> + // the chunks of a snapshot can be on an earlier media that another drive did not
> + // restore yet, so with several drives a full restore uses the phases of the selective
> + // restore, which only add the snapshots after all chunks are restored
> + let res = if snapshots.is_some() || namespaces || drives.len() > 1 {
> restore_list_worker(
> worker,
> - snapshots.unwrap_or_default(),
> + snapshots,
> inventory,
> media_set_uuid,
> drive_config,
> - std::slice::from_ref(&drive),
> + &drives,
> store_map,
> restore_owner,
> ¬ification_mode,
> @@ -433,7 +462,7 @@ pub fn restore(
> inventory,
> media_set_uuid,
> drive_config,
> - &drive,
> + &drives[0],
> store_map,
> restore_owner,
> ¬ification_mode,
> @@ -443,8 +472,10 @@ pub fn restore(
> if res.is_ok() {
> info!("Restore mediaset '{media_set}' done");
> }
> - if let Err(err) = set_tape_device_state(&drive, "") {
> - info!("could not unset drive state for {drive}: {err}");
> + for drive in drives.iter() {
> + if let Err(err) = set_tape_device_state(drive, "") {
> + info!("could not unset drive state for {drive}: {err}");
> + }
> }
>
> res
> @@ -626,7 +657,7 @@ fn log_required_tapes<'a>(inventory: &Inventory, list: impl Iterator<Item = &'a
> #[allow(clippy::too_many_arguments)]
> fn restore_list_worker(
> worker: Arc<WorkerTask>,
> - snapshots: Vec<String>,
> + snapshots: Option<Vec<String>>,
not sure why you change that to an option here?
an empty snapshot list should already be a good enough indicator?
i don't think it makes a good API do differentiate between
no list or an empty list...
> inventory: Inventory,
> media_set_uuid: Uuid,
> drive_config: SectionConfigData,
> @@ -646,11 +677,17 @@ fn restore_list_worker(
>
> let res = proxmox_lang::try_block!({
> // phase 0
> + // without snapshots and namespace mapping, this is a full restore using several drives
> + let restore_all = snapshots.is_none() && store_map.ns_map.is_none();
here we could just do an `is_empty()` check?
> + let snapshots = snapshots.unwrap_or_default();
and this would stay where it was, before calling the function
> let snapshots = if snapshots.is_empty() {
> let mut restorable = Vec::new();
> // restore source namespaces
> for (store, snapshot) in catalog.list_snapshots() {
> let (ns, dir) = match parse_ns_and_snapshot(snapshot) {
> + Ok((ns, dir)) if restore_all && store_map.target_store(store).is_some() => {
> + (ns, dir)
> + }
> Ok((ns, dir)) if store_map.has_full_mapping(store, &ns) => (ns, dir),
> Err(err) => {
> warn!("couldn't parse snapshot {snapshot} - {err}");
> @@ -665,7 +702,7 @@ fn restore_list_worker(
> &snapshot,
> &ns,
> &dir,
> - false,
> + restore_all,
> &user_info,
> auth_id,
> restore_owner,
> diff --git a/src/bin/proxmox-tape.rs b/src/bin/proxmox-tape.rs
> index dcbf2d0fb..860ed38fc 100644
> --- a/src/bin/proxmox-tape.rs
> +++ b/src/bin/proxmox-tape.rs
> @@ -906,6 +906,16 @@ async fn backup(mut param: Value) -> Result<(), Error> {
> schema: DRIVE_NAME_SCHEMA,
> optional: true,
> },
> + "additional-drives": {
> + description: "Use these drives in addition to 'drive', to restore from several \
> + media in parallel.",
> + type: Array,
> + optional: true,
> + max_length: 3,
> + items: {
> + schema: DRIVE_NAME_SCHEMA,
> + },
> + },
> "media-set": {
> description: "Media set UUID.",
> type: String,
next prev parent reply other threads:[~2026-11-11 13:18 UTC|newest]
Thread overview: 12+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-11-06 13:38 [RFC PATCH proxmox-backup 0/4] fix 7333 use multiple tape drives for restoring Jakob-Tobias Winter
2026-11-06 13:38 ` [RFC PATCH proxmox-backup 1/4] tape: drive: add a pool to use several drives for one task Jakob-Tobias Winter
2026-11-10 15:05 ` Dominik Csapak
2026-11-06 13:38 ` [RFC PATCH proxmox-backup 2/4] tape: restore: use a drive pool for the selective restore Jakob-Tobias Winter
2026-11-11 12:59 ` Dominik Csapak
2026-11-06 13:38 ` [RFC PATCH proxmox-backup 3/4] close #7333: tape: restore: allow using several drives in parallel Jakob-Tobias Winter
2026-11-11 13:18 ` Dominik Csapak [this message]
2026-11-06 13:38 ` [RFC PATCH proxmox-backup 4/4] ui: tape restore: add selector for additional drives Jakob-Tobias Winter
2026-11-11 13:19 ` Dominik Csapak
2026-11-10 13:15 ` [RFC PATCH proxmox-backup 0/4] fix 7333 use multiple tape drives for restoring Dominik Csapak
2026-11-10 15:43 ` Jakob-Tobias Winter
2026-11-11 13:38 ` Dominik Csapak
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=1584ed85-8fcb-455e-be64-758e93775c1f@proxmox.com \
--to=d.csapak@proxmox.com \
--cc=pbs-devel@lists.proxmox.com \
--cc=tobias@stuttgart.de \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox