[pbs-devel] [RFC proxmox 1/3] new proxmox-server-config crate

Dominik Csapak d.csapak at proxmox.com
Wed Oct 18 12:39:08 CEST 2023


aims to provide global server config that can be initialized from the
main daemon entry point, and used in other crates without passing around
the individual directories and the user

uses `OnceLock` with `static` to provide a global reference to the
finished instance.

general use is intended like this:

  ServerConfig::new("daemon-name", "/some/base/dir", some_user)?
    .with_task_dir("/some/other/dir")
    ...
    .setup()?;

and then use it in other places like this:

  let task_dir = get_server_config()?.task_dir();

Signed-off-by: Dominik Csapak <d.csapak at proxmox.com>
---
 Cargo.toml                                 |   1 +
 proxmox-server-config/Cargo.toml           |  16 ++
 proxmox-server-config/debian/changelog     |   5 +
 proxmox-server-config/debian/control       |  37 +++
 proxmox-server-config/debian/copyright     |  18 ++
 proxmox-server-config/debian/debcargo.toml |   7 +
 proxmox-server-config/src/lib.rs           | 302 +++++++++++++++++++++
 7 files changed, 386 insertions(+)
 create mode 100644 proxmox-server-config/Cargo.toml
 create mode 100644 proxmox-server-config/debian/changelog
 create mode 100644 proxmox-server-config/debian/control
 create mode 100644 proxmox-server-config/debian/copyright
 create mode 100644 proxmox-server-config/debian/debcargo.toml
 create mode 100644 proxmox-server-config/src/lib.rs

diff --git a/Cargo.toml b/Cargo.toml
index f8bc181..6b22c58 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -103,6 +103,7 @@ proxmox-router = { version = "2.1.1", path = "proxmox-router" }
 proxmox-schema = { version = "2.0.0", path = "proxmox-schema" }
 proxmox-section-config = { version = "2.0.0", path = "proxmox-section-config" }
 proxmox-serde = { version = "0.1.1", path = "proxmox-serde", features = [ "serde_json" ] }
+proxmox-server-config = { version = "0.1", path = "proxmox-server-config" }
 proxmox-sortable-macro = { version = "0.1.3", path = "proxmox-sortable-macro" }
 proxmox-sys = { version = "0.5.0", path = "proxmox-sys" }
 proxmox-tfa = { version = "4.0.4", path = "proxmox-tfa" }
