Compare commits

...

6 Commits

Author SHA1 Message Date
60ef712fac Fix hash conversion in NixOS update workflow
All checks were successful
Build and Release / build-and-release (push) Successful in 2m38s
- Replace xxd with Python for hex to base64 conversion
- Use standard tools available in GitHub Actions runners
- Should fix hash conversion error in automated workflow
2025-10-25 17:24:37 +02:00
1ed4666dfd Add automated NixOS configuration updates to release workflow
Some checks failed
Build and Release / build-and-release (push) Failing after 1m22s
- Clone nixosbox repository after creating release
- Download and hash new tarball automatically
- Update version and hash in cm-dashboard.nix
- Commit and push changes with automated message
- Eliminates manual NixOS config update step
2025-10-25 17:21:52 +02:00
59d260680e Integrate smart collector into metrics manager
All checks were successful
Build and Release / build-and-release (push) Successful in 1m54s
- Add SmartCollector import and initialization
- Enable in both normal and benchmark modes
- Fixes infinite smartctl loop issue by properly managing collector
- Smart collector now active when config.smart.enabled = true
2025-10-25 17:14:54 +02:00
9160fac80b Fix smart collector compilation errors
- Update to match current Metric structure
- Use correct Status enum and collector interface
- Fix MetricValue types and constructor usage
- Builds successfully with warnings only
2025-10-25 17:13:04 +02:00
83cb43bcf1 Restore missing smart collector implementation
Some checks failed
Build and Release / build-and-release (push) Failing after 1m24s
- Rewrite smart collector to match current architecture
- Add back to mod.rs exports
- Fixes infinite smartctl loop issue
- Uses simple health and temperature monitoring
2025-10-25 16:59:09 +02:00
b310206f1f Document automated binary release system
- Replace source build instructions with release workflow
- Document tag-based release process with Gitea Actions
- Include NixOS config update process for releases
- Highlight benefits of static binary approach
2025-10-25 16:36:07 +02:00
5 changed files with 262 additions and 34 deletions

View File

@@ -91,4 +91,38 @@ jobs:
curl -X POST \
-H "Authorization: token $GITEA_TOKEN" \
-F "attachment=@release/cm-dashboard-linux-x86_64.tar.gz" \
"https://gitea.cmtec.se/api/v1/repos/cm/cm-dashboard/releases/$RELEASE_ID/assets?name=cm-dashboard-linux-x86_64.tar.gz"
"https://gitea.cmtec.se/api/v1/repos/cm/cm-dashboard/releases/$RELEASE_ID/assets?name=cm-dashboard-linux-x86_64.tar.gz"
- name: Update NixOS Configuration
env:
GITEA_TOKEN: ${{ secrets.GITEATOKEN }}
run: |
VERSION="${{ steps.version.outputs.VERSION }}"
# Clone nixosbox repository
git clone https://$GITEA_TOKEN@gitea.cmtec.se/cm/nixosbox.git nixosbox-update
cd nixosbox-update
# Get hash for the new release tarball
TARBALL_URL="https://gitea.cmtec.se/cm/cm-dashboard/releases/download/$VERSION/cm-dashboard-linux-x86_64.tar.gz"
# Download tarball to get correct hash
curl -L -o cm-dashboard.tar.gz "$TARBALL_URL"
# Convert sha256 hex to base64 for Nix hash format using Python
NEW_HASH=$(sha256sum cm-dashboard.tar.gz | cut -d' ' -f1)
NIX_HASH="sha256-$(python3 -c "import base64, binascii; print(base64.b64encode(binascii.unhexlify('$NEW_HASH')).decode())")"
# Update the NixOS configuration
sed -i "s/version = \"v[^\"]*\"/version = \"$VERSION\"/" hosts/common/cm-dashboard.nix
sed -i "s/sha256 = \"sha256-[^\"]*\"/sha256 = \"$NIX_HASH\"/" hosts/common/cm-dashboard.nix
# Commit and push changes
git config user.name "Gitea Actions"
git config user.email "actions@gitea.cmtec.se"
git add hosts/common/cm-dashboard.nix
git commit -m "Auto-update cm-dashboard to $VERSION
- Update version to $VERSION with automated release
- Update tarball hash for new static binaries
- Automated update from cm-dashboard release workflow"
git push

View File

