Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| db0e41a7d3 | |||
| ec460496d8 | |||
| 33e700529e | |||
| d644b7d40a | |||
| f635ba9c75 | |||
| 76b6e3373e |
@@ -113,13 +113,13 @@ jobs:
|
|||||||
NIX_HASH="sha256-$(python3 -c "import base64, binascii; print(base64.b64encode(binascii.unhexlify('$NEW_HASH')).decode())")"
|
NIX_HASH="sha256-$(python3 -c "import base64, binascii; print(base64.b64encode(binascii.unhexlify('$NEW_HASH')).decode())")"
|
||||||
|
|
||||||
# Update the NixOS configuration
|
# Update the NixOS configuration
|
||||||
sed -i "s|version = \"v[^\"]*\"|version = \"$VERSION\"|" hosts/common/cm-dashboard.nix
|
sed -i "s|version = \"v[^\"]*\"|version = \"$VERSION\"|" hosts/services/cm-dashboard.nix
|
||||||
sed -i "s|sha256 = \"sha256-[^\"]*\"|sha256 = \"$NIX_HASH\"|" hosts/common/cm-dashboard.nix
|
sed -i "s|sha256 = \"sha256-[^\"]*\"|sha256 = \"$NIX_HASH\"|" hosts/services/cm-dashboard.nix
|
||||||
|
|
||||||
# Commit and push changes
|
# Commit and push changes
|
||||||
git config user.name "Gitea Actions"
|
git config user.name "Gitea Actions"
|
||||||
git config user.email "actions@gitea.cmtec.se"
|
git config user.email "actions@gitea.cmtec.se"
|
||||||
git add hosts/common/cm-dashboard.nix
|
git add hosts/services/cm-dashboard.nix
|
||||||
git commit -m "Auto-update cm-dashboard to $VERSION
|
git commit -m "Auto-update cm-dashboard to $VERSION
|
||||||
|
|
||||||
- Update version to $VERSION with automated release
|
- Update version to $VERSION with automated release
|
||||||
|
|||||||
@@ -115,7 +115,7 @@ This automatically:
|
|||||||
- Uploads binaries via Gitea API
|
- Uploads binaries via Gitea API
|
||||||
|
|
||||||
### NixOS Configuration Updates
|
### NixOS Configuration Updates
|
||||||
Edit `~/projects/nixosbox/hosts/common/cm-dashboard.nix`:
|
Edit `~/projects/nixosbox/hosts/services/cm-dashboard.nix`:
|
||||||
|
|
||||||
```nix
|
```nix
|
||||||
version = "v0.1.X";
|
version = "v0.1.X";
|
||||||
|
|||||||
6
Cargo.lock
generated
6
Cargo.lock
generated
@@ -270,7 +270,7 @@ checksum = "a1d728cc89cf3aee9ff92b05e62b19ee65a02b5702cff7d5a377e32c6ae29d8d"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cm-dashboard"
|
name = "cm-dashboard"
|
||||||
version = "0.1.67"
|
version = "0.1.72"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"chrono",
|
"chrono",
|
||||||
@@ -292,7 +292,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cm-dashboard-agent"
|
name = "cm-dashboard-agent"
|
||||||
version = "0.1.67"
|
version = "0.1.72"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"async-trait",
|
"async-trait",
|
||||||
@@ -315,7 +315,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cm-dashboard-shared"
|
name = "cm-dashboard-shared"
|
||||||
version = "0.1.67"
|
version = "0.1.72"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"chrono",
|
"chrono",
|
||||||
"serde",
|
"serde",
|
||||||
|
|||||||
@@ -329,7 +329,7 @@ This triggers automated:
|
|||||||
- Tarball upload to Gitea
|
- Tarball upload to Gitea
|
||||||
|
|
||||||
### NixOS Integration
|
### NixOS Integration
|
||||||
Update `~/projects/nixosbox/hosts/common/cm-dashboard.nix`:
|
Update `~/projects/nixosbox/hosts/services/cm-dashboard.nix`:
|
||||||
|
|
||||||
```nix
|
```nix
|
||||||
version = "v0.1.43";
|
version = "v0.1.43";
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "cm-dashboard-agent"
|
name = "cm-dashboard-agent"
|
||||||
version = "0.1.68"
|
version = "0.1.73"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
|||||||
@@ -351,36 +351,40 @@ impl Agent {
|
|||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
|
|
||||||
let output = tokio::process::Command::new("sudo")
|
// Spawn the systemctl command asynchronously to avoid blocking the agent
|
||||||
.arg("systemctl")
|
let service_name_clone = service_name.to_string();
|
||||||
.arg(action_str)
|
let action_str_clone = action_str.to_string();
|
||||||
.arg(format!("{}.service", service_name))
|
|
||||||
.output()
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
|
tokio::spawn(async move {
|
||||||
|
let result = tokio::process::Command::new("sudo")
|
||||||
|
.arg("systemctl")
|
||||||
|
.arg(&action_str_clone)
|
||||||
|
.arg(format!("{}.service", service_name_clone))
|
||||||
|
.output()
|
||||||
|
.await;
|
||||||
|
|
||||||
|
match result {
|
||||||
|
Ok(output) => {
|
||||||
if output.status.success() {
|
if output.status.success() {
|
||||||
info!("Service {} {} completed successfully", service_name, action_str);
|
info!("Service {} {} completed successfully", service_name_clone, action_str_clone);
|
||||||
if !output.stdout.is_empty() {
|
if !output.stdout.is_empty() {
|
||||||
debug!("stdout: {}", String::from_utf8_lossy(&output.stdout));
|
debug!("stdout: {}", String::from_utf8_lossy(&output.stdout));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Note: User-stopped flag will be cleared by systemd collector
|
|
||||||
// when service actually reaches 'active' state, not here
|
|
||||||
} else {
|
} else {
|
||||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||||
error!("Service {} {} failed: {}", service_name, action_str, stderr);
|
error!("Service {} {} failed: {}", service_name_clone, action_str_clone, stderr);
|
||||||
return Err(anyhow::anyhow!("systemctl {} {} failed: {}", action_str, service_name, stderr));
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
error!("Failed to execute systemctl {} {}: {}", action_str_clone, service_name_clone, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Force refresh metrics after service control to update service status
|
info!("Service {} {} command initiated (non-blocking)", service_name, action_str);
|
||||||
if matches!(action, ServiceAction::Start | ServiceAction::Stop | ServiceAction::UserStart | ServiceAction::UserStop) {
|
|
||||||
info!("Triggering immediate metric refresh after service control");
|
// Note: Service status will be updated by the normal metric collection cycle
|
||||||
if let Err(e) = self.collect_metrics_only().await {
|
// once the systemctl operation completes
|
||||||
error!("Failed to refresh metrics after service control: {}", e);
|
|
||||||
} else {
|
|
||||||
info!("Service status refreshed immediately after {} {}", action_str, service_name);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "cm-dashboard"
|
name = "cm-dashboard"
|
||||||
version = "0.1.68"
|
version = "0.1.73"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
|||||||
@@ -134,12 +134,6 @@ impl Dashboard {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Send a command to a specific agent
|
|
||||||
pub async fn send_command(&mut self, hostname: &str, command: AgentCommand) -> Result<()> {
|
|
||||||
self.zmq_command_sender
|
|
||||||
.send_command(hostname, command)
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn run(&mut self) -> Result<()> {
|
pub async fn run(&mut self) -> Result<()> {
|
||||||
info!("Starting dashboard main loop");
|
info!("Starting dashboard main loop");
|
||||||
@@ -212,35 +206,19 @@ impl Dashboard {
|
|||||||
metric_message.metrics.len()
|
metric_message.metrics.len()
|
||||||
);
|
);
|
||||||
|
|
||||||
// Check if this is the first time we've seen this host
|
// Track first contact with host (no command needed - agent sends data every 2s)
|
||||||
let is_new_host = !self
|
let is_new_host = !self
|
||||||
.initial_commands_sent
|
.initial_commands_sent
|
||||||
.contains(&metric_message.hostname);
|
.contains(&metric_message.hostname);
|
||||||
|
|
||||||
if is_new_host {
|
if is_new_host {
|
||||||
info!(
|
info!(
|
||||||
"First contact with host {}, sending initial CollectNow command",
|
"First contact with host {} - data will update automatically",
|
||||||
metric_message.hostname
|
|
||||||
);
|
|
||||||
|
|
||||||
// Send CollectNow command for immediate refresh
|
|
||||||
if let Err(e) = self
|
|
||||||
.send_command(&metric_message.hostname, AgentCommand::CollectNow)
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
error!(
|
|
||||||
"Failed to send initial CollectNow command to {}: {}",
|
|
||||||
metric_message.hostname, e
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
info!(
|
|
||||||
"✓ Sent initial CollectNow command to {}",
|
|
||||||
metric_message.hostname
|
metric_message.hostname
|
||||||
);
|
);
|
||||||
self.initial_commands_sent
|
self.initial_commands_sent
|
||||||
.insert(metric_message.hostname.clone());
|
.insert(metric_message.hostname.clone());
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Update metric store
|
// Update metric store
|
||||||
self.metric_store
|
self.metric_store
|
||||||
|
|||||||
@@ -84,6 +84,7 @@ impl ZmqConsumer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/// Connect to predefined hosts using their configuration
|
/// Connect to predefined hosts using their configuration
|
||||||
pub async fn connect_to_predefined_hosts(&mut self, hosts: &std::collections::HashMap<String, crate::config::HostDetails>) -> Result<()> {
|
pub async fn connect_to_predefined_hosts(&mut self, hosts: &std::collections::HashMap<String, crate::config::HostDetails>) -> Result<()> {
|
||||||
let default_port = self.config.subscriber_ports[0];
|
let default_port = self.config.subscriber_ports[0];
|
||||||
@@ -104,27 +105,13 @@ impl ZmqConsumer {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Connect to a host using its configuration details with fallback support
|
/// Connect to a host using its configuration details
|
||||||
pub async fn connect_to_host_with_details(&mut self, hostname: &str, host_details: &crate::config::HostDetails, port: u16) -> Result<()> {
|
pub async fn connect_to_host_with_details(&mut self, hostname: &str, host_details: &crate::config::HostDetails, port: u16) -> Result<()> {
|
||||||
// Get primary connection IP
|
// Get primary connection IP only - no fallbacks
|
||||||
let primary_ip = host_details.get_connection_ip(hostname);
|
let primary_ip = host_details.get_connection_ip(hostname);
|
||||||
|
|
||||||
// Try primary connection
|
// Connect directly without fallback attempts
|
||||||
if let Ok(()) = self.connect_to_host(&primary_ip, port).await {
|
self.connect_to_host(&primary_ip, port).await
|
||||||
info!("Connected to {} via primary address: {}", hostname, primary_ip);
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Try fallback IPs if primary fails
|
|
||||||
let fallbacks = host_details.get_fallback_ips(hostname);
|
|
||||||
for fallback_ip in fallbacks {
|
|
||||||
if let Ok(()) = self.connect_to_host(&fallback_ip, port).await {
|
|
||||||
info!("Connected to {} via fallback address: {}", hostname, fallback_ip);
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Err(anyhow::anyhow!("Failed to connect to {} using all available addresses", hostname))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Receive command output from any connected agent (non-blocking)
|
/// Receive command output from any connected agent (non-blocking)
|
||||||
|
|||||||
@@ -31,75 +31,15 @@ pub struct HostDetails {
|
|||||||
pub mac_address: Option<String>,
|
pub mac_address: Option<String>,
|
||||||
/// Primary IP address (local network)
|
/// Primary IP address (local network)
|
||||||
pub ip: Option<String>,
|
pub ip: Option<String>,
|
||||||
/// Tailscale network IP address
|
|
||||||
pub tailscale_ip: Option<String>,
|
|
||||||
/// Preferred connection type: "local", "tailscale", or "auto" (fallback)
|
|
||||||
#[serde(default = "default_connection_type")]
|
|
||||||
pub connection_type: String,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn default_connection_type() -> String {
|
|
||||||
"auto".to_string()
|
|
||||||
}
|
|
||||||
|
|
||||||
impl HostDetails {
|
impl HostDetails {
|
||||||
/// Get the preferred IP address for connection based on connection_type
|
/// Get the IP address for connection (uses ip field or hostname as fallback)
|
||||||
pub fn get_connection_ip(&self, hostname: &str) -> String {
|
pub fn get_connection_ip(&self, hostname: &str) -> String {
|
||||||
match self.connection_type.as_str() {
|
|
||||||
"tailscale" => {
|
|
||||||
if let Some(ref ts_ip) = self.tailscale_ip {
|
|
||||||
ts_ip.clone()
|
|
||||||
} else {
|
|
||||||
// Fallback to local IP or hostname
|
|
||||||
self.ip.as_ref().unwrap_or(&hostname.to_string()).clone()
|
self.ip.as_ref().unwrap_or(&hostname.to_string()).clone()
|
||||||
}
|
}
|
||||||
}
|
|
||||||
"local" => {
|
|
||||||
if let Some(ref local_ip) = self.ip {
|
|
||||||
local_ip.clone()
|
|
||||||
} else {
|
|
||||||
hostname.to_string()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"auto" | _ => {
|
|
||||||
// Try tailscale first, then local, then hostname
|
|
||||||
if let Some(ref ts_ip) = self.tailscale_ip {
|
|
||||||
ts_ip.clone()
|
|
||||||
} else if let Some(ref local_ip) = self.ip {
|
|
||||||
local_ip.clone()
|
|
||||||
} else {
|
|
||||||
hostname.to_string()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get fallback IP addresses for connection retry
|
|
||||||
pub fn get_fallback_ips(&self, hostname: &str) -> Vec<String> {
|
|
||||||
let mut fallbacks = Vec::new();
|
|
||||||
|
|
||||||
// Add all available IPs except the primary one
|
|
||||||
let primary = self.get_connection_ip(hostname);
|
|
||||||
|
|
||||||
if let Some(ref ts_ip) = self.tailscale_ip {
|
|
||||||
if ts_ip != &primary {
|
|
||||||
fallbacks.push(ts_ip.clone());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(ref local_ip) = self.ip {
|
|
||||||
if local_ip != &primary {
|
|
||||||
fallbacks.push(local_ip.clone());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Always include hostname as final fallback if not already primary
|
|
||||||
if hostname != primary {
|
|
||||||
fallbacks.push(hostname.to_string());
|
|
||||||
}
|
|
||||||
|
|
||||||
fallbacks
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// System configuration
|
/// System configuration
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "cm-dashboard-shared"
|
name = "cm-dashboard-shared"
|
||||||
version = "0.1.68"
|
version = "0.1.73"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
|||||||
Reference in New Issue
Block a user