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:
@@ -206,6 +206,7 @@ func main() {
|
||||
dashboard.POST("/agents/:id/update", agentHandler.TriggerUpdate)
|
||||
dashboard.POST("/agents/:id/heartbeat", agentHandler.TriggerHeartbeat)
|
||||
dashboard.GET("/agents/:id/heartbeat", agentHandler.GetHeartbeatStatus)
|
||||
dashboard.POST("/agents/:id/reboot", agentHandler.TriggerReboot)
|
||||
dashboard.GET("/updates", updateHandler.ListUpdates)
|
||||
dashboard.GET("/updates/:id", updateHandler.GetUpdate)
|
||||
dashboard.GET("/updates/:id/logs", updateHandler.GetUpdateLogs)
|
||||
|
||||
@@ -79,6 +79,7 @@ func (h *AgentHandler) RegisterAgent(c *gin.Context) {
|
||||
OSVersion: req.OSVersion,
|
||||
OSArchitecture: req.OSArchitecture,
|
||||
AgentVersion: req.AgentVersion,
|
||||
CurrentVersion: req.AgentVersion,
|
||||
LastSeen: time.Now(),
|
||||
Status: "online",
|
||||
Metadata: models.JSONB{},
|
||||
@@ -966,3 +967,63 @@ func (h *AgentHandler) SetRapidPollingMode(c *gin.Context) {
|
||||
"rapid_polling_until": rapidPollingUntil,
|
||||
})
|
||||
}
|
||||
|
||||
// TriggerReboot triggers a system reboot for an agent
|
||||
func (h *AgentHandler) TriggerReboot(c *gin.Context) {
|
||||
agentID, err := uuid.Parse(c.Param("id"))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid agent ID"})
|
||||
return
|
||||
}
|
||||
|
||||
// Check if agent exists
|
||||
agent, err := h.agentQueries.GetAgentByID(agentID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "agent not found"})
|
||||
return
|
||||
}
|
||||
|
||||
// Parse request body for optional parameters
|
||||
var req struct {
|
||||
DelayMinutes int `json:"delay_minutes"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
c.ShouldBindJSON(&req)
|
||||
|
||||
// Default to 1 minute delay if not specified
|
||||
if req.DelayMinutes == 0 {
|
||||
req.DelayMinutes = 1
|
||||
}
|
||||
if req.Message == "" {
|
||||
req.Message = "Reboot requested by RedFlag"
|
||||
}
|
||||
|
||||
// Create reboot command
|
||||
cmd := &models.AgentCommand{
|
||||
ID: uuid.New(),
|
||||
AgentID: agentID,
|
||||
CommandType: models.CommandTypeReboot,
|
||||
Params: models.JSONB{
|
||||
"delay_minutes": req.DelayMinutes,
|
||||
"message": req.Message,
|
||||
},
|
||||
Status: models.CommandStatusPending,
|
||||
CreatedAt: time.Now(),
|
||||
}
|
||||
|
||||
// Save command to database
|
||||
if err := h.commandQueries.CreateCommand(cmd); err != nil {
|
||||
log.Printf("Failed to create reboot command: %v", err)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to create reboot command"})
|
||||
return
|
||||
}
|
||||
|
||||
log.Printf("Reboot command created for agent %s (%s)", agent.Hostname, agentID)
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"message": "reboot command sent",
|
||||
"command_id": cmd.ID,
|
||||
"agent_id": agentID,
|
||||
"hostname": agent.Hostname,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
-- Add reboot tracking fields to agents table
|
||||
ALTER TABLE agents
|
||||
ADD COLUMN reboot_required BOOLEAN DEFAULT FALSE,
|
||||
ADD COLUMN last_reboot_at TIMESTAMP,
|
||||
ADD COLUMN reboot_reason TEXT;
|
||||
|
||||
-- Add index for efficient querying of agents needing reboot
|
||||
CREATE INDEX idx_agents_reboot_required ON agents(reboot_required) WHERE reboot_required = TRUE;
|
||||
|
||||
-- Add comment for documentation
|
||||
COMMENT ON COLUMN agents.reboot_required IS 'Whether the agent host requires a reboot to complete updates';
|
||||
COMMENT ON COLUMN agents.last_reboot_at IS 'Timestamp of the last system reboot';
|
||||
COMMENT ON COLUMN agents.reboot_reason IS 'Reason why reboot is required (e.g., kernel update, library updates)';
|
||||
@@ -204,3 +204,30 @@ func (q *AgentQueries) GetActiveAgentCount() (int, error) {
|
||||
err := q.db.Get(&count, query)
|
||||
return count, err
|
||||
}
|
||||
|
||||
// UpdateAgentRebootStatus updates the reboot status for an agent
|
||||
func (q *AgentQueries) UpdateAgentRebootStatus(id uuid.UUID, required bool, reason string) error {
|
||||
query := `
|
||||
UPDATE agents
|
||||
SET reboot_required = $1,
|
||||
reboot_reason = $2,
|
||||
updated_at = $3
|
||||
WHERE id = $4
|
||||
`
|
||||
_, err := q.db.Exec(query, required, reason, time.Now(), id)
|
||||
return err
|
||||
}
|
||||
|
||||
// UpdateAgentLastReboot updates the last reboot timestamp for an agent
|
||||
func (q *AgentQueries) UpdateAgentLastReboot(id uuid.UUID, rebootTime time.Time) error {
|
||||
query := `
|
||||
UPDATE agents
|
||||
SET last_reboot_at = $1,
|
||||
reboot_required = FALSE,
|
||||
reboot_reason = '',
|
||||
updated_at = $2
|
||||
WHERE id = $3
|
||||
`
|
||||
_, err := q.db.Exec(query, rebootTime, time.Now(), id)
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -19,11 +19,14 @@ type Agent struct {
|
||||
CurrentVersion string `json:"current_version" db:"current_version"` // Current running version
|
||||
UpdateAvailable bool `json:"update_available" db:"update_available"` // Whether update is available
|
||||
LastVersionCheck time.Time `json:"last_version_check" db:"last_version_check"` // Last time version was checked
|
||||
LastSeen time.Time `json:"last_seen" db:"last_seen"`
|
||||
Status string `json:"status" db:"status"`
|
||||
Metadata JSONB `json:"metadata" db:"metadata"`
|
||||
CreatedAt time.Time `json:"created_at" db:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at" db:"updated_at"`
|
||||
LastSeen time.Time `json:"last_seen" db:"last_seen"`
|
||||
Status string `json:"status" db:"status"`
|
||||
Metadata JSONB `json:"metadata" db:"metadata"`
|
||||
RebootRequired bool `json:"reboot_required" db:"reboot_required"`
|
||||
LastRebootAt *time.Time `json:"last_reboot_at" db:"last_reboot_at"`
|
||||
RebootReason string `json:"reboot_reason" db:"reboot_reason"`
|
||||
CreatedAt time.Time `json:"created_at" db:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at" db:"updated_at"`
|
||||
}
|
||||
|
||||
// AgentWithLastScan extends Agent with last scan information
|
||||
@@ -40,6 +43,9 @@ type AgentWithLastScan struct {
|
||||
LastSeen time.Time `json:"last_seen" db:"last_seen"`
|
||||
Status string `json:"status" db:"status"`
|
||||
Metadata JSONB `json:"metadata" db:"metadata"`
|
||||
RebootRequired bool `json:"reboot_required" db:"reboot_required"`
|
||||
LastRebootAt *time.Time `json:"last_reboot_at" db:"last_reboot_at"`
|
||||
RebootReason string `json:"reboot_reason" db:"reboot_reason"`
|
||||
CreatedAt time.Time `json:"created_at" db:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at" db:"updated_at"`
|
||||
LastScan *time.Time `json:"last_scan" db:"last_scan"`
|
||||
|
||||
@@ -50,6 +50,7 @@ const (
|
||||
CommandTypeUpdateAgent = "update_agent"
|
||||
CommandTypeEnableHeartbeat = "enable_heartbeat"
|
||||
CommandTypeDisableHeartbeat = "disable_heartbeat"
|
||||
CommandTypeReboot = "reboot"
|
||||
)
|
||||
|
||||
// Command statuses
|
||||
|
||||
Reference in New Issue
Block a user