Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1ed4666dfd | |||
| 59d260680e | |||
| 9160fac80b | |||
| 83cb43bcf1 | |||
| b310206f1f |
@@ -92,3 +92,36 @@ jobs:
|
|||||||
-H "Authorization: token $GITEA_TOKEN" \
|
-H "Authorization: token $GITEA_TOKEN" \
|
||||||
-F "attachment=@release/cm-dashboard-linux-x86_64.tar.gz" \
|
-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"
|
||||||
|
NEW_HASH=$(sha256sum cm-dashboard.tar.gz | cut -d' ' -f1)
|
||||||
|
NIX_HASH="sha256-$(echo -n $NEW_HASH | xxd -r -p | base64)"
|
||||||
|
|
||||||
|
# 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
|
||||||
65
CLAUDE.md
65
CLAUDE.md
@@ -295,60 +295,61 @@ Development: ~/projects/nixosbox → git commit → git push
|
|||||||
Deployment: git pull → /var/lib/cm-dashboard/nixos-config → rebuild
|
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
|
```bash
|
||||||
git log -1 --format="%H"
|
cd ~/projects/cm-dashboard
|
||||||
|
git tag v0.1.X
|
||||||
|
git push origin v0.1.X
|
||||||
```
|
```
|
||||||
|
|
||||||
2. **Update NixOS Configuration**
|
This automatically:
|
||||||
|
- Builds static binaries with `RUSTFLAGS="-C target-feature=+crt-static"`
|
||||||
|
- Creates GitHub-style release with tarball
|
||||||
|
- Uploads binaries via Gitea API
|
||||||
|
|
||||||
|
3. **NixOS Configuration Updates**
|
||||||
Edit `~/projects/nixosbox/hosts/common/cm-dashboard.nix`:
|
Edit `~/projects/nixosbox/hosts/common/cm-dashboard.nix`:
|
||||||
|
|
||||||
```nix
|
```nix
|
||||||
src = pkgs.fetchgit {
|
version = "v0.1.X";
|
||||||
url = "https://gitea.cmtec.se/cm/cm-dashboard.git";
|
src = pkgs.fetchurl {
|
||||||
rev = "NEW_COMMIT_HASH_HERE";
|
url = "https://gitea.cmtec.se/cm/cm-dashboard/releases/download/${version}/cm-dashboard-linux-x86_64.tar.gz";
|
||||||
sha256 = "sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="; # Placeholder
|
sha256 = "sha256-NEW_HASH_HERE";
|
||||||
};
|
};
|
||||||
```
|
```
|
||||||
|
|
||||||
3. **Get Correct Source Hash**
|
4. **Get Release Hash**
|
||||||
Build with placeholder hash to get the actual hash:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd ~/projects/nixosbox
|
cd ~/projects/nixosbox
|
||||||
nix-build --no-out-link -E 'with import <nixpkgs> {}; fetchgit {
|
nix-build --no-out-link -E 'with import <nixpkgs> {}; fetchurl {
|
||||||
url = "https://gitea.cmtec.se/cm/cm-dashboard.git";
|
url = "https://gitea.cmtec.se/cm/cm-dashboard/releases/download/v0.1.X/cm-dashboard-linux-x86_64.tar.gz";
|
||||||
rev = "NEW_COMMIT_HASH";
|
|
||||||
sha256 = "sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=";
|
sha256 = "sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=";
|
||||||
}' 2>&1 | grep "got:"
|
}' 2>&1 | grep "got:"
|
||||||
```
|
```
|
||||||
|
|
||||||
Example output:
|
5. **Commit and Deploy**
|
||||||
|
|
||||||
```
|
|
||||||
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**
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd ~/projects/nixosbox
|
cd ~/projects/nixosbox
|
||||||
git add hosts/common/cm-dashboard.nix
|
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
|
git push
|
||||||
```
|
```
|
||||||
|
|
||||||
6. **Rebuild System**
|
### Benefits
|
||||||
The user handles the system rebuild step - this cannot be automated.
|
|
||||||
|
- **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
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ pub mod disk;
|
|||||||
pub mod error;
|
pub mod error;
|
||||||
pub mod memory;
|
pub mod memory;
|
||||||
pub mod nixos;
|
pub mod nixos;
|
||||||
|
pub mod smart;
|
||||||
pub mod systemd;
|
pub mod systemd;
|
||||||
|
|
||||||
pub use error::CollectorError;
|
pub use error::CollectorError;
|
||||||
|
|||||||
178
agent/src/collectors/smart.rs
Normal file
178
agent/src/collectors/smart.rs
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,7 +4,7 @@ use tracing::{error, info};
|
|||||||
|
|
||||||
use crate::collectors::{
|
use crate::collectors::{
|
||||||
backup::BackupCollector, cpu::CpuCollector, disk::DiskCollector, memory::MemoryCollector,
|
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};
|
use crate::config::{AgentConfig, CollectorConfig};
|
||||||
|
|
||||||
@@ -61,6 +61,14 @@ impl MetricCollectionManager {
|
|||||||
info!("BENCHMARK: Backup collector only");
|
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") => {
|
Some("none") => {
|
||||||
// No collectors - test agent loop only
|
// No collectors - test agent loop only
|
||||||
info!("BENCHMARK: No collectors enabled");
|
info!("BENCHMARK: No collectors enabled");
|
||||||
@@ -101,6 +109,12 @@ impl MetricCollectionManager {
|
|||||||
collectors.push(Box::new(nixos_collector));
|
collectors.push(Box::new(nixos_collector));
|
||||||
info!("NixOS collector initialized");
|
info!("NixOS collector initialized");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if config.smart.enabled {
|
||||||
|
let smart_collector = SmartCollector;
|
||||||
|
collectors.push(Box::new(smart_collector));
|
||||||
|
info!("SMART collector initialized");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user