public inbox for pbs-devel@lists.proxmox.com
 help / color / mirror / Atom feed
From: Christian Ebner <c.ebner@proxmox.com>
To: pbs-devel@lists.proxmox.com
Subject: [PATCH proxmox-backup v2 03/10] api/datastore: refactor datastore creation helper logic
Date: Wed,  6 May 2026 18:56:44 +0200	[thread overview]
Message-ID: <20260506165651.1322947-4-c.ebner@proxmox.com> (raw)
In-Reply-To: <20260506165651.1322947-1-c.ebner@proxmox.com>

Move most of the logic from the datastore creation worker task helper
to the datastore implementation itself.

This has the advantage of better encapsulation, reducing the
interdependencies.

Signed-off-by: Christian Ebner <c.ebner@proxmox.com>
---
 pbs-datastore/src/datastore.rs | 131 ++++++++++++++++++++++++++++++++-
 src/api2/config/datastore.rs   | 123 ++-----------------------------
 2 files changed, 136 insertions(+), 118 deletions(-)

diff --git a/pbs-datastore/src/datastore.rs b/pbs-datastore/src/datastore.rs
index 34efcd398..84314cd29 100644
--- a/pbs-datastore/src/datastore.rs
+++ b/pbs-datastore/src/datastore.rs
@@ -19,7 +19,7 @@ use tracing::{info, warn};
 use proxmox_human_byte::HumanByte;
 use proxmox_s3_client::{
     RequestCounterThresholds, S3Client, S3ClientConf, S3ClientOptions, S3ObjectKey, S3PathPrefix,
-    S3RateLimiterOptions, S3RequestCounterConfig, SharedRequestCounters,
+    S3RateLimiterOptions, S3RequestCounterConfig, SharedRequestCounters, S3_HTTP_REQUEST_TIMEOUT,
 };
 use proxmox_schema::ApiType;
 
@@ -388,6 +388,17 @@ impl DatastoreThreadSettings {
     }
 }
 
+#[derive(Default, serde::Deserialize, serde::Serialize)]
+#[serde(rename_all = "kebab-case")]
+/// S3 in-use marker content
+///
+/// Allows to de-/serialize the contents of the in-use object contents stored on S3 backends to
+/// protect against access by multiple PBS instances.
+struct InUseContent {
+    #[serde(skip_serializing_if = "Option::is_none")]
+    hostname: Option<String>,
+}
+
 /// Returns the parsed datastore config (`datastore.cfg`) and its
 /// generation.
 ///
@@ -464,6 +475,124 @@ impl DataStore {
         })
     }
 
