From: "Kefu Chai" <k.chai@proxmox.com>
To: "Samuel Rufinatscha" <s.rufinatscha@proxmox.com>,
"Proxmox VE development discussion" <pve-devel@lists.proxmox.com>
Subject: Re: [pve-devel] [PATCH pve-cluster 02/15] pmxcfs-rs: add pmxcfs-config crate
Date: Mon, 26 Jan 2026 17:43:28 +0800 [thread overview]
Message-ID: <DFYF9A0PC0DB.3K172CX67OQA2@proxmox.com> (raw)
In-Reply-To: <e1cae1a8-6d6f-42a2-96eb-9feb2a931027@proxmox.com>
On Fri Jan 23, 2026 at 11:01 PM CST, Samuel Rufinatscha wrote:
> 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.
Sorry, the README.md was out-of-sync after the latest refatory. Fixed.
>
>> +
>> +## 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.
It's a value extracted from resolve_node_ip(), so it's just an IP
address. so switched to IpAddr, and tests are updated accordingly.
>
>> +
>> + /// 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.
Indeed. AtomicU8 is more light-weight and simpler than RwLock. changed
accordingly.
>
>> +}
>> +
>> +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
Config::new() is added. And tests are using it automatically.
>
>> + 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.
Ahh, thanks for pointing this out. Fixed.
>
>> + 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.
Indeed. I made all fields private and keep 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-26 9:43 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
2026-01-26 9:43 ` Kefu Chai [this message]
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=DFYF9A0PC0DB.3K172CX67OQA2@proxmox.com \
--to=k.chai@proxmox.com \
--cc=pve-devel@lists.proxmox.com \
--cc=s.rufinatscha@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