From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from firstgate.proxmox.com (firstgate.proxmox.com [212.224.123.68]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature RSA-PSS (2048 bits)) (No client certificate requested) by lists.proxmox.com (Postfix) with ESMTPS id EA7799B6C3 for ; Wed, 18 Oct 2023 12:39:45 +0200 (CEST) Received: from firstgate.proxmox.com (localhost [127.0.0.1]) by firstgate.proxmox.com (Proxmox) with ESMTP id CB73FD484 for ; Wed, 18 Oct 2023 12:39:15 +0200 (CEST) Received: from proxmox-new.maurer-it.com (proxmox-new.maurer-it.com [94.136.29.106]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature RSA-PSS (2048 bits)) (No client certificate requested) by firstgate.proxmox.com (Proxmox) with ESMTPS for ; Wed, 18 Oct 2023 12:39:14 +0200 (CEST) Received: from proxmox-new.maurer-it.com (localhost.localdomain [127.0.0.1]) by proxmox-new.maurer-it.com (Proxmox) with ESMTP id 0702142A52 for ; Wed, 18 Oct 2023 12:39:14 +0200 (CEST) From: Dominik Csapak To: pbs-devel@lists.proxmox.com Date: Wed, 18 Oct 2023 12:39:08 +0200 Message-Id: <20231018103911.3798182-2-d.csapak@proxmox.com> X-Mailer: git-send-email 2.30.2 In-Reply-To: <20231018103911.3798182-1-d.csapak@proxmox.com> References: <20231018103911.3798182-1-d.csapak@proxmox.com> MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-SPAM-LEVEL: Spam detection results: 0 AWL 0.012 Adjusted score from AWL reputation of From: address BAYES_00 -1.9 Bayes spam probability is 0 to 1% DMARC_MISSING 0.1 Missing DMARC policy KAM_DMARC_STATUS 0.01 Test Rule for DKIM or SPF Failure with Strict Alignment SPF_HELO_NONE 0.001 SPF: HELO does not publish an SPF Record SPF_PASS -0.001 SPF: sender matches SPF record Subject: [pbs-devel] [RFC proxmox 1/3] new proxmox-server-config crate X-BeenThere: pbs-devel@lists.proxmox.com X-Mailman-Version: 2.1.29 Precedence: list List-Id: Proxmox Backup Server development discussion List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , X-List-Received-Date: Wed, 18 Oct 2023 10:39:46 -0000 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 --- 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 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 , + rustc:native , + libstd-rust-dev , + librust-anyhow-1+default-dev , + librust-nix-0.26+default-dev (>= 0.26.1-~~) , + librust-proxmox-sys-0.5+default-dev +Maintainer: Proxmox Support Team +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 +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 . 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 " + +[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 = 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, + + task_dir: OnceLock, + log_dir: OnceLock, + cert_dir: OnceLock, + state_dir: OnceLock, + run_dir: OnceLock, + config_dir: OnceLock, +} + +fn check_dir_permissions(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>(name: &str, base_dir: P, user: User) -> std::io::Result { + 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.set_privileged_user(user)?; + Ok(self) + } + + /// Set the task directory. + pub fn set_task_dir>(&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>(mut self, path: P) -> Result { + 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>(&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>(mut self, path: P) -> Result { + 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>(&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>(mut self, path: P) -> Result { + 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>(&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>(mut self, path: P) -> Result { + 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>(&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>(mut self, path: P) -> Result { + 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>(&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>(mut self, path: P) -> Result { + 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