@@ -295,60 +295,61 @@ Development: ~/projects/nixosbox → git commit → git push
Deployment: git pull → /var/lib/cm-dashboard/nixos-config → rebuild
```
## NixOS Configuration Updates
## Automated Binary Release System
When code changes are made to cm-dashboard, the NixOS configuration at `~/projects/nixosbox` must be updated to deploy the changes.
**IMPLEMENTED:** cm-dashboard now uses automated binary releases instead of source builds.
### Update Process
### Release Workflow
1. **Get Latest Commit Hash**
1. **Automated Release Creation**
- Gitea Actions workflow builds static binaries on tag push
- Creates release with `cm-dashboard-linux-x86_64.tar.gz` tarball
- No manual intervention required for binary generation
2. **Creating New Releases**
```bash
git log -1 --format="%H"
cd ~/projects/cm-dashboard
git tag v0.1.X
git push origin v0.1.X
```
This automatically:
- Builds static binaries with `RUSTFLAGS="-C target-feature=+crt-static"`
- Creates GitHub-style release with tarball
- Uploads binaries via Gitea API
2. **Update NixOS Configuration**
3. **NixOS Configuration Updates**
Edit `~/projects/nixosbox/hosts/common/cm-dashboard.nix`:
```nix
src = pkgs.fetchgit {
url = "https://gitea.cmtec.se/cm/cm-dashboard.git";
rev = "NEW_COMMIT_HASH_HERE";
sha256 = "sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="; # Placeholder
version = "v0.1.X";
src = pkgs.fetchurl {
url = "https://gitea.cmtec.se/cm/cm-dashboard/releases/download/${version}/cm-dashboard-linux-x86_64.tar.gz";
sha256 = "sha256-NEW_HASH_HERE";
};
```
3. **Get Correct Source Hash**
Build with placeholder hash to get the actual hash:
4. **Get Release Hash**
```bash
cd ~/projects/nixosbox
nix-build --no-out-link -E 'with import <nixpkgs> {}; fetchgit {
url = "https://gitea.cmtec.se/cm/cm-dashboard.git";
rev = "NEW_COMMIT_HASH";
nix-build --no-out-link -E 'with import <nixpkgs> {}; fetchurl {
url = "https://gitea.cmtec.se/cm/cm-dashboard/releases/download/v0.1.X/cm-dashboard-linux-x86_64.tar.gz";
sha256 = "sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=";
}' 2>&1 | grep "got:"
```
Example output:
```
error: hash mismatch in fixed-output derivation '/nix/store/...':
specified: sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=
got: sha256-x8crxNusOUYRrkP9mYEOG+Ga3JCPIdJLkEAc5P1ZxdQ=
```
4. **Update Configuration with Correct Hash**
Replace the placeholder with the hash from the error message (the "got:" line).
5. **Commit NixOS Configuration**
5. **Commit and Deploy**
```bash
cd ~/projects/nixosbox
git add hosts/common/cm-dashboard.nix
git commit -m "Update cm-dashboard to latest version (SHORT_HASH)"
git commit -m "Update cm-dashboard to v0.1.X with static binaries"
git push
```
6. **Rebuild System**
The user handles the system rebuild step - this cannot be automated.
### Benefits
- **No compilation overhead** on each host
- **Consistent static binaries** across all hosts
- **Faster deployments** - download vs compile
- **No library dependency issues** - static linking
- **Automated pipeline** - tag push triggers everything

View File

@@ -8,6 +8,7 @@ pub mod disk;
pub mod error;
pub mod memory;
pub mod nixos;
pub mod smart;
pub mod systemd;
pub use error::CollectorError;

View File

@@ -0,0 +1,178 @@
use async_trait::async_trait;
use cm_dashboard_shared::{Metric, MetricValue, Status, StatusTracker};
use std::process::Stdio;
use tokio::process::Command;
use tracing::{debug, warn};
use super::{Collector, CollectorError};
pub struct SmartCollector;
impl SmartCollector {
/// Get list of storage devices to monitor
async fn get_devices(&self) -> Result<Vec<String>, CollectorError> {
let output = Command::new("lsblk")
.args(["-d", "-n", "-o", "NAME,TYPE"])
.stdout(Stdio::piped())
.stderr(Stdio::null())
.output()
.await
.map_err(|e| CollectorError::SystemRead {
path: "lsblk".to_string(),
error: e.to_string()
})?;
if !output.status.success() {
return Ok(Vec::new()); // Return empty if lsblk fails
}
let stdout = String::from_utf8_lossy(&output.stdout);
let mut devices = Vec::new();
for line in stdout.lines() {
let parts: Vec<&str> = line.split_whitespace().collect();
if parts.len() >= 2 && parts[1] == "disk" {
let device_name = parts[0];
if device_name.starts_with("nvme") || device_name.starts_with("sd") {
devices.push(format!("/dev/{}", device_name));
}
}
}
Ok(devices)
}
/// Collect SMART data for a single device
async fn collect_device_smart(&self, device: &str, status_tracker: &mut StatusTracker) -> Result<Vec<Metric>, CollectorError> {
debug!("Collecting SMART data for device: {}", device);
let output = Command::new("sudo")
.args(["smartctl", "-H", "-A", device]) // Health and attributes only
.stdout(Stdio::piped())
.stderr(Stdio::null())
.output()
.await
.map_err(|e| CollectorError::SystemRead {
path: "lsblk".to_string(),
error: e.to_string()
})?;
if !output.status.success() {
warn!("smartctl failed for device: {}", device);
return Ok(Vec::new());
}
let stdout = String::from_utf8_lossy(&output.stdout);
self.parse_smart_output(device, &stdout, status_tracker)
}
/// Parse smartctl output and create metrics
fn parse_smart_output(&self, device: &str, output: &str, status_tracker: &mut StatusTracker) -> Result<Vec<Metric>, CollectorError> {
let mut metrics = Vec::new();
let device_name = device.trim_start_matches("/dev/");
let mut health_ok = true;
let mut temperature: Option<f32> = None;
for line in output.lines() {
let line = line.trim();
// Parse health status
if line.contains("SMART overall-health self-assessment") {
if line.contains("FAILED") {
health_ok = false;
}
}
// Parse temperature from various formats
if (line.contains("Temperature") || line.contains("Airflow_Temperature")) && temperature.is_none() {
if let Some(temp) = self.extract_temperature(line) {
temperature = Some(temp);
}
}
}
// Create health metric
let health_status = if health_ok {
Status::Ok
} else {
Status::Critical
};
metrics.push(Metric::new(
format!("smart_health_{}", device_name),
MetricValue::String(if health_ok { "PASSED".to_string() } else { "FAILED".to_string() }),
health_status,
));
// Create temperature metric if available
if let Some(temp) = temperature {
let temp_status = if temp >= 70.0 {
Status::Critical
} else if temp >= 60.0 {
Status::Warning
} else {
Status::Ok
};
metrics.push(Metric::new(
format!("smart_temperature_{}", device_name),
MetricValue::Float(temp),
temp_status,
).with_unit("celsius".to_string()));
}
debug!("Collected {} SMART metrics for {}", metrics.len(), device);
Ok(metrics)
}
/// Extract temperature value from smartctl output line
fn extract_temperature(&self, line: &str) -> Option<f32> {
let parts: Vec<&str> = line.split_whitespace().collect();
for (i, part) in parts.iter().enumerate() {
if let Ok(temp) = part.parse::<f32>() {
// Check if this looks like a temperature value (reasonable range)
if temp > 0.0 && temp < 150.0 {
// Check context around the number
if i + 1 < parts.len() {
let next = parts[i + 1].to_lowercase();
if next.contains("celsius") || next.contains("°c") || next == "c" {
return Some(temp);
}
}
// For SMART attribute lines, temperature is often the 10th column
if parts.len() >= 10 && (line.contains("Temperature") || line.contains("Airflow_Temperature")) {
return Some(temp);
}
}
}
}
None
}
}
#[async_trait]
impl Collector for SmartCollector {
async fn collect(&self, status_tracker: &mut StatusTracker) -> Result<Vec<Metric>, CollectorError> {
debug!("Starting SMART data collection");
let devices = self.get_devices().await?;
let mut all_metrics = Vec::new();
for device in devices {
match self.collect_device_smart(&device, status_tracker).await {
Ok(mut metrics) => {
all_metrics.append(&mut metrics);
}
Err(e) => {
warn!("Failed to collect SMART data for {}: {}", device, e);
// Continue with other devices
}
}
}
debug!("Collected {} total SMART metrics", all_metrics.len());
Ok(all_metrics)
}
}

View File

@@ -4,7 +4,7 @@ use tracing::{error, info};
use crate::collectors::{
backup::BackupCollector, cpu::CpuCollector, disk::DiskCollector, memory::MemoryCollector,
nixos::NixOSCollector, systemd::SystemdCollector, Collector,
nixos::NixOSCollector, smart::SmartCollector, systemd::SystemdCollector, Collector,
};
use crate::config::{AgentConfig, CollectorConfig};
@@ -61,6 +61,14 @@ impl MetricCollectionManager {
info!("BENCHMARK: Backup collector only");
}
}
Some("smart") => {
// SMART collector only
if config.smart.enabled {
let smart_collector = SmartCollector;
collectors.push(Box::new(smart_collector));
info!("BENCHMARK: SMART collector only");
}
}
Some("none") => {
// No collectors - test agent loop only
info!("BENCHMARK: No collectors enabled");
@@ -101,6 +109,12 @@ impl MetricCollectionManager {
collectors.push(Box::new(nixos_collector));
info!("NixOS collector initialized");
}
if config.smart.enabled {
let smart_collector = SmartCollector;
collectors.push(Box::new(smart_collector));
info!("SMART collector initialized");
}
}
}