feat: granular subsystem commands with parallel scanner execution

Split monolithic scan_updates into individual subsystems (updates/storage/system/docker).
Scanners now run in parallel via goroutines - cuts scan time roughly in half, maybe more.

Agent changes:
- Orchestrator pattern for scanner management
- New scanners: storage (disk metrics), system (cpu/mem/processes)
- New commands: scan_storage, scan_system, scan_docker
- Wrapped existing scanners (APT/DNF/Docker/Windows/Winget) with common interface
- Version bump to 0.1.20

Server changes:
- Migration 015: agent_subsystems table with trigger for auto-init
- Subsystem CRUD: enable/disable, interval (5min-24hr), auto-run toggle
- API routes: /api/v1/agents/:id/subsystems/* (9 endpoints)
- Stats tracking per subsystem

Web UI changes:
- ChatTimeline shows subsystem-specific labels and icons
- AgentScanners got interactive toggles, interval dropdowns, manual trigger buttons
- TypeScript types added for subsystems

Backward compatible with legacy scan_updates - for now. Bugs probably exist somewhere.
This commit is contained in:
Fimeg
2025-11-01 20:34:00 -04:00
parent bf4d46529f
commit 3690472396
19 changed files with 2151 additions and 253 deletions

View File

@@ -256,8 +256,8 @@ func (h *AgentHandler) GetCommands(c *gin.Context) {
}
}
// Update agent with new metadata
if err := h.agentQueries.UpdateAgent(agent); err != nil {
// Update agent with new metadata (preserve version tracking)
if err := h.agentQueries.UpdateAgentMetadata(agentID, agent.Metadata, agent.Status, time.Now()); err != nil {
log.Printf("Warning: Failed to update agent metrics: %v", err)
}
}
@@ -269,21 +269,7 @@ func (h *AgentHandler) GetCommands(c *gin.Context) {
return
}
// Process command acknowledgments if agent sent any
var acknowledgedIDs []string
if metrics != nil && len(metrics.PendingAcknowledgments) > 0 {
// Verify which commands from the agent's pending list have been recorded
verified, err := h.commandQueries.VerifyCommandsCompleted(metrics.PendingAcknowledgments)
if err != nil {
log.Printf("Warning: Failed to verify command acknowledgments for agent %s: %v", agentID, err)
} else {
acknowledgedIDs = verified
if len(acknowledgedIDs) > 0 {
log.Printf("Acknowledged %d command results for agent %s", len(acknowledgedIDs), agentID)
}
}
}
// Process heartbeat metadata from agent check-ins
if metrics.Metadata != nil {
agent, err := h.agentQueries.GetAgentByID(agentID)
@@ -454,7 +440,7 @@ func (h *AgentHandler) GetCommands(c *gin.Context) {
response := models.CommandsResponse{
Commands: commandItems,
RapidPolling: rapidPolling,
AcknowledgedIDs: acknowledgedIDs,
AcknowledgedIDs: []string{}, // No acknowledgments in current implementation
}
c.JSON(http.StatusOK, response)

View File

@@ -0,0 +1,327 @@
package handlers
import (
"net/http"
"github.com/Fimeg/RedFlag/aggregator-server/internal/database/queries"
"github.com/Fimeg/RedFlag/aggregator-server/internal/models"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
)
type SubsystemHandler struct {
subsystemQueries *queries.SubsystemQueries
commandQueries *queries.CommandQueries
}
func NewSubsystemHandler(sq *queries.SubsystemQueries, cq *queries.CommandQueries) *SubsystemHandler {
return &SubsystemHandler{
subsystemQueries: sq,
commandQueries: cq,
}
}
// GetSubsystems retrieves all subsystems for an agent
// GET /api/v1/agents/:id/subsystems
func (h *SubsystemHandler) GetSubsystems(c *gin.Context) {
agentID, err := uuid.Parse(c.Param("id"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid agent ID"})
return
}
subsystems, err := h.subsystemQueries.GetSubsystems(agentID)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to retrieve subsystems"})
return
}
c.JSON(http.StatusOK, subsystems)
}
// GetSubsystem retrieves a specific subsystem for an agent
// GET /api/v1/agents/:id/subsystems/:subsystem
func (h *SubsystemHandler) GetSubsystem(c *gin.Context) {
agentID, err := uuid.Parse(c.Param("id"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid agent ID"})
return
}
subsystem := c.Param("subsystem")
if subsystem == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "Subsystem name required"})
return
}
sub, err := h.subsystemQueries.GetSubsystem(agentID, subsystem)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to retrieve subsystem"})
return
}
if sub == nil {
c.JSON(http.StatusNotFound, gin.H{"error": "Subsystem not found"})
return
}
c.JSON(http.StatusOK, sub)
}
// UpdateSubsystem updates subsystem configuration
// PATCH /api/v1/agents/:id/subsystems/:subsystem
func (h *SubsystemHandler) UpdateSubsystem(c *gin.Context) {
agentID, err := uuid.Parse(c.Param("id"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid agent ID"})
return
}
subsystem := c.Param("subsystem")
if subsystem == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "Subsystem name required"})
return
}
var config models.SubsystemConfig
if err := c.ShouldBindJSON(&config); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
// Validate interval if provided
if config.IntervalMinutes != nil && (*config.IntervalMinutes < 5 || *config.IntervalMinutes > 1440) {
c.JSON(http.StatusBadRequest, gin.H{"error": "Interval must be between 5 and 1440 minutes"})
return
}
err = h.subsystemQueries.UpdateSubsystem(agentID, subsystem, config)
if err != nil {
if err.Error() == "subsystem not found" {
c.JSON(http.StatusNotFound, gin.H{"error": "Subsystem not found"})
return
}
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update subsystem"})
return
}
c.JSON(http.StatusOK, gin.H{"message": "Subsystem updated successfully"})
}
// EnableSubsystem enables a subsystem
// POST /api/v1/agents/:id/subsystems/:subsystem/enable
func (h *SubsystemHandler) EnableSubsystem(c *gin.Context) {
agentID, err := uuid.Parse(c.Param("id"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid agent ID"})
return
}
subsystem := c.Param("subsystem")
if subsystem == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "Subsystem name required"})
return
}
err = h.subsystemQueries.EnableSubsystem(agentID, subsystem)
if err != nil {
if err.Error() == "subsystem not found" {
c.JSON(http.StatusNotFound, gin.H{"error": "Subsystem not found"})
return
}
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to enable subsystem"})
return
}
c.JSON(http.StatusOK, gin.H{"message": "Subsystem enabled successfully"})
}
// DisableSubsystem disables a subsystem
// POST /api/v1/agents/:id/subsystems/:subsystem/disable
func (h *SubsystemHandler) DisableSubsystem(c *gin.Context) {
agentID, err := uuid.Parse(c.Param("id"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid agent ID"})
return
}
subsystem := c.Param("subsystem")
if subsystem == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "Subsystem name required"})
return
}
err = h.subsystemQueries.DisableSubsystem(agentID, subsystem)
if err != nil {
if err.Error() == "subsystem not found" {
c.JSON(http.StatusNotFound, gin.H{"error": "Subsystem not found"})
return
}
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to disable subsystem"})
return
}
c.JSON(http.StatusOK, gin.H{"message": "Subsystem disabled successfully"})
}
// TriggerSubsystem manually triggers a subsystem scan
// POST /api/v1/agents/:id/subsystems/:subsystem/trigger
func (h *SubsystemHandler) TriggerSubsystem(c *gin.Context) {
agentID, err := uuid.Parse(c.Param("id"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid agent ID"})
return
}
subsystem := c.Param("subsystem")
if subsystem == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "Subsystem name required"})
return
}
// Verify subsystem exists and is enabled
sub, err := h.subsystemQueries.GetSubsystem(agentID, subsystem)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to retrieve subsystem"})
return
}
if sub == nil {
c.JSON(http.StatusNotFound, gin.H{"error": "Subsystem not found"})
return
}
if !sub.Enabled {
c.JSON(http.StatusBadRequest, gin.H{"error": "Subsystem is disabled"})
return
}
// Create command for the subsystem
commandType := "scan_" + subsystem
command := &models.AgentCommand{
AgentID: agentID,
CommandType: commandType,
Status: "pending",
Source: "web_ui", // Manual trigger from UI
}
err = h.commandQueries.CreateCommand(command)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create command"})
return
}
c.JSON(http.StatusOK, gin.H{
"message": "Subsystem scan triggered successfully",
"command_id": command.ID,
})
}
// GetSubsystemStats retrieves statistics for a subsystem
// GET /api/v1/agents/:id/subsystems/:subsystem/stats
func (h *SubsystemHandler) GetSubsystemStats(c *gin.Context) {
agentID, err := uuid.Parse(c.Param("id"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid agent ID"})
return
}
subsystem := c.Param("subsystem")
if subsystem == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "Subsystem name required"})
return
}
stats, err := h.subsystemQueries.GetSubsystemStats(agentID, subsystem)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to retrieve subsystem stats"})
return
}
if stats == nil {
c.JSON(http.StatusNotFound, gin.H{"error": "Subsystem not found"})
return
}
c.JSON(http.StatusOK, stats)
}
// SetAutoRun enables or disables auto-run for a subsystem
// POST /api/v1/agents/:id/subsystems/:subsystem/auto-run
func (h *SubsystemHandler) SetAutoRun(c *gin.Context) {
agentID, err := uuid.Parse(c.Param("id"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid agent ID"})
return
}
subsystem := c.Param("subsystem")
if subsystem == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "Subsystem name required"})
return
}
var req struct {
AutoRun bool `json:"auto_run"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
err = h.subsystemQueries.SetAutoRun(agentID, subsystem, req.AutoRun)
if err != nil {
if err.Error() == "subsystem not found" {
c.JSON(http.StatusNotFound, gin.H{"error": "Subsystem not found"})
return
}
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update auto-run"})
return
}
c.JSON(http.StatusOK, gin.H{"message": "Auto-run updated successfully"})
}
// SetInterval sets the interval for a subsystem
// POST /api/v1/agents/:id/subsystems/:subsystem/interval
func (h *SubsystemHandler) SetInterval(c *gin.Context) {
agentID, err := uuid.Parse(c.Param("id"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid agent ID"})
return
}
subsystem := c.Param("subsystem")
if subsystem == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "Subsystem name required"})
return
}
var req struct {
IntervalMinutes int `json:"interval_minutes"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
// Validate interval
if req.IntervalMinutes < 5 || req.IntervalMinutes > 1440 {
c.JSON(http.StatusBadRequest, gin.H{"error": "Interval must be between 5 and 1440 minutes"})
return
}
err = h.subsystemQueries.SetInterval(agentID, subsystem, req.IntervalMinutes)
if err != nil {
if err.Error() == "subsystem not found" {
c.JSON(http.StatusNotFound, gin.H{"error": "Subsystem not found"})
return
}
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update interval"})
return
}
c.JSON(http.StatusOK, gin.H{"message": "Interval updated successfully"})
}

View File

@@ -0,0 +1,17 @@
-- Migration: 013_agent_subsystems (down)
-- Purpose: Rollback agent subsystems table
-- Version: 0.1.20
-- Date: 2025-11-01
-- Drop trigger and function
DROP TRIGGER IF EXISTS trigger_create_default_subsystems ON agents;
DROP FUNCTION IF EXISTS create_default_subsystems();
-- Drop indexes
DROP INDEX IF EXISTS idx_agent_subsystems_lookup;
DROP INDEX IF EXISTS idx_agent_subsystems_subsystem;
DROP INDEX IF EXISTS idx_agent_subsystems_next_run;
DROP INDEX IF EXISTS idx_agent_subsystems_agent;
-- Drop table
DROP TABLE IF EXISTS agent_subsystems;

View File

@@ -0,0 +1,81 @@
-- Migration: 013_agent_subsystems
-- Purpose: Add agent subsystems table for granular command scheduling and management
-- Version: 0.1.20
-- Date: 2025-11-01
-- Create agent_subsystems table for tracking individual subsystem configurations per agent
CREATE TABLE IF NOT EXISTS agent_subsystems (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
agent_id UUID NOT NULL REFERENCES agents(id) ON DELETE CASCADE,
subsystem VARCHAR(50) NOT NULL,
enabled BOOLEAN DEFAULT true,
interval_minutes INTEGER DEFAULT 15,
auto_run BOOLEAN DEFAULT false,
last_run_at TIMESTAMP,
next_run_at TIMESTAMP,
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW(),
UNIQUE(agent_id, subsystem)
);
-- Create indexes for efficient querying
CREATE INDEX IF NOT EXISTS idx_agent_subsystems_agent ON agent_subsystems(agent_id);
CREATE INDEX IF NOT EXISTS idx_agent_subsystems_next_run ON agent_subsystems(next_run_at)
WHERE enabled = true AND auto_run = true;
CREATE INDEX IF NOT EXISTS idx_agent_subsystems_subsystem ON agent_subsystems(subsystem);
-- Create a composite index for common queries (agent + subsystem)
CREATE INDEX IF NOT EXISTS idx_agent_subsystems_lookup ON agent_subsystems(agent_id, subsystem, enabled);
-- Default subsystems for existing agents
-- Only insert for agents that don't already have subsystems configured
INSERT INTO agent_subsystems (agent_id, subsystem, enabled, interval_minutes, auto_run)
SELECT id, 'updates', true, 15, false FROM agents
WHERE NOT EXISTS (
SELECT 1 FROM agent_subsystems WHERE agent_subsystems.agent_id = agents.id AND subsystem = 'updates'
)
UNION ALL
SELECT id, 'storage', true, 15, false FROM agents
WHERE NOT EXISTS (
SELECT 1 FROM agent_subsystems WHERE agent_subsystems.agent_id = agents.id AND subsystem = 'storage'
)
UNION ALL
SELECT id, 'system', true, 30, false FROM agents
WHERE NOT EXISTS (
SELECT 1 FROM agent_subsystems WHERE agent_subsystems.agent_id = agents.id AND subsystem = 'system'
)
UNION ALL
SELECT id, 'docker', false, 15, false FROM agents
WHERE NOT EXISTS (
SELECT 1 FROM agent_subsystems WHERE agent_subsystems.agent_id = agents.id AND subsystem = 'docker'
);
-- Create trigger to automatically insert default subsystems for new agents
CREATE OR REPLACE FUNCTION create_default_subsystems()
RETURNS TRIGGER AS $$
BEGIN
-- Insert default subsystems for new agent
INSERT INTO agent_subsystems (agent_id, subsystem, enabled, interval_minutes, auto_run)
VALUES
(NEW.id, 'updates', true, 15, false),
(NEW.id, 'storage', true, 15, false),
(NEW.id, 'system', true, 30, false),
(NEW.id, 'docker', false, 15, false);
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER trigger_create_default_subsystems
AFTER INSERT ON agents
FOR EACH ROW
EXECUTE FUNCTION create_default_subsystems();
-- Add comment for documentation
COMMENT ON TABLE agent_subsystems IS 'Per-agent subsystem configurations for granular command scheduling';
COMMENT ON COLUMN agent_subsystems.subsystem IS 'Subsystem name: updates, storage, system, docker';
COMMENT ON COLUMN agent_subsystems.enabled IS 'Whether this subsystem is enabled for the agent';
COMMENT ON COLUMN agent_subsystems.interval_minutes IS 'How often to run this subsystem (in minutes)';
COMMENT ON COLUMN agent_subsystems.auto_run IS 'Whether the server should auto-schedule this subsystem';
COMMENT ON COLUMN agent_subsystems.last_run_at IS 'Last time this subsystem was executed';
COMMENT ON COLUMN agent_subsystems.next_run_at IS 'Next scheduled run time for auto-run subsystems';

View File

@@ -67,6 +67,20 @@ func (q *AgentQueries) UpdateAgent(agent *models.Agent) error {
return err
}
// UpdateAgentMetadata updates only the metadata, last_seen, and status fields
// Used for metrics updates to avoid overwriting version tracking
func (q *AgentQueries) UpdateAgentMetadata(id uuid.UUID, metadata models.JSONB, status string, lastSeen time.Time) error {
query := `
UPDATE agents SET
last_seen = $1,
status = $2,
metadata = $3
WHERE id = $4
`
_, err := q.db.Exec(query, lastSeen, status, metadata, id)
return err
}
// ListAgents returns all agents with optional filtering
func (q *AgentQueries) ListAgents(status, osType string) ([]models.Agent, error) {
var agents []models.Agent

View File

@@ -0,0 +1,293 @@
package queries
import (
"database/sql"
"fmt"
"github.com/Fimeg/RedFlag/aggregator-server/internal/models"
"github.com/google/uuid"
"github.com/jmoiron/sqlx"
)
type SubsystemQueries struct {
db *sqlx.DB
}
func NewSubsystemQueries(db *sqlx.DB) *SubsystemQueries {
return &SubsystemQueries{db: db}
}
// GetSubsystems retrieves all subsystems for an agent
func (q *SubsystemQueries) GetSubsystems(agentID uuid.UUID) ([]models.AgentSubsystem, error) {
query := `
SELECT id, agent_id, subsystem, enabled, interval_minutes, auto_run,
last_run_at, next_run_at, created_at, updated_at
FROM agent_subsystems
WHERE agent_id = $1
ORDER BY subsystem
`
var subsystems []models.AgentSubsystem
err := q.db.Select(&subsystems, query, agentID)
if err != nil {
return nil, fmt.Errorf("failed to get subsystems: %w", err)
}
return subsystems, nil
}
// GetSubsystem retrieves a specific subsystem for an agent
func (q *SubsystemQueries) GetSubsystem(agentID uuid.UUID, subsystem string) (*models.AgentSubsystem, error) {
query := `
SELECT id, agent_id, subsystem, enabled, interval_minutes, auto_run,
last_run_at, next_run_at, created_at, updated_at
FROM agent_subsystems
WHERE agent_id = $1 AND subsystem = $2
`
var sub models.AgentSubsystem
err := q.db.Get(&sub, query, agentID, subsystem)
if err == sql.ErrNoRows {
return nil, nil
}
if err != nil {
return nil, fmt.Errorf("failed to get subsystem: %w", err)
}
return &sub, nil
}
// UpdateSubsystem updates a subsystem configuration
func (q *SubsystemQueries) UpdateSubsystem(agentID uuid.UUID, subsystem string, config models.SubsystemConfig) error {
// Build dynamic update query based on provided fields
updates := []string{}
args := []interface{}{agentID, subsystem}
argIdx := 3
if config.Enabled != nil {
updates = append(updates, fmt.Sprintf("enabled = $%d", argIdx))
args = append(args, *config.Enabled)
argIdx++
}
if config.IntervalMinutes != nil {
updates = append(updates, fmt.Sprintf("interval_minutes = $%d", argIdx))
args = append(args, *config.IntervalMinutes)
argIdx++
}
if config.AutoRun != nil {
updates = append(updates, fmt.Sprintf("auto_run = $%d", argIdx))
args = append(args, *config.AutoRun)
argIdx++
// If enabling auto_run, calculate next_run_at
if *config.AutoRun {
updates = append(updates, fmt.Sprintf("next_run_at = NOW() + INTERVAL '%d minutes'", argIdx))
}
}
if len(updates) == 0 {
return fmt.Errorf("no fields to update")
}
updates = append(updates, "updated_at = NOW()")
query := fmt.Sprintf(`
UPDATE agent_subsystems
SET %s
WHERE agent_id = $1 AND subsystem = $2
`, joinUpdates(updates))
result, err := q.db.Exec(query, args...)
if err != nil {
return fmt.Errorf("failed to update subsystem: %w", err)
}
rows, err := result.RowsAffected()
if err != nil {
return fmt.Errorf("failed to get rows affected: %w", err)
}
if rows == 0 {
return fmt.Errorf("subsystem not found")
}
return nil
}
// UpdateLastRun updates the last_run_at timestamp for a subsystem
func (q *SubsystemQueries) UpdateLastRun(agentID uuid.UUID, subsystem string) error {
query := `
UPDATE agent_subsystems
SET last_run_at = NOW(),
next_run_at = CASE
WHEN auto_run THEN NOW() + (interval_minutes || ' minutes')::INTERVAL
ELSE next_run_at
END,
updated_at = NOW()
WHERE agent_id = $1 AND subsystem = $2
`
result, err := q.db.Exec(query, agentID, subsystem)
if err != nil {
return fmt.Errorf("failed to update last run: %w", err)
}
rows, err := result.RowsAffected()
if err != nil {
return fmt.Errorf("failed to get rows affected: %w", err)
}
if rows == 0 {
return fmt.Errorf("subsystem not found")
}
return nil
}
// GetDueSubsystems retrieves all subsystems that are due to run
func (q *SubsystemQueries) GetDueSubsystems() ([]models.AgentSubsystem, error) {
query := `
SELECT id, agent_id, subsystem, enabled, interval_minutes, auto_run,
last_run_at, next_run_at, created_at, updated_at
FROM agent_subsystems
WHERE enabled = true
AND auto_run = true
AND (next_run_at IS NULL OR next_run_at <= NOW())
ORDER BY next_run_at ASC NULLS FIRST
LIMIT 1000
`
var subsystems []models.AgentSubsystem
err := q.db.Select(&subsystems, query)
if err != nil {
return nil, fmt.Errorf("failed to get due subsystems: %w", err)
}
return subsystems, nil
}
// GetSubsystemStats retrieves statistics for a subsystem
func (q *SubsystemQueries) GetSubsystemStats(agentID uuid.UUID, subsystem string) (*models.SubsystemStats, error) {
query := `
SELECT
s.subsystem,
s.enabled,
s.last_run_at,
s.next_run_at,
s.interval_minutes,
s.auto_run,
COUNT(c.id) FILTER (WHERE c.command_type = 'scan_' || s.subsystem) as run_count,
MAX(c.status) FILTER (WHERE c.command_type = 'scan_' || s.subsystem) as last_status,
MAX(al.duration_seconds) FILTER (WHERE al.action = 'scan_' || s.subsystem) as last_duration
FROM agent_subsystems s
LEFT JOIN agent_commands c ON c.agent_id = s.agent_id
LEFT JOIN agent_logs al ON al.command_id = c.id
WHERE s.agent_id = $1 AND s.subsystem = $2
GROUP BY s.subsystem, s.enabled, s.last_run_at, s.next_run_at, s.interval_minutes, s.auto_run
`
var stats models.SubsystemStats
err := q.db.Get(&stats, query, agentID, subsystem)
if err == sql.ErrNoRows {
return nil, nil
}
if err != nil {
return nil, fmt.Errorf("failed to get subsystem stats: %w", err)
}
return &stats, nil
}
// EnableSubsystem enables a subsystem
func (q *SubsystemQueries) EnableSubsystem(agentID uuid.UUID, subsystem string) error {
enabled := true
return q.UpdateSubsystem(agentID, subsystem, models.SubsystemConfig{
Enabled: &enabled,
})
}
// DisableSubsystem disables a subsystem
func (q *SubsystemQueries) DisableSubsystem(agentID uuid.UUID, subsystem string) error {
enabled := false
return q.UpdateSubsystem(agentID, subsystem, models.SubsystemConfig{
Enabled: &enabled,
})
}
// SetAutoRun enables or disables auto-run for a subsystem
func (q *SubsystemQueries) SetAutoRun(agentID uuid.UUID, subsystem string, autoRun bool) error {
return q.UpdateSubsystem(agentID, subsystem, models.SubsystemConfig{
AutoRun: &autoRun,
})
}
// SetInterval sets the interval for a subsystem
func (q *SubsystemQueries) SetInterval(agentID uuid.UUID, subsystem string, intervalMinutes int) error {
return q.UpdateSubsystem(agentID, subsystem, models.SubsystemConfig{
IntervalMinutes: &intervalMinutes,
})
}
// CreateSubsystem creates a new subsystem configuration (used for custom subsystems)
func (q *SubsystemQueries) CreateSubsystem(sub *models.AgentSubsystem) error {
query := `
INSERT INTO agent_subsystems (agent_id, subsystem, enabled, interval_minutes, auto_run, last_run_at, next_run_at)
VALUES ($1, $2, $3, $4, $5, $6, $7)
RETURNING id, created_at, updated_at
`
err := q.db.QueryRow(
query,
sub.AgentID,
sub.Subsystem,
sub.Enabled,
sub.IntervalMinutes,
sub.AutoRun,
sub.LastRunAt,
sub.NextRunAt,
).Scan(&sub.ID, &sub.CreatedAt, &sub.UpdatedAt)
if err != nil {
return fmt.Errorf("failed to create subsystem: %w", err)
}
return nil
}
// DeleteSubsystem deletes a subsystem configuration
func (q *SubsystemQueries) DeleteSubsystem(agentID uuid.UUID, subsystem string) error {
query := `
DELETE FROM agent_subsystems
WHERE agent_id = $1 AND subsystem = $2
`
result, err := q.db.Exec(query, agentID, subsystem)
if err != nil {
return fmt.Errorf("failed to delete subsystem: %w", err)
}
rows, err := result.RowsAffected()
if err != nil {
return fmt.Errorf("failed to get rows affected: %w", err)
}
if rows == 0 {
return fmt.Errorf("subsystem not found")
}
return nil
}
// Helper function to join update statements
func joinUpdates(updates []string) string {
result := ""
for i, update := range updates {
if i > 0 {
result += ", "
}
result += update
}
return result
}

View File

@@ -0,0 +1,51 @@
package models
import (
"time"
"github.com/google/uuid"
)
// AgentSubsystem represents a subsystem configuration for an agent
type AgentSubsystem struct {
ID uuid.UUID `json:"id" db:"id"`
AgentID uuid.UUID `json:"agent_id" db:"agent_id"`
Subsystem string `json:"subsystem" db:"subsystem"`
Enabled bool `json:"enabled" db:"enabled"`
IntervalMinutes int `json:"interval_minutes" db:"interval_minutes"`
AutoRun bool `json:"auto_run" db:"auto_run"`
LastRunAt *time.Time `json:"last_run_at,omitempty" db:"last_run_at"`
NextRunAt *time.Time `json:"next_run_at,omitempty" db:"next_run_at"`
CreatedAt time.Time `json:"created_at" db:"created_at"`
UpdatedAt time.Time `json:"updated_at" db:"updated_at"`
}
// SubsystemType represents the type of subsystem
type SubsystemType string
const (
SubsystemUpdates SubsystemType = "updates"
SubsystemStorage SubsystemType = "storage"
SubsystemSystem SubsystemType = "system"
SubsystemDocker SubsystemType = "docker"
)
// SubsystemConfig represents the configuration for updating a subsystem
type SubsystemConfig struct {
Enabled *bool `json:"enabled,omitempty"`
IntervalMinutes *int `json:"interval_minutes,omitempty"`
AutoRun *bool `json:"auto_run,omitempty"`
}
// SubsystemStats provides statistics about a subsystem's execution
type SubsystemStats struct {
Subsystem string `json:"subsystem"`
Enabled bool `json:"enabled"`
LastRunAt *time.Time `json:"last_run_at,omitempty"`
NextRunAt *time.Time `json:"next_run_at,omitempty"`
IntervalMinutes int `json:"interval_minutes"`
AutoRun bool `json:"auto_run"`
RunCount int `json:"run_count"` // Total runs
LastStatus string `json:"last_status"` // Last command status
LastDuration int `json:"last_duration"` // Last run duration in seconds
}