1040 lines
34 KiB
Rust
1040 lines
34 KiB
Rust
// Copyright 2024 RustFS Team
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
//! System information display module.
|
|
//!
|
|
//! This module provides the `--info` command functionality for querying and displaying
|
|
//! various system information including system basics, runtime stats, build info,
|
|
//! configuration, and dependencies.
|
|
|
|
use super::{InfoOpts, InfoType};
|
|
use crate::runtime_sources::current_buffer_config;
|
|
use crate::version::build;
|
|
use rustfs_credentials::Masked;
|
|
use serde::Serialize;
|
|
use std::fmt;
|
|
|
|
/// CPU information
|
|
#[derive(Serialize)]
|
|
struct CpuInfo {
|
|
/// Number of logical CPU cores
|
|
core_count: usize,
|
|
/// CPU brand/vendor name
|
|
brand: String,
|
|
/// CPU frequency in MHz
|
|
frequency_mhz: u64,
|
|
/// CPU usage percentage (0-100)
|
|
usage_percent: f64,
|
|
}
|
|
|
|
impl CpuInfo {
|
|
fn collect(sys: &sysinfo::System) -> Self {
|
|
let core_count = sys.cpus().len();
|
|
let brand = sys
|
|
.cpus()
|
|
.first()
|
|
.map(|c| c.brand().to_string())
|
|
.unwrap_or_else(|| "Unknown".to_string());
|
|
let frequency_mhz = sys.cpus().first().map(|c| c.frequency()).unwrap_or(0);
|
|
|
|
// Calculate average CPU usage
|
|
let usage_percent = if core_count > 0 {
|
|
sys.cpus().iter().map(|c| c.cpu_usage() as f64).sum::<f64>() / core_count as f64
|
|
} else {
|
|
0.0
|
|
};
|
|
|
|
Self {
|
|
core_count,
|
|
brand,
|
|
frequency_mhz,
|
|
usage_percent,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Memory information
|
|
#[derive(Serialize)]
|
|
struct MemoryInfo {
|
|
/// Total system memory in bytes
|
|
total_bytes: u64,
|
|
/// Used memory in bytes
|
|
used_bytes: u64,
|
|
/// Available/free memory in bytes
|
|
available_bytes: u64,
|
|
/// Total swap memory in bytes
|
|
total_swap_bytes: u64,
|
|
/// Used swap memory in bytes
|
|
used_swap_bytes: u64,
|
|
/// Memory usage percentage
|
|
usage_percent: f64,
|
|
}
|
|
|
|
impl MemoryInfo {
|
|
fn collect(sys: &sysinfo::System) -> Self {
|
|
let total_bytes = sys.total_memory();
|
|
let used_bytes = sys.used_memory();
|
|
let available_bytes = sys.available_memory();
|
|
let total_swap_bytes = sys.total_swap();
|
|
let used_swap_bytes = sys.used_swap();
|
|
|
|
let usage_percent = if total_bytes > 0 {
|
|
(used_bytes as f64 / total_bytes as f64) * 100.0
|
|
} else {
|
|
0.0
|
|
};
|
|
|
|
Self {
|
|
total_bytes,
|
|
used_bytes,
|
|
available_bytes,
|
|
total_swap_bytes,
|
|
used_swap_bytes,
|
|
usage_percent,
|
|
}
|
|
}
|
|
|
|
fn format_bytes(bytes: u64) -> String {
|
|
const KB: u64 = 1024;
|
|
const MB: u64 = KB * 1024;
|
|
const GB: u64 = MB * 1024;
|
|
const TB: u64 = GB * 1024;
|
|
|
|
if bytes >= TB {
|
|
format!("{:.2} TB", bytes as f64 / TB as f64)
|
|
} else if bytes >= GB {
|
|
format!("{:.2} GB", bytes as f64 / GB as f64)
|
|
} else if bytes >= MB {
|
|
format!("{:.2} MB", bytes as f64 / MB as f64)
|
|
} else if bytes >= KB {
|
|
format!("{:.2} KB", bytes as f64 / KB as f64)
|
|
} else {
|
|
format!("{} B", bytes)
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Disk information
|
|
#[derive(Serialize)]
|
|
struct DiskInfo {
|
|
/// Disk mount point
|
|
mount_point: String,
|
|
/// Disk name/device
|
|
name: String,
|
|
/// File system type
|
|
file_system: String,
|
|
/// Total space in bytes
|
|
total_bytes: u64,
|
|
/// Used space in bytes
|
|
used_bytes: u64,
|
|
/// Available space in bytes
|
|
available_bytes: u64,
|
|
/// Usage percentage
|
|
usage_percent: f64,
|
|
/// Is this a removable disk
|
|
is_removable: bool,
|
|
}
|
|
|
|
impl DiskInfo {
|
|
fn collect_all() -> Vec<Self> {
|
|
let disks = sysinfo::Disks::new_with_refreshed_list();
|
|
disks
|
|
.iter()
|
|
.map(|disk| {
|
|
let total_bytes = disk.total_space();
|
|
let available_bytes = disk.available_space();
|
|
let used_bytes = total_bytes.saturating_sub(available_bytes);
|
|
let usage_percent = if total_bytes > 0 {
|
|
(used_bytes as f64 / total_bytes as f64) * 100.0
|
|
} else {
|
|
0.0
|
|
};
|
|
|
|
Self {
|
|
mount_point: disk.mount_point().to_string_lossy().to_string(),
|
|
name: disk.name().to_string_lossy().to_string(),
|
|
file_system: format!("{:?}", disk.file_system()),
|
|
total_bytes,
|
|
used_bytes,
|
|
available_bytes,
|
|
usage_percent,
|
|
is_removable: disk.is_removable(),
|
|
}
|
|
})
|
|
.collect()
|
|
}
|
|
}
|
|
|
|
/// Service disk information (disk where the service data is stored)
|
|
#[derive(Serialize)]
|
|
struct ServiceDiskInfo {
|
|
/// Disk mount point
|
|
mount_point: String,
|
|
/// Disk name/device
|
|
name: String,
|
|
/// File system type
|
|
file_system: String,
|
|
/// Total space in bytes
|
|
total_bytes: u64,
|
|
/// Used space in bytes
|
|
used_bytes: u64,
|
|
/// Available space in bytes
|
|
available_bytes: u64,
|
|
/// Usage percentage
|
|
usage_percent: f64,
|
|
/// Volume paths served by this disk
|
|
volume_paths: Vec<String>,
|
|
}
|
|
|
|
impl ServiceDiskInfo {
|
|
fn collect(volumes: &[String]) -> Option<Self> {
|
|
// Find the disk that contains the first volume path
|
|
let first_volume = volumes.first()?;
|
|
let volume_path = std::path::Path::new(first_volume);
|
|
|
|
// Find the disk that contains this volume
|
|
let disks = sysinfo::Disks::new_with_refreshed_list();
|
|
for disk in disks.iter() {
|
|
let mount_point = disk.mount_point();
|
|
if volume_path.starts_with(mount_point) {
|
|
let total_bytes = disk.total_space();
|
|
let available_bytes = disk.available_space();
|
|
let used_bytes = total_bytes.saturating_sub(available_bytes);
|
|
let usage_percent = if total_bytes > 0 {
|
|
(used_bytes as f64 / total_bytes as f64) * 100.0
|
|
} else {
|
|
0.0
|
|
};
|
|
|
|
// Find all volumes on this disk
|
|
let volume_paths: Vec<String> = volumes
|
|
.iter()
|
|
.filter(|v| std::path::Path::new(v).starts_with(mount_point))
|
|
.cloned()
|
|
.collect();
|
|
|
|
return Some(Self {
|
|
mount_point: mount_point.to_string_lossy().to_string(),
|
|
name: disk.name().to_string_lossy().to_string(),
|
|
file_system: format!("{:?}", disk.file_system()),
|
|
total_bytes,
|
|
used_bytes,
|
|
available_bytes,
|
|
usage_percent,
|
|
volume_paths,
|
|
});
|
|
}
|
|
}
|
|
|
|
None
|
|
}
|
|
|
|
fn format(&self) -> String {
|
|
let volumes_str = if self.volume_paths.is_empty() {
|
|
"(none)".to_string()
|
|
} else {
|
|
self.volume_paths.join(", ")
|
|
};
|
|
|
|
format!(
|
|
"## Service Disk Information\n\n\
|
|
| Property | Value |\n\
|
|
|----------|-------|\n\
|
|
| Disk | {} |\n\
|
|
| Mount Point | {} |\n\
|
|
| File System | {} |\n\
|
|
| Total Space | {} |\n\
|
|
| Used Space | {} ({:.1}%) |\n\
|
|
| Available Space | {} |\n\
|
|
| Served Volumes | {} |",
|
|
self.name,
|
|
self.mount_point,
|
|
self.file_system,
|
|
MemoryInfo::format_bytes(self.total_bytes),
|
|
MemoryInfo::format_bytes(self.used_bytes),
|
|
self.usage_percent,
|
|
MemoryInfo::format_bytes(self.available_bytes),
|
|
volumes_str
|
|
)
|
|
}
|
|
}
|
|
|
|
/// System basic information
|
|
#[derive(Serialize)]
|
|
struct SystemInfo {
|
|
os_type: String,
|
|
os_version: String,
|
|
architecture: String,
|
|
hostname: String,
|
|
kernel_version: String,
|
|
/// CPU information
|
|
cpu: CpuInfo,
|
|
/// Memory information
|
|
memory: MemoryInfo,
|
|
/// All disk information
|
|
disks: Vec<DiskInfo>,
|
|
}
|
|
|
|
impl SystemInfo {
|
|
fn collect() -> Self {
|
|
// Create system info collector
|
|
let mut sys = sysinfo::System::new_all();
|
|
sys.refresh_all();
|
|
sys.refresh_cpu_all();
|
|
|
|
Self {
|
|
os_type: sysinfo::System::distribution_id(),
|
|
os_version: sysinfo::System::long_os_version().unwrap_or_else(|| "Unknown".to_string()),
|
|
architecture: std::env::consts::ARCH.to_string(),
|
|
hostname: sysinfo::System::host_name().unwrap_or_else(|| "Unknown".to_string()),
|
|
kernel_version: sysinfo::System::kernel_long_version(),
|
|
cpu: CpuInfo::collect(&sys),
|
|
memory: MemoryInfo::collect(&sys),
|
|
disks: DiskInfo::collect_all(),
|
|
}
|
|
}
|
|
|
|
fn format(&self) -> String {
|
|
let mut output = String::new();
|
|
|
|
// System Information Table
|
|
output.push_str("## System Information\n\n");
|
|
output.push_str("| Property | Value |\n");
|
|
output.push_str("|----------|-------|\n");
|
|
output.push_str(&format!("| OS | {} |\n", self.os_type));
|
|
output.push_str(&format!("| OS Version | {} |\n", self.os_version));
|
|
output.push_str(&format!("| Architecture | {} |\n", self.architecture));
|
|
output.push_str(&format!("| Hostname | {} |\n", self.hostname));
|
|
output.push_str(&format!("| Kernel Version | {} |\n", self.kernel_version));
|
|
|
|
// CPU Information Table
|
|
output.push_str("\n## CPU Information\n\n");
|
|
output.push_str("| Property | Value |\n");
|
|
output.push_str("|----------|-------|\n");
|
|
output.push_str(&format!("| Cores | {} |\n", self.cpu.core_count));
|
|
output.push_str(&format!("| Brand | {} |\n", self.cpu.brand));
|
|
output.push_str(&format!("| Frequency | {} MHz |\n", self.cpu.frequency_mhz));
|
|
output.push_str(&format!("| Usage | {:.1}% |\n", self.cpu.usage_percent));
|
|
|
|
// Memory Information Table
|
|
output.push_str("\n## Memory Information\n\n");
|
|
output.push_str("| Property | Value |\n");
|
|
output.push_str("|----------|-------|\n");
|
|
output.push_str(&format!("| Total | {} |\n", MemoryInfo::format_bytes(self.memory.total_bytes)));
|
|
output.push_str(&format!(
|
|
"| Used | {} ({:.1}%) |\n",
|
|
MemoryInfo::format_bytes(self.memory.used_bytes),
|
|
self.memory.usage_percent
|
|
));
|
|
output.push_str(&format!("| Available | {} |\n", MemoryInfo::format_bytes(self.memory.available_bytes)));
|
|
output.push_str(&format!("| Total Swap | {} |\n", MemoryInfo::format_bytes(self.memory.total_swap_bytes)));
|
|
output.push_str(&format!("| Used Swap | {} |\n", MemoryInfo::format_bytes(self.memory.used_swap_bytes)));
|
|
|
|
// Disk Information Table
|
|
output.push_str("\n## Disk Information\n\n");
|
|
if self.disks.is_empty() {
|
|
output.push_str("*No disks found*\n");
|
|
} else {
|
|
output.push_str("| Name | Mount Point | Type | Total | Used | Available | Usage | Removable |\n");
|
|
output.push_str("|------|-------------|------|-------|------|-----------|-------|----------|\n");
|
|
for disk in &self.disks {
|
|
output.push_str(&format!(
|
|
"| {} | {} | {} | {} | {} | {} | {:.1}% | {} |\n",
|
|
disk.name,
|
|
disk.mount_point,
|
|
disk.file_system,
|
|
MemoryInfo::format_bytes(disk.total_bytes),
|
|
MemoryInfo::format_bytes(disk.used_bytes),
|
|
MemoryInfo::format_bytes(disk.available_bytes),
|
|
disk.usage_percent,
|
|
disk.is_removable
|
|
));
|
|
}
|
|
}
|
|
|
|
output
|
|
}
|
|
}
|
|
impl fmt::Display for SystemInfo {
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
write!(f, "{}", self.format())
|
|
}
|
|
}
|
|
|
|
/// Runtime information
|
|
#[derive(Serialize)]
|
|
struct RuntimeInfo {
|
|
process_id: u32,
|
|
memory_usage_mb: f64,
|
|
cpu_usage_percent: f64,
|
|
thread_count: usize,
|
|
}
|
|
|
|
impl RuntimeInfo {
|
|
fn collect() -> Self {
|
|
let (process_id, memory_usage_mb, cpu_usage_percent) = if let Ok(pid) = sysinfo::get_current_pid() {
|
|
let mut sys = sysinfo::System::new();
|
|
sys.refresh_processes_specifics(
|
|
sysinfo::ProcessesToUpdate::Some(&[pid]),
|
|
true,
|
|
sysinfo::ProcessRefreshKind::everything(),
|
|
);
|
|
|
|
let process = sys.process(pid);
|
|
let (memory_usage_mb, cpu_usage_percent) = if let Some(p) = process {
|
|
let memory = p.memory() as f64 / 1024.0 / 1024.0; // Convert to MB
|
|
let cpu = p.cpu_usage() as f64;
|
|
(memory, cpu)
|
|
} else {
|
|
(0.0, 0.0)
|
|
};
|
|
|
|
(pid.as_u32(), memory_usage_mb, cpu_usage_percent)
|
|
} else {
|
|
// Failed to retrieve current PID; degrade gracefully by
|
|
// skipping per-process stats and using sentinel values.
|
|
(0, 0.0, 0.0)
|
|
};
|
|
|
|
// // Get available CPU parallelism (roughly, logical cores available to the process)
|
|
let cpu_parallelism = std::thread::available_parallelism().map(|p| p.get()).unwrap_or(1);
|
|
|
|
Self {
|
|
process_id,
|
|
memory_usage_mb,
|
|
cpu_usage_percent,
|
|
thread_count: cpu_parallelism,
|
|
}
|
|
}
|
|
|
|
fn format(&self) -> String {
|
|
let pid_display = if self.process_id == 0 {
|
|
"unknown".to_string()
|
|
} else {
|
|
self.process_id.to_string()
|
|
};
|
|
|
|
format!(
|
|
"## Runtime Information\n\n\
|
|
| Property | Value |\n\
|
|
|----------|-------|\n\
|
|
| Process ID | {} |\n\
|
|
| Memory Usage | {:.2} MB |\n\
|
|
| CPU Usage | {:.2}% |\n\
|
|
| CPU Parallelism | {} |",
|
|
pid_display, self.memory_usage_mb, self.cpu_usage_percent, self.thread_count
|
|
)
|
|
}
|
|
}
|
|
impl fmt::Display for RuntimeInfo {
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
write!(f, "{}", self.format())
|
|
}
|
|
}
|
|
|
|
/// Build information (using shadow-rs generated constants)
|
|
struct BuildInfo;
|
|
|
|
impl BuildInfo {
|
|
fn format() -> String {
|
|
format!(
|
|
"## Build Information\n\n\
|
|
| Property | Value |\n\
|
|
|----------|-------|\n\
|
|
| Version | {} |\n\
|
|
| Build Time | {} |\n\
|
|
| Build Profile | {} |\n\
|
|
| Build OS | {} |\n\
|
|
| Rust Version | {} |\n\
|
|
| Git Branch | {} |\n\
|
|
| Git Commit | {} |\n\
|
|
| Git Tag | {} |\n\
|
|
| Git Status | {} |",
|
|
build::PKG_VERSION,
|
|
build::BUILD_TIME,
|
|
build::BUILD_RUST_CHANNEL,
|
|
build::BUILD_OS,
|
|
build::RUST_VERSION,
|
|
build::BRANCH,
|
|
build::COMMIT_HASH,
|
|
build::TAG,
|
|
build::GIT_STATUS_FILE
|
|
)
|
|
}
|
|
}
|
|
|
|
impl fmt::Display for BuildInfo {
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
write!(f, "{}", Self::format())
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// JSON Output Structures
|
|
// ============================================================================
|
|
|
|
/// Build information for JSON output
|
|
#[derive(Serialize)]
|
|
struct BuildInfoJson {
|
|
version: &'static str,
|
|
build_time: &'static str,
|
|
build_profile: &'static str,
|
|
build_os: &'static str,
|
|
rust_version: &'static str,
|
|
git_branch: &'static str,
|
|
git_commit: &'static str,
|
|
git_tag: &'static str,
|
|
git_status: &'static str,
|
|
}
|
|
|
|
impl BuildInfo {
|
|
fn collect_json() -> BuildInfoJson {
|
|
BuildInfoJson {
|
|
version: build::PKG_VERSION,
|
|
build_time: build::BUILD_TIME,
|
|
build_profile: build::BUILD_RUST_CHANNEL,
|
|
build_os: build::BUILD_OS,
|
|
rust_version: build::RUST_VERSION,
|
|
git_branch: build::BRANCH,
|
|
git_commit: build::COMMIT_HASH,
|
|
git_tag: build::TAG,
|
|
git_status: build::GIT_STATUS_FILE,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Configuration information for JSON output
|
|
#[derive(Serialize)]
|
|
struct ConfigInfoJson {
|
|
address: String,
|
|
console_enable: bool,
|
|
console_address: String,
|
|
region: Option<String>,
|
|
access_key: String,
|
|
obs_endpoint: String,
|
|
tls_path: Option<String>,
|
|
kms_enable: bool,
|
|
kms_backend: String,
|
|
buffer_profile: String,
|
|
workload_profile: Option<WorkloadProfileJson>,
|
|
}
|
|
|
|
/// Workload profile information for JSON output
|
|
#[derive(Serialize)]
|
|
struct WorkloadProfileJson {
|
|
name: String,
|
|
buffer_min_size: usize,
|
|
buffer_max_size: usize,
|
|
default_unknown: usize,
|
|
}
|
|
|
|
fn collect_config_info_json() -> ConfigInfoJson {
|
|
let snapshot = super::get_config_snapshot_for_display();
|
|
|
|
// Get workload profile info
|
|
let workload_profile = {
|
|
use super::workload_profiles::is_buffer_profile_enabled;
|
|
if is_buffer_profile_enabled() {
|
|
let config = current_buffer_config().unwrap_or_default();
|
|
let profile = config.workload_profile();
|
|
let buffer_config = profile.config();
|
|
Some(WorkloadProfileJson {
|
|
name: config.workload_name(),
|
|
buffer_min_size: buffer_config.min_size,
|
|
buffer_max_size: buffer_config.max_size,
|
|
default_unknown: buffer_config.default_unknown,
|
|
})
|
|
} else {
|
|
None
|
|
}
|
|
};
|
|
|
|
ConfigInfoJson {
|
|
address: snapshot.address.clone(),
|
|
console_enable: snapshot.console_enable,
|
|
console_address: snapshot.console_address.clone(),
|
|
region: snapshot.region.clone(),
|
|
access_key: snapshot.access_key.clone(),
|
|
obs_endpoint: snapshot.obs_endpoint.clone(),
|
|
tls_path: snapshot.tls_path.clone(),
|
|
kms_enable: snapshot.kms_enable,
|
|
kms_backend: snapshot.kms_backend.clone(),
|
|
buffer_profile: snapshot.buffer_profile.clone(),
|
|
workload_profile,
|
|
}
|
|
}
|
|
|
|
/// Feature information for JSON output
|
|
#[derive(Serialize)]
|
|
struct FeatureInfoJson {
|
|
name: &'static str,
|
|
enabled: bool,
|
|
description: &'static str,
|
|
}
|
|
|
|
struct FeatureSpec {
|
|
name: &'static str,
|
|
enabled: bool,
|
|
description: &'static str,
|
|
dependencies: &'static str,
|
|
default_enabled: bool,
|
|
}
|
|
|
|
fn feature_specs() -> [FeatureSpec; 7] {
|
|
[
|
|
FeatureSpec {
|
|
name: "metrics-gpu",
|
|
enabled: cfg!(feature = "metrics-gpu"),
|
|
description: "Metrics GPU support",
|
|
dependencies: "rustfs-obs/gpu",
|
|
default_enabled: false,
|
|
},
|
|
FeatureSpec {
|
|
name: "ftps",
|
|
enabled: cfg!(feature = "ftps"),
|
|
description: "FTPS protocol support",
|
|
dependencies: "rustfs-protocols/ftps",
|
|
default_enabled: true,
|
|
},
|
|
FeatureSpec {
|
|
name: "swift",
|
|
enabled: cfg!(feature = "swift"),
|
|
description: "Swift storage backend",
|
|
dependencies: "rustfs-protocols/swift",
|
|
default_enabled: false,
|
|
},
|
|
FeatureSpec {
|
|
name: "webdav",
|
|
enabled: cfg!(feature = "webdav"),
|
|
description: "WebDAV protocol support",
|
|
dependencies: "rustfs-protocols/webdav",
|
|
default_enabled: true,
|
|
},
|
|
FeatureSpec {
|
|
name: "license",
|
|
enabled: cfg!(feature = "license"),
|
|
description: "License validation",
|
|
dependencies: "(none)",
|
|
default_enabled: false,
|
|
},
|
|
FeatureSpec {
|
|
name: "io-scheduler-debug",
|
|
enabled: cfg!(feature = "io-scheduler-debug"),
|
|
description: "Enable debug information in I/O scheduler",
|
|
dependencies: "(none)",
|
|
default_enabled: false,
|
|
},
|
|
FeatureSpec {
|
|
name: "full",
|
|
enabled: cfg!(feature = "full"),
|
|
description: "All features enabled",
|
|
dependencies: "metrics-gpu + ftps + swift + webdav",
|
|
default_enabled: false,
|
|
},
|
|
]
|
|
}
|
|
|
|
/// Dependency information for JSON output
|
|
#[derive(Serialize)]
|
|
struct DepsInfoJson {
|
|
enabled_count: usize,
|
|
total_count: usize,
|
|
features: Vec<FeatureInfoJson>,
|
|
}
|
|
|
|
fn collect_deps_info_json() -> DepsInfoJson {
|
|
let features: Vec<FeatureInfoJson> = feature_specs()
|
|
.into_iter()
|
|
.map(|feature| FeatureInfoJson {
|
|
name: feature.name,
|
|
enabled: feature.enabled,
|
|
description: feature.description,
|
|
})
|
|
.collect();
|
|
|
|
let enabled_count = features.iter().filter(|f| f.enabled).count();
|
|
let total_count = features.len();
|
|
|
|
DepsInfoJson {
|
|
enabled_count,
|
|
total_count,
|
|
features,
|
|
}
|
|
}
|
|
|
|
/// All information combined for JSON output
|
|
#[derive(Serialize)]
|
|
struct AllInfoJson {
|
|
system: SystemInfo,
|
|
runtime: RuntimeInfo,
|
|
build: BuildInfoJson,
|
|
config: ConfigInfoJson,
|
|
deps: DepsInfoJson,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
service_disk: Option<ServiceDiskInfo>,
|
|
}
|
|
|
|
/// Print info as JSON
|
|
fn print_info_json<T: Serialize>(info: &T) {
|
|
match serde_json::to_string_pretty(info) {
|
|
Ok(json) => println!("{}", json),
|
|
Err(e) => eprintln!("Failed to serialize info to JSON: {}", e),
|
|
}
|
|
}
|
|
|
|
/// Execute info command with JSON output
|
|
fn execute_info_json(info_type: Option<InfoType>, volumes: &[String]) {
|
|
match info_type {
|
|
None => {
|
|
// Output all info as nested JSON object
|
|
let all_info = AllInfoJson {
|
|
system: SystemInfo::collect(),
|
|
runtime: RuntimeInfo::collect(),
|
|
build: BuildInfo::collect_json(),
|
|
config: collect_config_info_json(),
|
|
deps: collect_deps_info_json(),
|
|
service_disk: ServiceDiskInfo::collect(volumes),
|
|
};
|
|
print_info_json(&all_info);
|
|
}
|
|
Some(InfoType::System) => {
|
|
print_info_json(&SystemInfo::collect());
|
|
}
|
|
Some(InfoType::Runtime) => {
|
|
print_info_json(&RuntimeInfo::collect());
|
|
}
|
|
Some(InfoType::Build) => {
|
|
print_info_json(&BuildInfo::collect_json());
|
|
}
|
|
Some(InfoType::Config) => {
|
|
print_info_json(&collect_config_info_json());
|
|
}
|
|
Some(InfoType::Deps) => {
|
|
print_info_json(&collect_deps_info_json());
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Configuration information display
|
|
fn format_config_info() -> String {
|
|
// Get config snapshot for display (from global if initialized, otherwise from env)
|
|
let snapshot = super::get_config_snapshot_for_display();
|
|
|
|
// Get workload profile info
|
|
let workload_info = get_workload_profile_info();
|
|
|
|
// Mask the access key for display
|
|
let masked_access_key = &Masked(Some(&snapshot.access_key));
|
|
|
|
let obs_endpoint = if snapshot.obs_endpoint.is_empty() {
|
|
"(not set)"
|
|
} else {
|
|
&snapshot.obs_endpoint
|
|
};
|
|
let protocol_info = format_protocol_config_info();
|
|
|
|
format!(
|
|
"## Configuration Information\n\n\
|
|
| Property | Value |\n\
|
|
|----------|-------|\n\
|
|
| Server Address | {} |\n\
|
|
| Console Enable | {} |\n\
|
|
| Console Address | {} |\n\
|
|
| Region | {} |\n\
|
|
| Access Key | {} |\n\
|
|
| Secret Key | **** |\n\
|
|
| OBS Endpoint | {} |\n\
|
|
| TLS Path | {} |\n\
|
|
| KMS Enabled | {} |\n\
|
|
| KMS Backend | {} |\n\
|
|
| Buffer Profile | {} |\n\
|
|
{}\n\
|
|
{}",
|
|
snapshot.address,
|
|
snapshot.console_enable,
|
|
snapshot.console_address,
|
|
snapshot.region.as_deref().unwrap_or("(not set)"),
|
|
masked_access_key,
|
|
obs_endpoint,
|
|
snapshot.tls_path.as_deref().unwrap_or("(not set)"),
|
|
snapshot.kms_enable,
|
|
snapshot.kms_backend,
|
|
snapshot.buffer_profile,
|
|
workload_info,
|
|
protocol_info
|
|
)
|
|
}
|
|
|
|
fn format_protocol_config_info() -> String {
|
|
const DEFAULT_FTPS_PASSIVE_PORTS: &str = "40000-50000";
|
|
const DEFAULT_WEBDAV_MAX_BODY_SIZE: u64 = 5 * 1024 * 1024 * 1024;
|
|
const DEFAULT_WEBDAV_REQUEST_TIMEOUT_SECS: u64 = 300;
|
|
const DEFAULT_WEBDAV_MAX_CONNECTIONS: usize = 1024;
|
|
|
|
let ftps_enable = rustfs_utils::get_env_bool(rustfs_config::ENV_FTPS_ENABLE, false);
|
|
let ftps_address = rustfs_utils::get_env_str(rustfs_config::ENV_FTPS_ADDRESS, rustfs_config::DEFAULT_FTPS_ADDRESS);
|
|
let ftps_tls_enabled = rustfs_utils::get_env_bool(rustfs_config::ENV_FTPS_TLS_ENABLED, true);
|
|
let ftps_certs_dir =
|
|
rustfs_utils::get_env_opt_str(rustfs_config::ENV_FTPS_CERTS_DIR).unwrap_or_else(|| "(not set)".to_string());
|
|
let ftps_ca_file = rustfs_utils::get_env_opt_str(rustfs_config::ENV_FTPS_CA_FILE).unwrap_or_else(|| "(not set)".to_string());
|
|
let ftps_passive_ports = rustfs_utils::get_env_opt_str(rustfs_config::ENV_FTPS_PASSIVE_PORTS)
|
|
.unwrap_or_else(|| DEFAULT_FTPS_PASSIVE_PORTS.to_string());
|
|
let ftps_external_ip =
|
|
rustfs_utils::get_env_opt_str(rustfs_config::ENV_FTPS_EXTERNAL_IP).unwrap_or_else(|| "(not set)".to_string());
|
|
|
|
let webdav_enable = rustfs_utils::get_env_bool(rustfs_config::ENV_WEBDAV_ENABLE, false);
|
|
let webdav_address = rustfs_utils::get_env_str(rustfs_config::ENV_WEBDAV_ADDRESS, rustfs_config::DEFAULT_WEBDAV_ADDRESS);
|
|
let webdav_tls_enabled = rustfs_utils::get_env_bool(rustfs_config::ENV_WEBDAV_TLS_ENABLED, true);
|
|
let webdav_certs_dir =
|
|
rustfs_utils::get_env_opt_str(rustfs_config::ENV_WEBDAV_CERTS_DIR).unwrap_or_else(|| "(not set)".to_string());
|
|
let webdav_ca_file =
|
|
rustfs_utils::get_env_opt_str(rustfs_config::ENV_WEBDAV_CA_FILE).unwrap_or_else(|| "(not set)".to_string());
|
|
let webdav_max_body_size = rustfs_utils::get_env_u64(rustfs_config::ENV_WEBDAV_MAX_BODY_SIZE, DEFAULT_WEBDAV_MAX_BODY_SIZE);
|
|
let webdav_request_timeout =
|
|
rustfs_utils::get_env_u64(rustfs_config::ENV_WEBDAV_REQUEST_TIMEOUT, DEFAULT_WEBDAV_REQUEST_TIMEOUT_SECS);
|
|
let webdav_max_connections =
|
|
rustfs_utils::get_env_usize(rustfs_config::ENV_WEBDAV_MAX_CONNECTIONS, DEFAULT_WEBDAV_MAX_CONNECTIONS);
|
|
|
|
format!(
|
|
"| FTPS | --- |\n\
|
|
| FTPS > Build Feature | {} |\n\
|
|
| FTPS > Enabled (`{}`) | {} |\n\
|
|
| FTPS > Address (`{}`) | {} |\n\
|
|
| FTPS > TLS Enabled (`{}`) | {} |\n\
|
|
| FTPS > Certs Dir (`{}`) | {} |\n\
|
|
| FTPS > CA File (`{}`) | {} |\n\
|
|
| FTPS > Passive Ports (`{}`) | {} |\n\
|
|
| FTPS > External IP (`{}`) | {} |\n\
|
|
| WebDAV | --- |\n\
|
|
| WebDAV > Build Feature | {} |\n\
|
|
| WebDAV > Enabled (`{}`) | {} |\n\
|
|
| WebDAV > Address (`{}`) | {} |\n\
|
|
| WebDAV > TLS Enabled (`{}`) | {} |\n\
|
|
| WebDAV > Certs Dir (`{}`) | {} |\n\
|
|
| WebDAV > CA File (`{}`) | {} |\n\
|
|
| WebDAV > Max Body Size (`{}`) | {} bytes |\n\
|
|
| WebDAV > Request Timeout (`{}`) | {} seconds |\n\
|
|
| WebDAV > Max Connections (`{}`) | {} |",
|
|
if cfg!(feature = "ftps") { "enabled" } else { "disabled" },
|
|
rustfs_config::ENV_FTPS_ENABLE,
|
|
ftps_enable,
|
|
rustfs_config::ENV_FTPS_ADDRESS,
|
|
ftps_address,
|
|
rustfs_config::ENV_FTPS_TLS_ENABLED,
|
|
ftps_tls_enabled,
|
|
rustfs_config::ENV_FTPS_CERTS_DIR,
|
|
ftps_certs_dir,
|
|
rustfs_config::ENV_FTPS_CA_FILE,
|
|
ftps_ca_file,
|
|
rustfs_config::ENV_FTPS_PASSIVE_PORTS,
|
|
ftps_passive_ports,
|
|
rustfs_config::ENV_FTPS_EXTERNAL_IP,
|
|
ftps_external_ip,
|
|
if cfg!(feature = "webdav") { "enabled" } else { "disabled" },
|
|
rustfs_config::ENV_WEBDAV_ENABLE,
|
|
webdav_enable,
|
|
rustfs_config::ENV_WEBDAV_ADDRESS,
|
|
webdav_address,
|
|
rustfs_config::ENV_WEBDAV_TLS_ENABLED,
|
|
webdav_tls_enabled,
|
|
rustfs_config::ENV_WEBDAV_CERTS_DIR,
|
|
webdav_certs_dir,
|
|
rustfs_config::ENV_WEBDAV_CA_FILE,
|
|
webdav_ca_file,
|
|
rustfs_config::ENV_WEBDAV_MAX_BODY_SIZE,
|
|
webdav_max_body_size,
|
|
rustfs_config::ENV_WEBDAV_REQUEST_TIMEOUT,
|
|
webdav_request_timeout,
|
|
rustfs_config::ENV_WEBDAV_MAX_CONNECTIONS,
|
|
webdav_max_connections
|
|
)
|
|
}
|
|
|
|
/// Get workload profile information from global buffer config
|
|
fn get_workload_profile_info() -> String {
|
|
use super::workload_profiles::is_buffer_profile_enabled;
|
|
|
|
if !is_buffer_profile_enabled() {
|
|
return "| Workload Profile | (disabled) |".to_string();
|
|
}
|
|
|
|
let config = current_buffer_config().unwrap_or_default();
|
|
let profile = config.workload_profile();
|
|
let name = config.workload_name();
|
|
let buffer_config = profile.config();
|
|
|
|
format!(
|
|
"| Workload Profile | {} |\n\
|
|
| Buffer Min Size | {} bytes |\n\
|
|
| Buffer Max Size | {} bytes |\n\
|
|
| Default Unknown | {} bytes |",
|
|
name, buffer_config.min_size, buffer_config.max_size, buffer_config.default_unknown
|
|
)
|
|
}
|
|
|
|
/// Dependency information
|
|
fn format_deps_info() -> String {
|
|
let features = feature_specs();
|
|
|
|
let enabled_count = features.iter().filter(|feature| feature.enabled).count();
|
|
|
|
let mut output = format!(
|
|
"## Build Features\n\n\
|
|
| Property | Value |\n\
|
|
|----------|-------|\n\
|
|
| Enabled Features | {}/{} |\n\n",
|
|
enabled_count,
|
|
features.len()
|
|
);
|
|
|
|
output.push_str("### Feature Status\n\n");
|
|
output.push_str("| Feature | Status | Description |\n");
|
|
output.push_str("|---------|--------|-------------|\n");
|
|
for feature in &features {
|
|
let status = if feature.enabled { "✓" } else { "✗" };
|
|
output.push_str(&format!("| {} | {} | {} |\n", feature.name, status, feature.description));
|
|
}
|
|
|
|
output.push_str("\n### Default Features\n\n");
|
|
output.push_str("| Feature | Note |\n");
|
|
output.push_str("|---------|------|\n");
|
|
for feature in features.iter().filter(|feature| feature.default_enabled) {
|
|
output.push_str(&format!("| {} | enabled by default |\n", feature.name));
|
|
}
|
|
|
|
output.push_str("\n### Feature Dependencies\n\n");
|
|
output.push_str("| Feature | Dependencies |\n");
|
|
output.push_str("|---------|-------------|\n");
|
|
for feature in &features {
|
|
output.push_str(&format!("| {} | {} |\n", feature.name, feature.dependencies));
|
|
}
|
|
|
|
output
|
|
}
|
|
|
|
/// Execute the info command
|
|
pub fn execute_info(opts: &InfoOpts) {
|
|
execute_info_with_volumes(opts, &[])
|
|
}
|
|
|
|
/// Execute info command with volume paths for service disk information
|
|
pub fn execute_info_with_volumes(opts: &InfoOpts, volumes: &[String]) {
|
|
let info_type = if opts.all {
|
|
None // None means display all
|
|
} else {
|
|
opts.info_type
|
|
};
|
|
|
|
// JSON output path
|
|
if opts.json {
|
|
execute_info_json(info_type, volumes);
|
|
return;
|
|
}
|
|
|
|
// Markdown output path (default)
|
|
match info_type {
|
|
None => {
|
|
// Display all information
|
|
println!("{}", SystemInfo::collect());
|
|
println!();
|
|
println!("{}", RuntimeInfo::collect());
|
|
println!();
|
|
println!("{}", BuildInfo);
|
|
println!();
|
|
println!("{}", format_config_info());
|
|
println!();
|
|
// Display service disk info if volumes are configured
|
|
if let Some(service_disk) = ServiceDiskInfo::collect(volumes) {
|
|
println!("{}", service_disk.format());
|
|
println!();
|
|
}
|
|
println!("{}", format_deps_info());
|
|
}
|
|
Some(InfoType::System) => {
|
|
println!("{}", SystemInfo::collect());
|
|
}
|
|
Some(InfoType::Runtime) => {
|
|
println!("{}", RuntimeInfo::collect());
|
|
}
|
|
Some(InfoType::Build) => {
|
|
println!("{}", BuildInfo);
|
|
}
|
|
Some(InfoType::Config) => {
|
|
println!("{}", format_config_info());
|
|
}
|
|
Some(InfoType::Deps) => {
|
|
println!("{}", format_deps_info());
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn test_system_info_collect() {
|
|
let info = SystemInfo::collect();
|
|
assert!(!info.os_type.is_empty());
|
|
assert!(!info.architecture.is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn test_runtime_info_collect() {
|
|
let info = RuntimeInfo::collect();
|
|
assert!(info.process_id > 0);
|
|
}
|
|
|
|
#[test]
|
|
fn test_collect_deps_info_json_matches_cargo_features() {
|
|
let info = collect_deps_info_json();
|
|
let feature_names: Vec<_> = info.features.iter().map(|feature| feature.name).collect();
|
|
|
|
assert_eq!(info.total_count, 7);
|
|
assert_eq!(info.features.len(), 7);
|
|
assert!(feature_names.contains(&"metrics-gpu"));
|
|
assert!(feature_names.contains(&"io-scheduler-debug"));
|
|
assert!(!feature_names.contains(&"manual-test-runners"));
|
|
assert!(!feature_names.contains(&"metrics"));
|
|
assert!(!feature_names.contains(&"direct-io"));
|
|
}
|
|
|
|
#[test]
|
|
fn test_format_deps_info_matches_cargo_feature_output() {
|
|
let output = format_deps_info();
|
|
|
|
assert!(output.contains("| metrics-gpu |"));
|
|
assert!(output.contains("| io-scheduler-debug |"));
|
|
assert!(!output.contains("| manual-test-runners |"));
|
|
assert!(output.contains("| ftps | enabled by default |"));
|
|
assert!(output.contains("| webdav | enabled by default |"));
|
|
assert!(output.contains("| full | metrics-gpu + ftps + swift + webdav |"));
|
|
assert!(!output.contains("| direct-io |"));
|
|
}
|
|
|
|
#[test]
|
|
fn test_format_config_info_includes_ftps_and_webdav_subitems() {
|
|
let output = format_config_info();
|
|
|
|
assert!(output.contains("| FTPS | --- |"));
|
|
assert!(output.contains("| FTPS > Enabled (`RUSTFS_FTPS_ENABLE`) |"));
|
|
assert!(output.contains("| FTPS > Address (`RUSTFS_FTPS_ADDRESS`) |"));
|
|
assert!(output.contains("| FTPS > Passive Ports (`RUSTFS_FTPS_PASSIVE_PORTS`) |"));
|
|
assert!(output.contains("| WebDAV | --- |"));
|
|
assert!(output.contains("| WebDAV > Enabled (`RUSTFS_WEBDAV_ENABLE`) |"));
|
|
assert!(output.contains("| WebDAV > Address (`RUSTFS_WEBDAV_ADDRESS`) |"));
|
|
assert!(output.contains("| WebDAV > Max Body Size (`RUSTFS_WEBDAV_MAX_BODY_SIZE`) |"));
|
|
}
|
|
}
|