From: Samuel Rufinatscha <s.rufinatscha@proxmox.com>
To: Proxmox VE development discussion <pve-devel@lists.proxmox.com>,
Kefu Chai <k.chai@proxmox.com>
Subject: Re: [pve-devel] [PATCH pve-cluster 02/15] pmxcfs-rs: add pmxcfs-config crate
Date: Fri, 23 Jan 2026 16:01:59 +0100 [thread overview]
Message-ID: <e1cae1a8-6d6f-42a2-96eb-9feb2a931027@proxmox.com> (raw)
In-Reply-To: <20260106142440.2368585-3-k.chai@proxmox.com>
comments inline
On 1/6/26 3:25 PM, Kefu Chai wrote:
> Add configuration management crate that provides:
> - Config struct for runtime configuration
> - Node hostname, IP, and group ID tracking
> - Debug and local mode flags
> - Thread-safe configuration access via parking_lot Mutex
>
> This is a foundational crate with no internal dependencies, only
> requiring parking_lot for synchronization. Other crates will use
> this for accessing runtime configuration.
>
> Signed-off-by: Kefu Chai <k.chai@proxmox.com>
> ---
> src/pmxcfs-rs/Cargo.toml | 3 +-
> src/pmxcfs-rs/pmxcfs-config/Cargo.toml | 16 +
> src/pmxcfs-rs/pmxcfs-config/README.md | 127 +++++++
> src/pmxcfs-rs/pmxcfs-config/src/lib.rs | 471 +++++++++++++++++++++++++
> 4 files changed, 616 insertions(+), 1 deletion(-)
> create mode 100644 src/pmxcfs-rs/pmxcfs-config/Cargo.toml
> create mode 100644 src/pmxcfs-rs/pmxcfs-config/README.md
> create mode 100644 src/pmxcfs-rs/pmxcfs-config/src/lib.rs
>
> diff --git a/src/pmxcfs-rs/Cargo.toml b/src/pmxcfs-rs/Cargo.toml
> index 15d88f52..28e20bb7 100644
> --- a/src/pmxcfs-rs/Cargo.toml
> +++ b/src/pmxcfs-rs/Cargo.toml
> @@ -1,7 +1,8 @@
> # Workspace root for pmxcfs Rust implementation
> [workspace]
> members = [
> - "pmxcfs-api-types", # Shared types and error definitions
> + "pmxcfs-api-types", # Shared types and error definitions
> + "pmxcfs-config", # Configuration management
> ]
> resolver = "2"
>
> diff --git a/src/pmxcfs-rs/pmxcfs-config/Cargo.toml b/src/pmxcfs-rs/pmxcfs-config/Cargo.toml
> new file mode 100644
> index 00000000..f5a60995
> --- /dev/null
> +++ b/src/pmxcfs-rs/pmxcfs-config/Cargo.toml
> @@ -0,0 +1,16 @@
> +[package]
> +name = "pmxcfs-config"
> +description = "Configuration management for pmxcfs"
> +
> +version.workspace = true
> +edition.workspace = true
> +authors.workspace = true
> +license.workspace = true
> +repository.workspace = true
> +
> +[lints]
> +workspace = true
> +
> +[dependencies]
> +# Concurrency primitives
> +parking_lot.workspace = true
> diff --git a/src/pmxcfs-rs/pmxcfs-config/README.md b/src/pmxcfs-rs/pmxcfs-config/README.md
> new file mode 100644
> index 00000000..c06b2170
> --- /dev/null
> +++ b/src/pmxcfs-rs/pmxcfs-config/README.md
> @@ -0,0 +1,127 @@
> +# pmxcfs-config
> +
> +**Configuration Management** and **Cluster Services** for pmxcfs.
> +
> +This crate provides configuration structures and cluster integration services including quorum tracking and cluster configuration monitoring via Corosync APIs.
> +
> +## Overview
> +
> +This crate contains:
> +1. **Config struct**: Runtime configuration (node name, IPs, flags)
> +2. Integration with Corosync services (tracked in main pmxcfs crate):
> + - **QuorumService** (`pmxcfs/src/quorum_service.rs`) - Quorum monitoring
> + - **ClusterConfigService** (`pmxcfs/src/cluster_config_service.rs`) - Config tracking
This patch only contains the Config struct, but not Cluster Services
or QuorumService, please revist commit message and README.
> +
> +## Config Struct
> +
> +The `Config` struct holds daemon-wide configuration including node hostname, IP address, www-data group ID, debug flag, local mode flag, and cluster name.
> +
> +## Cluster Services
> +
> +The following services are implemented in the main pmxcfs crate but documented here for completeness.
> +
> +### QuorumService
> +
> +**C Equivalent:** `src/pmxcfs/quorum.c` - `service_quorum_new()`
> +**Rust Location:** `src/pmxcfs-rs/pmxcfs/src/quorum_service.rs`
> +
> +Monitors cluster quorum status via Corosync quorum API.
> +
> +#### Features
> +- Tracks quorum state (quorate/inquorate)
> +- Monitors member list changes
> +- Automatic reconnection on Corosync restart
> +- Updates `Status` quorum flag
> +
> +#### C to Rust Mapping
> +
> +| C Function | Rust Equivalent | Location |
> +|-----------|-----------------|----------|
> +| `service_quorum_new()` | `QuorumService::new()` | quorum_service.rs |
> +| `service_quorum_destroy()` | (Drop trait / finalize) | Automatic |
> +| `quorum_notification_fn` | quorum_notification closure | quorum_service.rs |
> +| `nodelist_notification_fn` | nodelist_notification closure | quorum_service.rs |
> +
> +#### Quorum Notifications
> +
> +The service monitors quorum state changes and member list changes, updating the Status accordingly.
> +
> +### ClusterConfigService
> +
> +**C Equivalent:** `src/pmxcfs/confdb.c` - `service_confdb_new()`
> +**Rust Location:** `src/pmxcfs-rs/pmxcfs/src/cluster_config_service.rs`
> +
> +Monitors Corosync cluster configuration (cmap) and tracks node membership.
> +
> +#### Features
> +- Monitors cluster membership via Corosync cmap API
> +- Tracks node additions/removals
> +- Registers nodes in Status
> +- Automatic reconnection on Corosync restart
> +
> +#### C to Rust Mapping
> +
> +| C Function | Rust Equivalent | Location |
> +|-----------|-----------------|----------|
> +| `service_confdb_new()` | `ClusterConfigService::new()` | cluster_config_service.rs |
> +| `service_confdb_destroy()` | (Drop trait / finalize) | Automatic |
> +| `confdb_track_fn` | (direct cmap queries) | Different approach |
> +
> +#### Configuration Tracking
> +
> +The service monitors:
> +- `nodelist.node.*.nodeid` - Node IDs
> +- `nodelist.node.*.name` - Node names
> +- `nodelist.node.*.ring*_addr` - Node IP addresses
> +
> +Updates `Status` with current cluster membership.
> +
> +## Key Differences from C Implementation
> +
> +### Cluster Config Service API
> +
> +**C Version (confdb.c):**
> +- Uses deprecated confdb API
> +- Track changes via confdb notifications
> +
> +**Rust Version:**
> +- Uses modern cmap API
> +- Direct cmap queries
> +
> +Both read the same data, but Rust uses the modern Corosync API.
> +
> +### Service Integration
> +
> +**C Version:**
> +- qb_loop manages lifecycle
> +
> +**Rust Version:**
> +- Service trait abstracts lifecycle
> +- ServiceManager handles retry
> +- Tokio async dispatch
> +
> +## Known Issues / TODOs
> +
> +### Compatibility
> +- **Quorum tracking**: Compatible with C implementation
> +- **Node registration**: Equivalent behavior
> +- **cmap vs confdb**: Rust uses modern cmap API (C uses deprecated confdb)
> +
> +### Missing Features
> +- None identified
> +
> +### Behavioral Differences (Benign)
> +- **API choice**: Rust uses cmap, C uses confdb (both read same data)
> +- **Lifecycle**: Rust uses Service trait, C uses manual lifecycle
> +
> +## References
> +
> +### C Implementation
> +- `src/pmxcfs/quorum.c` / `quorum.h` - Quorum service
> +- `src/pmxcfs/confdb.c` / `confdb.h` - Cluster config service
> +
> +### Related Crates
> +- **pmxcfs**: Main daemon with QuorumService and ClusterConfigService
> +- **pmxcfs-status**: Status tracking updated by these services
> +- **pmxcfs-services**: Service framework used by both services
> +- **rust-corosync**: Corosync FFI bindings
> diff --git a/src/pmxcfs-rs/pmxcfs-config/src/lib.rs b/src/pmxcfs-rs/pmxcfs-config/src/lib.rs
> new file mode 100644
> index 00000000..5e1ee1b2
> --- /dev/null
> +++ b/src/pmxcfs-rs/pmxcfs-config/src/lib.rs
> @@ -0,0 +1,471 @@
> +use parking_lot::RwLock;
> +use std::sync::Arc;
> +
> +/// Global configuration for pmxcfs
> +pub struct Config {
> + /// Node name (hostname without domain)
> + pub nodename: String,
> +
> + /// Node IP address
> + pub node_ip: String,
Consider using std::net::IpAddr (or SocketAddr if a port is part of the
value). Tests currently mix IP vs IP:PORT, so it’s unclear what node_ip
is supposed to represent.
> +
> + /// www-data group ID for file permissions
> + pub www_data_gid: u32,
> +
> + /// Debug mode enabled
> + pub debug: bool,
> +
> + /// Force local mode (no clustering)
> + pub local_mode: bool,
> +
> + /// Cluster name (CPG group name)
> + pub cluster_name: String,
> +
> + /// Debug level (0 = normal, 1+ = debug) - mutable at runtime
> + debug_level: RwLock<u8>,
in the crate docs it says: “The Config struct uses Arc<AtomicU8> for
debug_level” but the implementation uses parking_lot::RwLock<u8>.
Unless we need lock coupling with other fields, AtomicU8 would likely
be sufficient (and cheaper) for debug_level. Also please re-check the
commit message, which mentions parking_lot::Mutex.
> +}
> +
> +impl Clone for Config {
> + fn clone(&self) -> Self {
> + Self {
> + nodename: self.nodename.clone(),
> + node_ip: self.node_ip.clone(),
> + www_data_gid: self.www_data_gid,
> + debug: self.debug,
> + local_mode: self.local_mode,
> + cluster_name: self.cluster_name.clone(),
> + debug_level: RwLock::new(*self.debug_level.read()),
> + }
> + }
> +}
> +
> +impl std::fmt::Debug for Config {
> + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
> + f.debug_struct("Config")
> + .field("nodename", &self.nodename)
> + .field("node_ip", &self.node_ip)
> + .field("www_data_gid", &self.www_data_gid)
> + .field("debug", &self.debug)
> + .field("local_mode", &self.local_mode)
> + .field("cluster_name", &self.cluster_name)
> + .field("debug_level", &*self.debug_level.read())
> + .finish()
> + }
> +}
> +
> +impl Config {
> + pub fn new(
> + nodename: String,
> + node_ip: String,
> + www_data_gid: u32,
> + debug: bool,
> + local_mode: bool,
> + cluster_name: String,
> + ) -> Arc<Self> {
The constructor returns Arc<Config>
I think we could keep new() -> Self, and provide convenience
constructor shared() -> Arc<Self>.
This would allow local usage (e.g. for tests) without heap allocation
of the struct
> + let debug_level = if debug { 1 } else { 0 };
debug_level is derived from debug at creation time, but thereafter:
set_debug_level() does not update debug and is_debug() would continue
to reflect the initial flag, not the effective debug level
is_debug() should just be a helper that returns self.debug_level() > 0.
The debug field should probably be removed entirely.
> + Arc::new(Self {
> + nodename,
> + node_ip,
> + www_data_gid,
> + debug,
> + local_mode,
> + cluster_name,
> + debug_level: RwLock::new(debug_level),
> + })
> + }
> +
> + pub fn cluster_name(&self) -> &str {
> + &self.cluster_name
> + }
> +
> + pub fn nodename(&self) -> &str {
> + &self.nodename
> + }
> +
> + pub fn node_ip(&self) -> &str {
> + &self.node_ip
> + }
> +
> + pub fn www_data_gid(&self) -> u32 {
> + self.www_data_gid
> + }
> +
> + pub fn is_debug(&self) -> bool {
> + self.debug
> + }
> +
> + pub fn is_local_mode(&self) -> bool {
> + self.local_mode
> + }
> +
> + /// Get current debug level (0 = normal, 1+ = debug)
> + pub fn debug_level(&self) -> u8 {
> + *self.debug_level.read()
> + }
> +
> + /// Set debug level (0 = normal, 1+ = debug)
> + pub fn set_debug_level(&self, level: u8) {
> + *self.debug_level.write() = level;
> + }
Right now most fields are pub but also getters are exposed. This will
make it harder to enforce invariants.
I would suggest to make fields private and keep getters, or keep fields
public and drop the getters.
> +}
> +
> +#[cfg(test)]
> +mod tests {
> + //! Unit tests for Config struct
> + //!
> + //! This test module provides comprehensive coverage for:
> + //! - Configuration creation and initialization
> + //! - Getter methods for all configuration fields
> + //! - Debug level mutation and thread safety
> + //! - Concurrent access patterns (reads and writes)
> + //! - Clone independence
> + //! - Debug formatting
> + //! - Edge cases (empty strings, long strings, special characters, unicode)
> + //!
> + //! ## Thread Safety
> + //!
> + //! The Config struct uses `Arc<AtomicU8>` for debug_level to allow
> + //! safe concurrent reads and writes. Tests verify:
> + //! - 10 threads × 100 operations (concurrent modifications)
> + //! - 20 threads × 1000 operations (concurrent reads)
> + //!
> + //! ## Edge Cases
> + //!
> + //! Tests cover various edge cases including:
> + //! - Empty strings for node/cluster names
> + //! - Long strings (1000+ characters)
> + //! - Special characters in strings
> + //! - Unicode support (emoji, non-ASCII characters)
> +
> + use super::*;
> + use std::thread;
> +
> + // ===== Basic Construction Tests =====
> +
> + #[test]
> + fn test_config_creation() {
> + let config = Config::new(
> + "node1".to_string(),
> + "192.168.1.10".to_string(),
> + 33,
> + false,
> + false,
> + "pmxcfs".to_string(),
> + );
> +
> + assert_eq!(config.nodename(), "node1");
> + assert_eq!(config.node_ip(), "192.168.1.10");
> + assert_eq!(config.www_data_gid(), 33);
> + assert!(!config.is_debug());
> + assert!(!config.is_local_mode());
> + assert_eq!(config.cluster_name(), "pmxcfs");
> + assert_eq!(
> + config.debug_level(),
> + 0,
> + "Debug level should be 0 when debug is false"
> + );
> + }
> +
> + #[test]
> + fn test_config_creation_with_debug() {
> + let config = Config::new(
> + "node2".to_string(),
> + "10.0.0.5".to_string(),
> + 1000,
> + true,
> + false,
> + "test-cluster".to_string(),
> + );
> +
> + assert!(config.is_debug());
> + assert_eq!(
> + config.debug_level(),
> + 1,
> + "Debug level should be 1 when debug is true"
> + );
> + }
> +
> + #[test]
> + fn test_config_creation_local_mode() {
> + let config = Config::new(
> + "localhost".to_string(),
> + "127.0.0.1".to_string(),
> + 33,
> + false,
> + true,
> + "local".to_string(),
> + );
> +
> + assert!(config.is_local_mode());
> + assert!(!config.is_debug());
> + }
> +
> + // ===== Getter Tests =====
> +
> + #[test]
> + fn test_all_getters() {
> + let config = Config::new(
> + "testnode".to_string(),
> + "172.16.0.1".to_string(),
> + 999,
> + true,
> + true,
> + "my-cluster".to_string(),
> + );
> +
> + // Test all getter methods
> + assert_eq!(config.nodename(), "testnode");
> + assert_eq!(config.node_ip(), "172.16.0.1");
> + assert_eq!(config.www_data_gid(), 999);
> + assert!(config.is_debug());
> + assert!(config.is_local_mode());
> + assert_eq!(config.cluster_name(), "my-cluster");
> + assert_eq!(config.debug_level(), 1);
> + }
> +
> + // ===== Debug Level Mutation Tests =====
> +
> + #[test]
> + fn test_debug_level_mutation() {
> + let config = Config::new(
> + "node1".to_string(),
> + "192.168.1.1".to_string(),
> + 33,
> + false,
> + false,
> + "pmxcfs".to_string(),
> + );
> +
> + assert_eq!(config.debug_level(), 0);
> +
> + config.set_debug_level(1);
> + assert_eq!(config.debug_level(), 1);
> +
> + config.set_debug_level(5);
> + assert_eq!(config.debug_level(), 5);
> +
> + config.set_debug_level(0);
> + assert_eq!(config.debug_level(), 0);
> + }
> +
> + #[test]
> + fn test_debug_level_max_value() {
> + let config = Config::new(
> + "node1".to_string(),
> + "192.168.1.1".to_string(),
> + 33,
> + false,
> + false,
> + "pmxcfs".to_string(),
> + );
> +
> + config.set_debug_level(255);
> + assert_eq!(config.debug_level(), 255);
> +
> + config.set_debug_level(0);
> + assert_eq!(config.debug_level(), 0);
> + }
> +
> + // ===== Thread Safety Tests =====
> +
> + #[test]
> + fn test_debug_level_thread_safety() {
> + let config = Config::new(
> + "node1".to_string(),
> + "192.168.1.1".to_string(),
> + 33,
> + false,
> + false,
> + "pmxcfs".to_string(),
> + );
> +
> + let config_clone = Arc::clone(&config);
> +
> + // Spawn multiple threads that concurrently modify debug level
> + let handles: Vec<_> = (0..10)
> + .map(|i| {
> + let cfg = Arc::clone(&config);
> + thread::spawn(move || {
> + for _ in 0..100 {
> + cfg.set_debug_level(i);
> + let _ = cfg.debug_level();
> + }
> + })
> + })
> + .collect();
> +
> + // All threads should complete without panicking
> + for handle in handles {
> + handle.join().unwrap();
> + }
> +
> + // Final value should be one of the values set by threads
> + let final_level = config_clone.debug_level();
> + assert!(
> + final_level < 10,
> + "Debug level should be < 10, got {final_level}"
> + );
> + }
> +
> + #[test]
> + fn test_concurrent_reads() {
> + let config = Config::new(
> + "node1".to_string(),
> + "192.168.1.1".to_string(),
> + 33,
> + true,
> + false,
> + "pmxcfs".to_string(),
> + );
> +
> + // Spawn multiple threads that concurrently read config
> + let handles: Vec<_> = (0..20)
> + .map(|_| {
> + let cfg = Arc::clone(&config);
> + thread::spawn(move || {
> + for _ in 0..1000 {
> + assert_eq!(cfg.nodename(), "node1");
> + assert_eq!(cfg.node_ip(), "192.168.1.1");
> + assert_eq!(cfg.www_data_gid(), 33);
> + assert!(cfg.is_debug());
> + assert!(!cfg.is_local_mode());
> + assert_eq!(cfg.cluster_name(), "pmxcfs");
> + }
> + })
> + })
> + .collect();
> +
> + for handle in handles {
> + handle.join().unwrap();
> + }
> + }
> +
> + // ===== Clone Tests =====
> +
> + #[test]
> + fn test_config_clone() {
> + let config1 = Config::new(
> + "node1".to_string(),
> + "192.168.1.1".to_string(),
> + 33,
> + true,
> + false,
> + "pmxcfs".to_string(),
> + );
> +
> + config1.set_debug_level(5);
> +
> + let config2 = (*config1).clone();
> +
> + // Cloned config should have same values
> + assert_eq!(config2.nodename(), config1.nodename());
> + assert_eq!(config2.node_ip(), config1.node_ip());
> + assert_eq!(config2.www_data_gid(), config1.www_data_gid());
> + assert_eq!(config2.is_debug(), config1.is_debug());
> + assert_eq!(config2.is_local_mode(), config1.is_local_mode());
> + assert_eq!(config2.cluster_name(), config1.cluster_name());
> + assert_eq!(config2.debug_level(), 5);
> +
> + // Modifying one should not affect the other
> + config2.set_debug_level(10);
> + assert_eq!(config1.debug_level(), 5);
> + assert_eq!(config2.debug_level(), 10);
> + }
> +
> + // ===== Debug Formatting Tests =====
> +
> + #[test]
> + fn test_debug_format() {
> + let config = Config::new(
> + "node1".to_string(),
> + "192.168.1.1".to_string(),
> + 33,
> + true,
> + false,
> + "pmxcfs".to_string(),
> + );
> +
> + let debug_str = format!("{config:?}");
> +
> + // Check that debug output contains all fields
> + assert!(debug_str.contains("Config"));
> + assert!(debug_str.contains("nodename"));
> + assert!(debug_str.contains("node1"));
> + assert!(debug_str.contains("node_ip"));
> + assert!(debug_str.contains("192.168.1.1"));
> + assert!(debug_str.contains("www_data_gid"));
> + assert!(debug_str.contains("33"));
> + assert!(debug_str.contains("debug"));
> + assert!(debug_str.contains("true"));
> + assert!(debug_str.contains("local_mode"));
> + assert!(debug_str.contains("false"));
> + assert!(debug_str.contains("cluster_name"));
> + assert!(debug_str.contains("pmxcfs"));
> + assert!(debug_str.contains("debug_level"));
> + }
> +
> + // ===== Edge Cases and Boundary Tests =====
> +
> + #[test]
> + fn test_empty_strings() {
> + let config = Config::new(String::new(), String::new(), 0, false, false, String::new());
> +
> + assert_eq!(config.nodename(), "");
> + assert_eq!(config.node_ip(), "");
> + assert_eq!(config.cluster_name(), "");
> + assert_eq!(config.www_data_gid(), 0);
> + }
> +
> + #[test]
> + fn test_long_strings() {
> + let long_name = "a".repeat(1000);
> + let long_ip = "192.168.1.".to_string() + &"1".repeat(100);
> + let long_cluster = "cluster-".to_string() + &"x".repeat(500);
> +
> + let config = Config::new(
> + long_name.clone(),
> + long_ip.clone(),
> + u32::MAX,
> + true,
> + true,
> + long_cluster.clone(),
> + );
> +
> + assert_eq!(config.nodename(), long_name);
> + assert_eq!(config.node_ip(), long_ip);
> + assert_eq!(config.cluster_name(), long_cluster);
> + assert_eq!(config.www_data_gid(), u32::MAX);
> + }
> +
> + #[test]
> + fn test_special_characters_in_strings() {
> + let config = Config::new(
> + "node-1_test.local".to_string(),
> + "192.168.1.10:8006".to_string(),
> + 33,
> + false,
> + false,
> + "my-cluster_v2.0".to_string(),
> + );
> +
> + assert_eq!(config.nodename(), "node-1_test.local");
> + assert_eq!(config.node_ip(), "192.168.1.10:8006");
> + assert_eq!(config.cluster_name(), "my-cluster_v2.0");
> + }
> +
> + #[test]
> + fn test_unicode_in_strings() {
> + let config = Config::new(
> + "ノード1".to_string(),
> + "::1".to_string(),
> + 33,
> + false,
> + false,
> + "集群".to_string(),
> + );
> +
> + assert_eq!(config.nodename(), "ノード1");
> + assert_eq!(config.node_ip(), "::1");
> + assert_eq!(config.cluster_name(), "集群");
> + }
> +}
_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel
next prev parent reply other threads:[~2026-01-23 15:02 UTC|newest]
Thread overview: 19+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-01-06 14:24 [pve-devel] [PATCH pve-cluster 00/15 v1] Rewrite pmxcfs with Rust Kefu Chai
2026-01-06 14:24 ` [pve-devel] [PATCH pve-cluster 01/15] pmxcfs-rs: add workspace and pmxcfs-api-types crate Kefu Chai
2026-01-23 14:17 ` Samuel Rufinatscha
2026-01-26 9:00 ` Kefu Chai
2026-01-06 14:24 ` [pve-devel] [PATCH pve-cluster 02/15] pmxcfs-rs: add pmxcfs-config crate Kefu Chai
2026-01-23 15:01 ` Samuel Rufinatscha [this message]
2026-01-26 9:43 ` Kefu Chai
2026-01-06 14:24 ` [pve-devel] [PATCH pve-cluster 03/15] pmxcfs-rs: add pmxcfs-logger crate Kefu Chai
2026-01-06 14:24 ` [pve-devel] [PATCH pve-cluster 04/15] pmxcfs-rs: add pmxcfs-rrd crate Kefu Chai
2026-01-06 14:24 ` [pve-devel] [PATCH pve-cluster 05/15] pmxcfs-rs: add pmxcfs-memdb crate Kefu Chai
2026-01-06 14:24 ` [pve-devel] [PATCH pve-cluster 06/15] pmxcfs-rs: add pmxcfs-status crate Kefu Chai
2026-01-06 14:24 ` [pve-devel] [PATCH pve-cluster 07/15] pmxcfs-rs: add pmxcfs-test-utils infrastructure crate Kefu Chai
2026-01-06 14:24 ` [pve-devel] [PATCH pve-cluster 08/15] pmxcfs-rs: add pmxcfs-services crate Kefu Chai
2026-01-06 14:24 ` [pve-devel] [PATCH pve-cluster 09/15] pmxcfs-rs: add pmxcfs-ipc crate Kefu Chai
2026-01-06 14:24 ` [pve-devel] [PATCH pve-cluster 10/15] pmxcfs-rs: add pmxcfs-dfsm crate Kefu Chai
2026-01-06 14:24 ` [pve-devel] [PATCH pve-cluster 11/15] pmxcfs-rs: vendor patched rust-corosync for CPG compatibility Kefu Chai
2026-01-06 14:24 ` [pve-devel] [PATCH pve-cluster 13/15] pmxcfs-rs: add integration and workspace tests Kefu Chai
2026-01-06 14:24 ` [pve-devel] [PATCH pve-cluster 14/15] pmxcfs-rs: add Makefile for build automation Kefu Chai
2026-01-06 14:24 ` [pve-devel] [PATCH pve-cluster 15/15] pmxcfs-rs: add project documentation Kefu Chai
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=e1cae1a8-6d6f-42a2-96eb-9feb2a931027@proxmox.com \
--to=s.rufinatscha@proxmox.com \
--cc=k.chai@proxmox.com \
--cc=pve-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 an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.