diff --git a/proxmox-server-config/Cargo.toml b/proxmox-server-config/Cargo.toml
new file mode 100644
index 0000000..f0f4de2
--- /dev/null
+++ b/proxmox-server-config/Cargo.toml
@@ -0,0 +1,16 @@
+[package]
+name = "proxmox-server-config"
+version = "0.1.0"
+authors.workspace = true
+edition.workspace = true
+license.workspace = true
+repository.workspace = true
+description = "Generic Server Config crate"
+
+exclude.workspace = true
+
+[dependencies]
+anyhow.workspace = true
+nix.workspace = true
+
+proxmox-sys.workspace = true
diff --git a/proxmox-server-config/debian/changelog b/proxmox-server-config/debian/changelog
new file mode 100644
index 0000000..4c21324
--- /dev/null
+++ b/proxmox-server-config/debian/changelog
@@ -0,0 +1,5 @@
+rust-proxmox-server-config (0.1.0-1) stable; urgency=medium
+
+  * initial version
+
+ -- Proxmox Support Team <support at proxmox.com>  Tue, 03 Oct 2023 10:56:15 +0200
diff --git a/proxmox-server-config/debian/control b/proxmox-server-config/debian/control
new file mode 100644
index 0000000..48a3ff3
--- /dev/null
+++ b/proxmox-server-config/debian/control
@@ -0,0 +1,37 @@
+Source: rust-proxmox-server-config
+Section: rust
+Priority: optional
+Build-Depends: debhelper (>= 12),
+ dh-cargo (>= 25),
+ cargo:native <!nocheck>,
+ rustc:native <!nocheck>,
+ libstd-rust-dev <!nocheck>,
+ librust-anyhow-1+default-dev <!nocheck>,
+ librust-nix-0.26+default-dev (>= 0.26.1-~~) <!nocheck>,
+ librust-proxmox-sys-0.5+default-dev <!nocheck>
+Maintainer: Proxmox Support Team <support at proxmox.com>
+Standards-Version: 4.6.1
+Vcs-Git: git://git.proxmox.com/git/proxmox.git
+Vcs-Browser: https://git.proxmox.com/?p=proxmox.git
+X-Cargo-Crate: proxmox-server-config
+Rules-Requires-Root: no
+
+Package: librust-proxmox-server-config-dev
+Architecture: any
+Multi-Arch: same
+Depends:
+ ${misc:Depends},
+ librust-anyhow-1+default-dev,
+ librust-nix-0.26+default-dev (>= 0.26.1-~~),
+ librust-proxmox-sys-0.5+default-dev
+Provides:
+ librust-proxmox-server-config+default-dev (= ${binary:Version}),
+ librust-proxmox-server-config-0-dev (= ${binary:Version}),
+ librust-proxmox-server-config-0+default-dev (= ${binary:Version}),
+ librust-proxmox-server-config-0.1-dev (= ${binary:Version}),
+ librust-proxmox-server-config-0.1+default-dev (= ${binary:Version}),
+ librust-proxmox-server-config-0.1.0-dev (= ${binary:Version}),
+ librust-proxmox-server-config-0.1.0+default-dev (= ${binary:Version})
+Description: Generic Server Config crate - Rust source code
+ This package contains the source for the Rust proxmox-server-config crate,
+ packaged by debcargo for use with cargo and dh-cargo.
diff --git a/proxmox-server-config/debian/copyright b/proxmox-server-config/debian/copyright
new file mode 100644
index 0000000..0d9eab3
--- /dev/null
+++ b/proxmox-server-config/debian/copyright
@@ -0,0 +1,18 @@
+Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
+
+Files:
+ *
+Copyright: 2019 - 2023 Proxmox Server Solutions GmbH <support at proxmox.com>
+License: AGPL-3.0-or-later
+ This program is free software: you can redistribute it and/or modify it under
+ the terms of the GNU Affero General Public License as published by the Free
+ Software Foundation, either version 3 of the License, or (at your option) any
+ later version.
+ .
+ This program is distributed in the hope that it will be useful, but WITHOUT
+ ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+ FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
+ details.
+ .
+ You should have received a copy of the GNU Affero General Public License along
+ with this program. If not, see <https://www.gnu.org/licenses/>.
diff --git a/proxmox-server-config/debian/debcargo.toml b/proxmox-server-config/debian/debcargo.toml
new file mode 100644
index 0000000..b7864cd
--- /dev/null
+++ b/proxmox-server-config/debian/debcargo.toml
@@ -0,0 +1,7 @@
+overlay = "."
+crate_src_path = ".."
+maintainer = "Proxmox Support Team <support at proxmox.com>"
+
+[source]
+vcs_git = "git://git.proxmox.com/git/proxmox.git"
+vcs_browser = "https://git.proxmox.com/?p=proxmox.git"
diff --git a/proxmox-server-config/src/lib.rs b/proxmox-server-config/src/lib.rs
new file mode 100644
index 0000000..8377978
--- /dev/null
+++ b/proxmox-server-config/src/lib.rs
@@ -0,0 +1,302 @@
+//! A generic server config abstraction
+//!
+//! Used for proxmox daemons to have a central point for configuring things
+//! like base/log/task/certificate directories, user for creating files, etc.
+
+use std::os::linux::fs::MetadataExt;
+use std::path::{Path, PathBuf};
+use std::sync::OnceLock;
+
+use anyhow::{format_err, Context, Error};
+use nix::unistd::User;
+
+use proxmox_sys::fs::{create_path, CreateOptions};
+
+static SERVER_CONFIG: OnceLock<ServerConfig> = OnceLock::new();
+
+/// Get the global [`ServerConfig`] instance.
+///
+/// Before using this, you must create a new [`ServerConfig`] and call
+/// [`setup`](ServerConfig::setup) on it.
+///
+/// Returns an [`Error`](anyhow::Error) when no server config was yet initialized.
+pub fn get_server_config() -> Result<&'static ServerConfig, Error> {
+    SERVER_CONFIG
+        .get()
+        .ok_or_else(|| format_err!("not server config initialized"))
+}
+
+/// A Server configuration object.
+///
+/// contains the name, user and used directories of a server, like the log directory or
+/// the state directory.
+///
+/// Is created by calling [`new`](Self::new) and can be set as a global object
+/// with [`setup`](Self::setup)
+///
+/// # Example
+///
+/// On server initialize run something like this:
+/// ```
+/// use proxmox_server_config::ServerConfig;
+///
+/// # fn function() -> Result<(), anyhow::Error> {
+/// # let some_user = nix::unistd::User::from_uid(nix::unistd::ROOT).unwrap().unwrap();
+/// # let privileged_user = nix::unistd::User::from_uid(nix::unistd::ROOT).unwrap().unwrap();
+/// ServerConfig::new("name-of-daemon", "/some/base/dir", some_user)?
+///     .with_privileged_user(privileged_user)?
+///     .with_task_dir("/var/log/tasks")?
+///     .with_config_dir("/etc/some-dir")?
+///     // ...
+///     .setup()?;
+/// # Ok(())
+/// # }
+/// ```
+///
+/// Then you can use it in other parts of your daemon:
+///
+/// ```
+/// use proxmox_server_config:: get_server_config;
+///
+/// # fn function() -> Result<(), anyhow::Error> {
+/// let task_dir = get_server_config()?.task_dir();
+/// let user = get_server_config()?.user();
+/// // ...and so on
+/// # Ok(())
+/// # }
+/// ```
+pub struct ServerConfig {
+    name: String,
+    base_dir: PathBuf,
+    user: User,
+    privileged_user: OnceLock<User>,
+
+    task_dir: OnceLock<PathBuf>,
+    log_dir: OnceLock<PathBuf>,
+    cert_dir: OnceLock<PathBuf>,
+    state_dir: OnceLock<PathBuf>,
+    run_dir: OnceLock<PathBuf>,
+    config_dir: OnceLock<PathBuf>,
+}
+
+fn check_dir_permissions<M: MetadataExt>(metadata: M, user: &User) -> bool {
+    let mode = metadata.st_mode();
+    let user_write = metadata.st_uid() == user.uid.as_raw() && mode & 0o200 > 0;
+    let group_write = metadata.st_gid() == user.gid.as_raw() && mode & 0o020 > 0;
+    let other_write = mode & 0o002 > 0;
+
+    user_write || group_write || other_write
+}
+
+impl ServerConfig {
+    /// Creates a new instance of [`ServerConfig`], with sensible defaults derived from the given
+    /// `name` and `base_dir`. Permissions are checked against the given `user`.
+    pub fn new<P: AsRef<Path>>(name: &str, base_dir: P, user: User) -> std::io::Result<Self> {
+        let base_dir = base_dir.as_ref();
+
+        let metadata = std::fs::metadata(base_dir)?;
+        if !metadata.is_dir() {
+            return Err(std::io::Error::new(
+                std::io::ErrorKind::NotFound,
+                "base directory does not exists or is not a directory",
+            ));
+        }
+        if !check_dir_permissions(metadata, &user) {
+            return Err(std::io::Error::new(
+                std::io::ErrorKind::PermissionDenied,
+                format!(
+                    "user '{}' does not have enough permissions on base directory",
+                    user.name
+                ),
+            ));
+        }
+
+        Ok(Self {
+            name: name.to_string(),
+            base_dir: base_dir.to_owned(),
+            user,
+            privileged_user: OnceLock::new(),
+            task_dir: OnceLock::new(),
+            log_dir: OnceLock::new(),
+            cert_dir: OnceLock::new(),
+            state_dir: OnceLock::new(),
+            run_dir: OnceLock::new(),
+            config_dir: OnceLock::new(),
+        })
+    }
+
+    /// Finishes the server config setup by creating the dirs and creating a global
+    /// reference to it with [`OnceLock`]
+    ///
+    /// Creates the configured directories. If no error is returned, all directories are created
+    /// and writable with the configured user. After this, you can get a reference to the
+    /// config with [`get_server_config`].
+    pub fn setup(self) -> Result<(), Error> {
+        self.create_dirs()?;
+
+        SERVER_CONFIG
+            .set(self)
+            .map_err(|_| format_err!("Server config already set"))?;
+
+        Ok(())
+    }
+
+    fn create_dirs(&self) -> Result<(), Error> {
+        let opts = CreateOptions::new()
+            .owner(self.user.uid)
+            .group(self.user.gid);
+
+        create_path(&self.base_dir, Some(opts.clone()), Some(opts.clone()))
+            .context("could not create base directory")?;
+
+        create_path(self.task_dir(), Some(opts.clone()), Some(opts.clone()))
+            .context("could not create task directory")?;
+
+        create_path(self.log_dir(), Some(opts.clone()), Some(opts.clone()))
+            .context("could not create log directory")?;
+
+        create_path(self.cert_dir(), Some(opts.clone()), Some(opts.clone()))
+            .context("could not create certificate directory")?;
+
+        create_path(self.state_dir(), Some(opts.clone()), Some(opts.clone()))
+            .context("could not create state directory")?;
+
+        create_path(self.run_dir(), Some(opts.clone()), Some(opts))
+            .context("could not create run directory")?;
+
+        Ok(())
+    }
+
+    /// Returns the configured user
+    pub fn user(&self) -> &User {
+        &self.user
+    }
+
+    /// Returns the configured privileged user. Defaults to the regular configured user.
+    pub fn privileged_user(&self) -> &User {
+        self.privileged_user.get_or_init(|| self.user.clone())
+    }
+
+    /// Set the privileged user
+    pub fn set_privileged_user(&mut self, user: User) -> Result<(), Error> {
+        self.privileged_user
+            .set(user)
+            .map_err(|_| format_err!("already set privileged user"))
+    }
+
+    /// Builder style method to set the privileged user
+    pub fn with_privileged_user(mut self, user: User) -> Result<Self, Error> {
+        self.set_privileged_user(user)?;
+        Ok(self)
+    }
+
+    /// Set the task directory.
+    pub fn set_task_dir<P: AsRef<Path>>(&mut self, path: P) -> Result<(), Error> {
+        self.task_dir
+            .set(path.as_ref().to_owned())
+            .map_err(|_| format_err!("already set task dir"))
+    }
+
+    /// Builder style method to set the task directory.
+    pub fn with_task_dir<P: AsRef<Path>>(mut self, path: P) -> Result<Self, Error> {
+        self.set_task_dir(path)?;
+        Ok(self)
+    }
+
+    /// Get the task directory
+    pub fn task_dir(&self) -> &Path {
+        self.task_dir.get_or_init(|| self.base_dir.join("tasks"))
+    }
+
+    /// Set the log directory.
+    pub fn set_log_dir<P: AsRef<Path>>(&mut self, path: P) -> Result<(), Error> {
+        self.log_dir
+            .set(path.as_ref().to_owned())
+            .map_err(|_| format_err!("already set log dir"))
+    }
+
+    /// Builder style method to set the log directory.
+    pub fn with_log_dir<P: AsRef<Path>>(mut self, path: P) -> Result<Self, Error> {
+        self.set_log_dir(path)?;
+        Ok(self)
+    }
+
+    /// Get the log directory
+    pub fn log_dir(&self) -> &Path {
+        self.log_dir.get_or_init(|| self.base_dir.join("logs"))
+    }
+
+    /// Set the cert directory.
+    pub fn set_cert_dir<P: AsRef<Path>>(&mut self, path: P) -> Result<(), Error> {
+        self.cert_dir
+            .set(path.as_ref().to_owned())
+            .map_err(|_| format_err!("already set cert dir"))
+    }
+
+    /// Builder style method to set the cert directory.
+    pub fn with_cert_dir<P: AsRef<Path>>(mut self, path: P) -> Result<Self, Error> {
+        self.set_cert_dir(path)?;
+        Ok(self)
+    }
+
+    /// Get the cert directory
+    pub fn cert_dir(&self) -> &Path {
+        self.cert_dir
+            .get_or_init(|| self.base_dir.join("certificates"))
+    }
+
+    /// Set the state directory.
+    pub fn set_state_dir<P: AsRef<Path>>(&mut self, path: P) -> Result<(), Error> {
+        self.state_dir
+            .set(path.as_ref().to_owned())
+            .map_err(|_| format_err!("already set state dir"))
+    }
+
+    /// Builder style method to set the state directory.
+    pub fn with_state_dir<P: AsRef<Path>>(mut self, path: P) -> Result<Self, Error> {
+        self.set_state_dir(path)?;
+        Ok(self)
+    }
+
+    /// Get the state directory
+    pub fn state_dir(&self) -> &Path {
+        self.state_dir.get_or_init(|| self.base_dir.join("states"))
+    }
+
+    /// Set the run directory.
+    pub fn set_run_dir<P: AsRef<Path>>(&mut self, path: P) -> Result<(), Error> {
+        self.run_dir
+            .set(path.as_ref().to_owned())
+            .map_err(|_| format_err!("already set run dir"))
+    }
+
+    /// Builder style method to set the run directory.
+    pub fn with_run_dir<P: AsRef<Path>>(mut self, path: P) -> Result<Self, Error> {
+        self.set_run_dir(path)?;
+        Ok(self)
+    }
+
+    /// Get the run directory
+    pub fn run_dir(&self) -> &Path {
+        self.run_dir
+            .get_or_init(|| PathBuf::from(format!("/run/{}", self.name)))
+    }
+
+    /// Set the config directory.
+    pub fn set_config_dir<P: AsRef<Path>>(&mut self, path: P) -> Result<(), Error> {
+        self.config_dir
+            .set(path.as_ref().to_owned())
+            .map_err(|_| format_err!("already set config dir"))
+    }
+
+    /// Builder style method to set the config directory.
+    pub fn with_config_dir<P: AsRef<Path>>(mut self, path: P) -> Result<Self, Error> {
+        self.set_config_dir(path)?;
+        Ok(self)
+    }
+
+    /// Get the config directory
+    pub fn config_dir(&self) -> &Path {
+        self.config_dir.get_or_init(|| self.base_dir.join("config"))
+    }
+}
-- 
2.30.2






More information about the pbs-devel mailing list