+    /// Create or reuse the chunk store from given datastore configuration
+    pub fn construct_chunk_store(
+        store_config: &mut DataStoreConfig,
+        tuning: &DatastoreTuning,
+        reuse_existing: bool,
+        overwrite_in_use: bool,
+    ) -> Result<(), Error> {
+        let (backend_type, backend_s3_client) =
+            match DataStore::s3_client_and_backend_from_datastore_config(store_config)? {
+                (backend_type, Some(s3_client)) => {
+                    if !overwrite_in_use {
+                        let object_key = S3ObjectKey::try_from(S3_DATASTORE_IN_USE_MARKER)
+                            .context("failed to generate s3 object key")?;
+                        if let Some(response) = proxmox_async::runtime::block_on(
+                            s3_client.get_object(object_key.clone()),
+                        )
+                        .context("failed to get in-use marker from bucket")?
+                        {
+                            let content =
+                                proxmox_async::runtime::block_on(response.content.collect())
+                                    .unwrap_or_default();
+                            let content =
+                                String::from_utf8(content.to_bytes().to_vec()).unwrap_or_default();
+                            let in_use: InUseContent =
+                                serde_json::from_str(&content).unwrap_or_default();
+                            if let Some(hostname) = in_use.hostname {
+                                bail!(
+                                    "Bucket already contains datastore in use by host {hostname}"
+                                );
+                            } else {
+                                bail!("Bucket already contains datastore in use");
+                            }
+                        }
+                    }
+
+                    (backend_type, Some(Arc::new(s3_client)))
+                }
+                (backend_type, None) => (backend_type, None),
+            };
+
+        let path: PathBuf = store_config.absolute_path().into();
+
+        let chunk_store = if reuse_existing && backend_type == DatastoreBackendType::Filesystem {
+            ChunkStore::verify_chunkstore(&path).and_then(|_| {
+                // Must be the only instance accessing and locking the chunk store,
+                // dropping will close all other locks from this process on the lockfile as well.
+                ChunkStore::open(
+                    &store_config.name,
+                    &path,
+                    tuning.sync_level.unwrap_or_default(),
+                )
+            })?
+        } else {
+            if !reuse_existing && backend_type == DatastoreBackendType::Filesystem {
+                if let Ok(dir) = std::fs::read_dir(&path) {
+                    for file in dir {
+                        let name = file?.file_name();
+                        let name = name.to_str();
+                        if !name.is_some_and(|name| name.starts_with('.') || name == "lost+found") {
+                            bail!("datastore path not empty");
+                        }
+                    }
+                }
+            }
+            if reuse_existing && backend_type == DatastoreBackendType::S3 {
+                let chunks_path = path.join(".chunks");
+                if let Err(err) = std::fs::remove_dir_all(&chunks_path) {
+                    if err.kind() != std::io::ErrorKind::NotFound {
+                        return Err(err).with_context(|| {
+                            format!("failed to remove pre-existing chunks in {chunks_path:?}")
+                        });
+                    }
+                }
+                // starting out in maintenance mode s3-refresh,
+                // so no other operation will start until done with that.
+                store_config.set_maintenance_mode(Some(MaintenanceMode {
+                    ty: MaintenanceType::S3Refresh,
+                    message: None,
+                }))?;
+            }
+            let backup_user = pbs_config::backup_user()?;
+            ChunkStore::create(
+                &store_config.name,
+                path.clone(),
+                backup_user.uid,
+                backup_user.gid,
+                tuning.sync_level.unwrap_or_default(),
+            )?
+        };
+
+        if let Some(ref s3_client) = backend_s3_client {
+            let object_key = S3ObjectKey::try_from(S3_DATASTORE_IN_USE_MARKER)
+                .context("failed to generate s3 object key")?;
+            let content = serde_json::to_string(&InUseContent {
+                hostname: Some(proxmox_sys::nodename().to_string()),
+            })
+            .context("failed to encode hostname")?;
+            proxmox_async::runtime::block_on(s3_client.put_object(
+                object_key,
+                hyper::body::Bytes::from(content).into(),
+                Some(S3_HTTP_REQUEST_TIMEOUT),
+                true,
+            ))
+            .context("failed to upload in-use marker for datastore")?;
+        }
+
+        if tuning.gc_atime_safety_check.unwrap_or(true) {
+            chunk_store
+                .check_fs_atime_updates(true, backend_s3_client)
+                .context("access time safety check failed")?;
+            info!("Access time update check successful.");
+        } else {
+            info!("Access time update check skipped.");
+        }
+
+        Ok(())
+    }
+
     /// Get the backend for this datastore based on it's configuration
     pub fn backend(&self) -> Result<DatastoreBackend, Error> {
         let backend = match self.backend_type() {
diff --git a/src/api2/config/datastore.rs b/src/api2/config/datastore.rs
index 7c3531a4b..70e349b2f 100644
--- a/src/api2/config/datastore.rs
+++ b/src/api2/config/datastore.rs
@@ -1,11 +1,9 @@
 use std::path::{Path, PathBuf};
-use std::sync::Arc;
 
 use ::serde::{Deserialize, Serialize};
 use anyhow::{bail, format_err, Context, Error};
-use http_body_util::BodyExt;
 use serde_json::Value;
-use tracing::{info, warn};
+use tracing::warn;
 
 use proxmox_router::{http_bail, Permission, Router, RpcEnvironment, RpcEnvironmentType};
 use proxmox_schema::{api, param_bail, ApiType};
@@ -14,12 +12,11 @@ use proxmox_uuid::Uuid;
 
 use pbs_api_types::{
     Authid, DataStoreConfig, DataStoreConfigUpdater, DatastoreBackendType, DatastoreNotify,
-    DatastoreTuning, KeepOptions, MaintenanceMode, MaintenanceType, PruneJobConfig,
-    PruneJobOptions, DATASTORE_SCHEMA, PRIV_DATASTORE_ALLOCATE, PRIV_DATASTORE_AUDIT,
-    PRIV_DATASTORE_MODIFY, PRIV_SYS_MODIFY, PROXMOX_CONFIG_DIGEST_SCHEMA, UPID_SCHEMA,
+    DatastoreTuning, KeepOptions, MaintenanceMode, PruneJobConfig, PruneJobOptions,
+    DATASTORE_SCHEMA, PRIV_DATASTORE_ALLOCATE, PRIV_DATASTORE_AUDIT, PRIV_DATASTORE_MODIFY,
+    PRIV_SYS_MODIFY, PROXMOX_CONFIG_DIGEST_SCHEMA, UPID_SCHEMA,
 };
 use pbs_config::BackupLockGuard;
-use pbs_datastore::chunk_store::ChunkStore;
 
 use crate::api2::admin::datastore::do_mount_device;
 use crate::api2::admin::prune::list_prune_jobs;
@@ -31,20 +28,12 @@ use crate::api2::config::tape_backup_job::{delete_tape_backup_job, list_tape_bac
 use crate::api2::config::verify::delete_verification_job;
 use pbs_config::CachedUserInfo;
 
-use pbs_datastore::{get_datastore_mount_status, DataStore, S3_DATASTORE_IN_USE_MARKER};
+use pbs_datastore::{get_datastore_mount_status, DataStore};
 use proxmox_rest_server::WorkerTask;
-use proxmox_s3_client::{S3ObjectKey, S3_HTTP_REQUEST_TIMEOUT};
 
 use crate::server::jobstate;
 use crate::tools::disks::unmount_by_mountpoint;
 
-#[derive(Default, serde::Deserialize, serde::Serialize)]
-#[serde(rename_all = "kebab-case")]
-struct InUseContent {
-    #[serde(skip_serializing_if = "Option::is_none")]
-    hostname: Option<String>,
-}
-
 #[api(
     input: {
         properties: {},
@@ -133,107 +122,7 @@ pub(crate) fn do_create_datastore(
         UnmountGuard::new(None)
     };
 
-    let (backend_type, backend_s3_client) =
-        match DataStore::s3_client_and_backend_from_datastore_config(&datastore)? {
-            (backend_type, Some(s3_client)) => {
-                if !overwrite_in_use {
-                    let object_key = S3ObjectKey::try_from(S3_DATASTORE_IN_USE_MARKER)
-                        .context("failed to generate s3 object key")?;
-                    if let Some(response) =
-                        proxmox_async::runtime::block_on(s3_client.get_object(object_key.clone()))
-                            .context("failed to get in-use marker from bucket")?
-                    {
-                        let content = proxmox_async::runtime::block_on(response.content.collect())
-                            .unwrap_or_default();
-                        let content =
-                            String::from_utf8(content.to_bytes().to_vec()).unwrap_or_default();
-                        let in_use: InUseContent =
-                            serde_json::from_str(&content).unwrap_or_default();
-                        if let Some(hostname) = in_use.hostname {
-                            bail!("Bucket already contains datastore in use by host {hostname}");
-                        } else {
-                            bail!("Bucket already contains datastore in use");
-                        }
-                    }
-                }
-
-                (backend_type, Some(Arc::new(s3_client)))
-            }
-            (backend_type, None) => (backend_type, None),
-        };
-
-    let chunk_store = if reuse_datastore && backend_type == DatastoreBackendType::Filesystem {
-        ChunkStore::verify_chunkstore(&path).and_then(|_| {
-            // Must be the only instance accessing and locking the chunk store,
-            // dropping will close all other locks from this process on the lockfile as well.
-            ChunkStore::open(
-                &datastore.name,
-                &path,
-                tuning.sync_level.unwrap_or_default(),
-            )
-        })?
-    } else {
-        if !reuse_datastore && backend_type == DatastoreBackendType::Filesystem {
-            if let Ok(dir) = std::fs::read_dir(&path) {
-                for file in dir {
-                    let name = file?.file_name();
-                    let name = name.to_str();
-                    if !name.is_some_and(|name| name.starts_with('.') || name == "lost+found") {
-                        bail!("datastore path not empty");
-                    }
-                }
-            }
-        }
-        if reuse_datastore && backend_type == DatastoreBackendType::S3 {
-            let chunks_path = path.join(".chunks");
-            if let Err(err) = std::fs::remove_dir_all(&chunks_path) {
-                if err.kind() != std::io::ErrorKind::NotFound {
-                    return Err(err).with_context(|| {
-                        format!("failed to remove pre-existing chunks in {chunks_path:?}")
-                    });
-                }
-            }
-            // starting out in maintenance mode s3-refresh,
-            // so no other operation will start until done with that.
-            datastore.set_maintenance_mode(Some(MaintenanceMode {
-                ty: MaintenanceType::S3Refresh,
-                message: None,
-            }))?;
-        }
-        let backup_user = pbs_config::backup_user()?;
-        ChunkStore::create(
-            &datastore.name,
-            path.clone(),
-            backup_user.uid,
-            backup_user.gid,
-            tuning.sync_level.unwrap_or_default(),
-        )?
-    };
-
-    if let Some(ref s3_client) = backend_s3_client {
-        let object_key = S3ObjectKey::try_from(S3_DATASTORE_IN_USE_MARKER)
-            .context("failed to generate s3 object key")?;
-        let content = serde_json::to_string(&InUseContent {
-            hostname: Some(proxmox_sys::nodename().to_string()),
-        })
-        .context("failed to encode hostname")?;
-        proxmox_async::runtime::block_on(s3_client.put_object(
-            object_key,
-            hyper::body::Bytes::from(content).into(),
-            Some(S3_HTTP_REQUEST_TIMEOUT),
-            true,
-        ))
-        .context("failed to upload in-use marker for datastore")?;
-    }
-
-    if tuning.gc_atime_safety_check.unwrap_or(true) {
-        chunk_store
-            .check_fs_atime_updates(true, backend_s3_client)
-            .context("access time safety check failed")?;
-        info!("Access time update check successful.");
-    } else {
-        info!("Access time update check skipped.");
-    }
+    DataStore::construct_chunk_store(&mut datastore, &tuning, reuse_datastore, overwrite_in_use)?;
 
     config.set_data(&datastore.name, "datastore", &datastore)?;
 
-- 
2.47.3





  parent reply	other threads:[~2026-05-06 16:57 UTC|newest]

Thread overview: 11+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-05-06 16:56 [PATCH proxmox{,-backup} v2 00/10] keep datastore config unlock during long running operations Christian Ebner
2026-05-06 16:56 ` [PATCH proxmox v2 01/10] pbs-api-types: add datastore create maintenance-mode type Christian Ebner
2026-05-06 16:56 ` [PATCH proxmox-backup v2 02/10] api: config: rearrange independent code block for datastore creation Christian Ebner
2026-05-06 16:56 ` Christian Ebner [this message]
2026-05-06 16:56 ` [PATCH proxmox-backup v2 04/10] datastore: restrict chunk store scope to pbs-datastore crate Christian Ebner
2026-05-06 16:56 ` [PATCH proxmox-backup v2 05/10] datastore: move lock files base path constant to central location Christian Ebner
2026-05-06 16:56 ` [PATCH proxmox-backup v2 06/10] datastore: move file lock helper to centralized place Christian Ebner
2026-05-06 16:56 ` [PATCH proxmox-backup v2 07/10] datastore: create lockdir with correct mode for backup user access Christian Ebner
2026-05-06 16:56 ` [PATCH proxmox-backup v2 08/10] api/datastore: use maintenance-mode lock to protect against changes Christian Ebner
2026-05-06 16:56 ` [PATCH proxmox-backup v2 09/10] api: config: unlocked s3 bucket access check for datastore creation Christian Ebner
2026-05-06 16:56 ` [PATCH proxmox-backup v2 10/10] datastore: protect datastore creation by maintenance-mode Christian Ebner

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=20260506165651.1322947-4-c.ebner@proxmox.com \
    --to=c.ebner@proxmox.com \
    --cc=pbs-devel@lists.proxmox.com \
    /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