feat: add host restart detection and fix agent version display
Potential fixes for issues #4 and #6. Agent version display: - Set CurrentVersion during registration instead of waiting for first check-in - Update UI to show "Initial Registration" instead of "Unknown" Host restart detection: - Added reboot_required, last_reboot_at, reboot_reason fields to agents table - Agent now detects pending reboots (Debian/Ubuntu via /var/run/reboot-required, RHEL/Fedora via needs-restarting) - New reboot command type with 1-minute grace period - UI shows restart alerts and adds restart button in quick actions - Restart indicator badge in agent list The reboot detection runs during system info collection and gets reported back to the server automatically. Using shutdown command for now until we make the restart mechanism user-adjustable later - need to think on that. Also need to come up with a Windows derivative outside of reading event log for detecting reboots.
This commit is contained in:
@@ -6,6 +6,7 @@ import (
|
||||
"log"
|
||||
"math/rand"
|
||||
"os"
|
||||
"os/exec"
|
||||
"runtime"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -532,6 +533,11 @@ func runAgent(cfg *config.Config) error {
|
||||
log.Printf("[Heartbeat] Error disabling heartbeat: %v\n", err)
|
||||
}
|
||||
|
||||
|
||||
case "reboot":
|
||||
if err := handleReboot(apiClient, cfg, cmd.ID, cmd.Params); err != nil {
|
||||
log.Printf("[Reboot] Error processing reboot command: %v\n", err)
|
||||
}
|
||||
default:
|
||||
log.Printf("Unknown command type: %s\n", cmd.Type)
|
||||
}
|
||||
@@ -1400,6 +1406,94 @@ func reportSystemInfo(apiClient *client.Client, cfg *config.Config) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// handleReboot handles reboot command
|
||||
func handleReboot(apiClient *client.Client, cfg *config.Config, commandID string, params map[string]interface{}) error {
|
||||
log.Println("[Reboot] Processing reboot request...")
|
||||
|
||||
// Parse parameters
|
||||
delayMinutes := 1 // Default to 1 minute
|
||||
message := "System reboot requested by RedFlag"
|
||||
|
||||
if delay, ok := params["delay_minutes"]; ok {
|
||||
if delayFloat, ok := delay.(float64); ok {
|
||||
delayMinutes = int(delayFloat)
|
||||
}
|
||||
}
|
||||
if msg, ok := params["message"].(string); ok && msg != "" {
|
||||
message = msg
|
||||
}
|
||||
|
||||
log.Printf("[Reboot] Scheduling system reboot in %d minute(s): %s", delayMinutes, message)
|
||||
|
||||
var cmd *exec.Cmd
|
||||
|
||||
// Execute platform-specific reboot command
|
||||
if runtime.GOOS == "linux" {
|
||||
// Linux: shutdown -r +MINUTES "message"
|
||||
cmd = exec.Command("shutdown", "-r", fmt.Sprintf("+%d", delayMinutes), message)
|
||||
} else if runtime.GOOS == "windows" {
|
||||
// Windows: shutdown /r /t SECONDS /c "message"
|
||||
delaySeconds := delayMinutes * 60
|
||||
cmd = exec.Command("shutdown", "/r", "/t", fmt.Sprintf("%d", delaySeconds), "/c", message)
|
||||
} else {
|
||||
err := fmt.Errorf("reboot not supported on platform: %s", runtime.GOOS)
|
||||
log.Printf("[Reboot] Error: %v", err)
|
||||
|
||||
// Report failure
|
||||
logReport := client.LogReport{
|
||||
CommandID: commandID,
|
||||
Action: "reboot",
|
||||
Result: "failed",
|
||||
Stdout: "",
|
||||
Stderr: err.Error(),
|
||||
ExitCode: 1,
|
||||
DurationSeconds: 0,
|
||||
}
|
||||
apiClient.ReportLog(cfg.AgentID, logReport)
|
||||
return err
|
||||
}
|
||||
|
||||
// Execute reboot command
|
||||
output, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
log.Printf("[Reboot] Failed to schedule reboot: %v", err)
|
||||
log.Printf("[Reboot] Output: %s", string(output))
|
||||
|
||||
// Report failure
|
||||
logReport := client.LogReport{
|
||||
CommandID: commandID,
|
||||
Action: "reboot",
|
||||
Result: "failed",
|
||||
Stdout: string(output),
|
||||
Stderr: err.Error(),
|
||||
ExitCode: 1,
|
||||
DurationSeconds: 0,
|
||||
}
|
||||
apiClient.ReportLog(cfg.AgentID, logReport)
|
||||
return err
|
||||
}
|
||||
|
||||
log.Printf("[Reboot] System reboot scheduled successfully")
|
||||
log.Printf("[Reboot] The system will reboot in %d minute(s)", delayMinutes)
|
||||
|
||||
// Report success
|
||||
logReport := client.LogReport{
|
||||
CommandID: commandID,
|
||||
Action: "reboot",
|
||||
Result: "success",
|
||||
Stdout: fmt.Sprintf("System reboot scheduled for %d minute(s) from now. Message: %s", delayMinutes, message),
|
||||
Stderr: "",
|
||||
ExitCode: 0,
|
||||
DurationSeconds: 0,
|
||||
}
|
||||
|
||||
if reportErr := apiClient.ReportLog(cfg.AgentID, logReport); reportErr != nil {
|
||||
log.Printf("[Reboot] Failed to report reboot command result: %v", reportErr)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// formatTimeSince formats a duration as "X time ago"
|
||||
func formatTimeSince(t time.Time) string {
|
||||
duration := time.Since(t)
|
||||
|
||||
@@ -21,6 +21,8 @@ type SystemInfo struct {
|
||||
DiskInfo []DiskInfo `json:"disk_info"`
|
||||
RunningProcesses int `json:"running_processes"`
|
||||
Uptime string `json:"uptime"`
|
||||
RebootRequired bool `json:"reboot_required"`
|
||||
RebootReason string `json:"reboot_reason"`
|
||||
Metadata map[string]string `json:"metadata"`
|
||||
}
|
||||
|
||||
@@ -113,6 +115,11 @@ func GetSystemInfo(agentVersion string) (*SystemInfo, error) {
|
||||
}
|
||||
}
|
||||
|
||||
// Check if system requires reboot
|
||||
rebootRequired, rebootReason := checkRebootRequired()
|
||||
info.RebootRequired = rebootRequired
|
||||
info.RebootReason = rebootReason
|
||||
|
||||
// Add collection timestamp
|
||||
info.Metadata["collected_at"] = time.Now().Format(time.RFC3339)
|
||||
|
||||
@@ -467,4 +474,64 @@ func GetLightweightMetrics() (*LightweightMetrics, error) {
|
||||
// In the future, we could add a background goroutine to track CPU usage
|
||||
|
||||
return metrics, nil
|
||||
}
|
||||
}
|
||||
// checkRebootRequired checks if the system requires a reboot
|
||||
func checkRebootRequired() (bool, string) {
|
||||
if runtime.GOOS == "linux" {
|
||||
return checkLinuxRebootRequired()
|
||||
} else if runtime.GOOS == "windows" {
|
||||
return checkWindowsRebootRequired()
|
||||
}
|
||||
return false, ""
|
||||
}
|
||||
|
||||
// checkLinuxRebootRequired checks if a Linux system requires a reboot
|
||||
func checkLinuxRebootRequired() (bool, string) {
|
||||
// Method 1: Check Debian/Ubuntu reboot-required file
|
||||
if err := exec.Command("test", "-f", "/var/run/reboot-required").Run(); err == nil {
|
||||
// File exists, reboot is required
|
||||
// Try to read the packages that require reboot
|
||||
if output, err := exec.Command("cat", "/var/run/reboot-required.pkgs").Output(); err == nil {
|
||||
packages := strings.TrimSpace(string(output))
|
||||
if packages != "" {
|
||||
// Truncate if too long
|
||||
if len(packages) > 200 {
|
||||
packages = packages[:200] + "..."
|
||||
}
|
||||
return true, "Packages: " + packages
|
||||
}
|
||||
}
|
||||
return true, "System updates require reboot"
|
||||
}
|
||||
|
||||
// Method 2: Check RHEL/Fedora/Rocky using needs-restarting
|
||||
cmd := exec.Command("needs-restarting", "-r")
|
||||
if err := cmd.Run(); err != nil {
|
||||
if exitErr, ok := err.(*exec.ExitError); ok {
|
||||
// Exit code 1 means reboot is needed
|
||||
if exitErr.ExitCode() == 1 {
|
||||
return true, "Kernel or system libraries updated"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false, ""
|
||||
}
|
||||
|
||||
// checkWindowsRebootRequired checks if a Windows system requires a reboot
|
||||
func checkWindowsRebootRequired() (bool, string) {
|
||||
// Check Windows Update pending reboot registry keys
|
||||
// HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Auto Update\RebootRequired
|
||||
cmd := exec.Command("reg", "query", "HKLM\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\WindowsUpdate\\Auto Update\\RebootRequired")
|
||||
if err := cmd.Run(); err == nil {
|
||||
return true, "Windows updates require reboot"
|
||||
}
|
||||
|
||||
// Check Component Based Servicing pending reboot
|
||||
cmd = exec.Command("reg", "query", "HKLM\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Component Based Servicing\\RebootPending")
|
||||
if err := cmd.Run(); err == nil {
|
||||
return true, "Component updates require reboot"
|
||||
}
|
||||
|
||||
return false, ""
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user