public inbox for pbs-devel@lists.proxmox.com
 help / color / mirror / Atom feed
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,
>                       &notification_mode,
> @@ -433,7 +462,7 @@ pub fn restore(
>                       inventory,
>                       media_set_uuid,
>                       drive_config,
> -                    &drive,
> +                    &drives[0],
>                       store_map,
>                       restore_owner,
>                       &notification_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,





  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
Service provided by Proxmox Server Solutions GmbH | Privacy | Legal