Implement proper storage metrics (P0-009)\n\n- Add dedicated storage_metrics table\n- Create StorageMetricReport models with proper field names\n- Add ReportStorageMetrics to agent client\n- Update storage scanner to use new method\n- Implement server-side handlers and queries\n- Register new routes and update UI\n- Remove legacy Scan() method\n- Follow ETHOS principles: honest naming, clean architecture

This commit is contained in:
Fimeg
2025-12-17 16:38:36 -05:00
parent f7c8d23c5d
commit 0fff047cb5
43 changed files with 3641 additions and 248 deletions

View File

@@ -8,7 +8,6 @@ import (
"math/rand"
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"
"time"
@@ -18,6 +17,7 @@ import (
"github.com/Fimeg/RedFlag/aggregator-agent/internal/circuitbreaker"
"github.com/Fimeg/RedFlag/aggregator-agent/internal/client"
"github.com/Fimeg/RedFlag/aggregator-agent/internal/config"
"github.com/Fimeg/RedFlag/aggregator-agent/internal/constants"
"github.com/Fimeg/RedFlag/aggregator-agent/internal/crypto"
"github.com/Fimeg/RedFlag/aggregator-agent/internal/display"
"github.com/Fimeg/RedFlag/aggregator-agent/internal/installer"
@@ -26,33 +26,14 @@ import (
"github.com/Fimeg/RedFlag/aggregator-agent/internal/scanner"
"github.com/Fimeg/RedFlag/aggregator-agent/internal/service"
"github.com/Fimeg/RedFlag/aggregator-agent/internal/system"
"github.com/Fimeg/RedFlag/aggregator-agent/internal/version"
"github.com/google/uuid"
)
const (
AgentVersion = "0.1.23" // v0.1.23: Real security metrics and config sync
)
var (
lastConfigVersion int64 = 0 // Track last applied config version
)
// getConfigPath returns the platform-specific config path
func getConfigPath() string {
if runtime.GOOS == "windows" {
return "C:\\ProgramData\\RedFlag\\config.json"
}
return "/etc/redflag/config.json"
}
// getStatePath returns the platform-specific state directory path
func getStatePath() string {
if runtime.GOOS == "windows" {
return "C:\\ProgramData\\RedFlag\\state"
}
return "/var/lib/redflag"
}
// reportLogWithAck reports a command log to the server and tracks it for acknowledgment
func reportLogWithAck(apiClient *client.Client, cfg *config.Config, ackTracker *acknowledgment.Tracker, logReport client.LogReport) error {
// Track this command result as pending acknowledgment
@@ -85,7 +66,7 @@ func getCurrentPollingInterval(cfg *config.Config) int {
cfg.RapidPollingEnabled = false
cfg.RapidPollingUntil = time.Time{}
// Save the updated config to clean up expired rapid mode
if err := cfg.Save(getConfigPath()); err != nil {
if err := cfg.Save(constants.GetAgentConfigPath()); err != nil {
log.Printf("Warning: Failed to cleanup expired rapid polling mode: %v", err)
}
}
@@ -138,7 +119,7 @@ func main() {
// Handle version command
if *versionCmd {
fmt.Printf("RedFlag Agent v%s\n", AgentVersion)
fmt.Printf("RedFlag Agent v%s\n", version.Version)
fmt.Printf("Self-hosted update management platform\n")
os.Exit(0)
}
@@ -201,7 +182,7 @@ func main() {
ProxyHTTP: *proxyHTTP,
ProxyHTTPS: *proxyHTTPS,
ProxyNoProxy: *proxyNoProxy,
LogLevel: *logLevel,
LogLevel: *logLevel,
ConfigFile: *configFile,
Tags: tags,
Organization: *organization,
@@ -210,7 +191,7 @@ func main() {
}
// Determine config path
configPath := getConfigPath()
configPath := constants.GetAgentConfigPath()
if *configFile != "" {
configPath = *configFile
}
@@ -218,30 +199,30 @@ func main() {
// Check for migration requirements before loading configuration
migrationConfig := migration.NewFileDetectionConfig()
// Set old paths to detect existing installations
migrationConfig.OldConfigPath = "/etc/aggregator"
migrationConfig.OldStatePath = "/var/lib/aggregator"
migrationConfig.OldConfigPath = constants.LegacyConfigPath
migrationConfig.OldStatePath = constants.LegacyStatePath
// Set new paths that agent will actually use
migrationConfig.NewConfigPath = filepath.Dir(configPath)
migrationConfig.NewStatePath = getStatePath()
migrationConfig.NewConfigPath = constants.GetAgentConfigDir()
migrationConfig.NewStatePath = constants.GetAgentStateDir()
// Detect migration requirements
migrationDetection, err := migration.DetectMigrationRequirements(migrationConfig)
if err != nil {
log.Printf("Warning: Failed to detect migration requirements: %v", err)
} else if migrationDetection.RequiresMigration {
log.Printf("[RedFlag Server Migrator] Migration detected: %s → %s", migrationDetection.CurrentAgentVersion, AgentVersion)
log.Printf("[RedFlag Server Migrator] Migration detected: %s → %s", migrationDetection.CurrentAgentVersion, version.Version)
log.Printf("[RedFlag Server Migrator] Required migrations: %v", migrationDetection.RequiredMigrations)
// Create migration plan
migrationPlan := &migration.MigrationPlan{
Detection: migrationDetection,
TargetVersion: AgentVersion,
TargetVersion: version.Version,
Config: migrationConfig,
BackupPath: filepath.Join(getStatePath(), "migration_backups"), // Set backup path within agent's state directory
BackupPath: constants.GetMigrationBackupDir(), // Set backup path within agent's state directory
}
// Execute migration
executor := migration.NewMigrationExecutor(migrationPlan)
executor := migration.NewMigrationExecutor(migrationPlan, configPath)
result, err := executor.ExecuteMigration()
if err != nil {
log.Printf("[RedFlag Server Migrator] Migration failed: %v", err)
@@ -262,14 +243,14 @@ func main() {
}
// Always set the current agent version in config
if cfg.AgentVersion != AgentVersion {
if cfg.AgentVersion != version.Version {
if cfg.AgentVersion != "" {
log.Printf("[RedFlag Server Migrator] Version change detected: %s → %s", cfg.AgentVersion, AgentVersion)
log.Printf("[RedFlag Server Migrator] Version change detected: %s → %s", cfg.AgentVersion, version.Version)
log.Printf("[RedFlag Server Migrator] Performing lightweight migration check...")
}
// Update config version to match current agent
cfg.AgentVersion = AgentVersion
cfg.AgentVersion = version.Version
// Save updated config
if err := cfg.Save(configPath); err != nil {
@@ -364,7 +345,7 @@ func main() {
func registerAgent(cfg *config.Config, serverURL string) error {
// Get detailed system information
sysInfo, err := system.GetSystemInfo(AgentVersion)
sysInfo, err := system.GetSystemInfo(version.Version)
if err != nil {
log.Printf("Warning: Failed to get detailed system info: %v\n", err)
// Fall back to basic detection
@@ -375,7 +356,7 @@ func registerAgent(cfg *config.Config, serverURL string) error {
OSType: osType,
OSVersion: osVersion,
OSArchitecture: osArch,
AgentVersion: AgentVersion,
AgentVersion: version.Version,
Metadata: make(map[string]string),
}
}
@@ -429,14 +410,14 @@ func registerAgent(cfg *config.Config, serverURL string) error {
}
req := client.RegisterRequest{
Hostname: sysInfo.Hostname,
OSType: sysInfo.OSType,
OSVersion: sysInfo.OSVersion,
OSArchitecture: sysInfo.OSArchitecture,
AgentVersion: sysInfo.AgentVersion,
MachineID: machineID,
Hostname: sysInfo.Hostname,
OSType: sysInfo.OSType,
OSVersion: sysInfo.OSVersion,
OSArchitecture: sysInfo.OSArchitecture,
AgentVersion: sysInfo.AgentVersion,
MachineID: machineID,
PublicKeyFingerprint: publicKeyFingerprint,
Metadata: metadata,
Metadata: metadata,
}
resp, err := apiClient.Register(req)
@@ -458,7 +439,7 @@ func registerAgent(cfg *config.Config, serverURL string) error {
}
// Save configuration
if err := cfg.Save(getConfigPath()); err != nil {
if err := cfg.Save(constants.GetAgentConfigPath()); err != nil {
return fmt.Errorf("failed to save config: %w", err)
}
@@ -496,7 +477,7 @@ func renewTokenIfNeeded(apiClient *client.Client, cfg *config.Config, err error)
tempClient := client.NewClient(cfg.ServerURL, "")
// Attempt to renew access token using refresh token
if err := tempClient.RenewToken(cfg.AgentID, cfg.RefreshToken); err != nil {
if err := tempClient.RenewToken(cfg.AgentID, cfg.RefreshToken, version.Version); err != nil {
log.Printf("❌ Refresh token renewal failed: %v", err)
log.Printf("💡 Refresh token may be expired (>90 days) - re-registration required")
return nil, fmt.Errorf("refresh token renewal failed: %w - please re-register agent", err)
@@ -506,7 +487,7 @@ func renewTokenIfNeeded(apiClient *client.Client, cfg *config.Config, err error)
cfg.Token = tempClient.GetToken()
// Save updated config
if err := cfg.Save(getConfigPath()); err != nil {
if err := cfg.Save(constants.GetAgentConfigPath()); err != nil {
log.Printf("⚠️ Warning: Failed to save renewed access token: %v", err)
}
@@ -625,7 +606,7 @@ func syncServerConfig(apiClient *client.Client, cfg *config.Config) error {
}
func runAgent(cfg *config.Config) error {
log.Printf("🚩 RedFlag Agent v%s starting...\n", AgentVersion)
log.Printf("🚩 RedFlag Agent v%s starting...\n", version.Version)
log.Printf("==================================================================")
log.Printf("📋 AGENT ID: %s", cfg.AgentID)
log.Printf("🌐 SERVER: %s", cfg.ServerURL)
@@ -688,7 +669,7 @@ func runAgent(cfg *config.Config) error {
// - System: handleScanSystem → ReportMetrics()
// Initialize acknowledgment tracker for command result reliability
ackTracker := acknowledgment.NewTracker(getStatePath())
ackTracker := acknowledgment.NewTracker(constants.GetAgentStateDir())
if err := ackTracker.Load(); err != nil {
log.Printf("Warning: Failed to load pending acknowledgments: %v", err)
} else {
@@ -734,7 +715,7 @@ func runAgent(cfg *config.Config) error {
}
}
log.Printf("Checking in with server... (Agent v%s)", AgentVersion)
log.Printf("Checking in with server... (Agent v%s)", version.Version)
// Collect lightweight system metrics
sysMetrics, err := system.GetLightweightMetrics()
@@ -749,7 +730,7 @@ func runAgent(cfg *config.Config) error {
DiskTotalGB: sysMetrics.DiskTotalGB,
DiskPercent: sysMetrics.DiskPercent,
Uptime: sysMetrics.Uptime,
Version: AgentVersion,
Version: version.Version,
}
}
@@ -890,7 +871,6 @@ func runAgent(cfg *config.Config) error {
log.Printf("[Heartbeat] Error disabling heartbeat: %v\n", err)
}
case "reboot":
if err := handleReboot(apiClient, cfg, ackTracker, cmd.ID, cmd.Params); err != nil {
log.Printf("[Reboot] Error processing reboot command: %v\n", err)
@@ -1298,26 +1278,26 @@ func handleDryRunUpdate(apiClient *client.Client, cfg *config.Config, ackTracker
// Convert installer.InstallResult to client.InstallResult for reporting
clientResult := &client.InstallResult{
Success: result.Success,
ErrorMessage: result.ErrorMessage,
Stdout: result.Stdout,
Stderr: result.Stderr,
ExitCode: result.ExitCode,
DurationSeconds: result.DurationSeconds,
Action: result.Action,
Success: result.Success,
ErrorMessage: result.ErrorMessage,
Stdout: result.Stdout,
Stderr: result.Stderr,
ExitCode: result.ExitCode,
DurationSeconds: result.DurationSeconds,
Action: result.Action,
PackagesInstalled: result.PackagesInstalled,
ContainersUpdated: result.ContainersUpdated,
Dependencies: result.Dependencies,
IsDryRun: true,
Dependencies: result.Dependencies,
IsDryRun: true,
}
// Report dependencies back to server
depReport := client.DependencyReport{
PackageName: packageName,
PackageType: packageType,
Dependencies: result.Dependencies,
UpdateID: params["update_id"].(string),
DryRunResult: clientResult,
PackageName: packageName,
PackageType: packageType,
Dependencies: result.Dependencies,
UpdateID: params["update_id"].(string),
DryRunResult: clientResult,
}
if reportErr := apiClient.ReportDependencies(cfg.AgentID, depReport); reportErr != nil {
@@ -1490,7 +1470,7 @@ func handleEnableHeartbeat(apiClient *client.Client, cfg *config.Config, ackTrac
cfg.RapidPollingUntil = expiryTime
// Save config to persist heartbeat settings
if err := cfg.Save(getConfigPath()); err != nil {
if err := cfg.Save(constants.GetAgentConfigPath()); err != nil {
log.Printf("[Heartbeat] Warning: Failed to save config: %v", err)
}
@@ -1522,7 +1502,7 @@ func handleEnableHeartbeat(apiClient *client.Client, cfg *config.Config, ackTrac
DiskTotalGB: sysMetrics.DiskTotalGB,
DiskPercent: sysMetrics.DiskPercent,
Uptime: sysMetrics.Uptime,
Version: AgentVersion,
Version: version.Version,
}
// Include heartbeat metadata to show enabled state
metrics.Metadata = map[string]interface{}{
@@ -1554,7 +1534,7 @@ func handleDisableHeartbeat(apiClient *client.Client, cfg *config.Config, ackTra
cfg.RapidPollingUntil = time.Time{} // Zero value
// Save config to persist heartbeat settings
if err := cfg.Save(getConfigPath()); err != nil {
if err := cfg.Save(constants.GetAgentConfigPath()); err != nil {
log.Printf("[Heartbeat] Warning: Failed to save config: %v", err)
}
@@ -1586,7 +1566,7 @@ func handleDisableHeartbeat(apiClient *client.Client, cfg *config.Config, ackTra
DiskTotalGB: sysMetrics.DiskTotalGB,
DiskPercent: sysMetrics.DiskPercent,
Uptime: sysMetrics.Uptime,
Version: AgentVersion,
Version: version.Version,
}
// Include empty heartbeat metadata to explicitly show disabled state
metrics.Metadata = map[string]interface{}{
@@ -1612,7 +1592,7 @@ func handleDisableHeartbeat(apiClient *client.Client, cfg *config.Config, ackTra
// reportSystemInfo collects and reports detailed system information to the server
func reportSystemInfo(apiClient *client.Client, cfg *config.Config) error {
// Collect detailed system information
sysInfo, err := system.GetSystemInfo(AgentVersion)
sysInfo, err := system.GetSystemInfo(version.Version)
if err != nil {
return fmt.Errorf("failed to get system info: %w", err)
}
@@ -1620,16 +1600,16 @@ func reportSystemInfo(apiClient *client.Client, cfg *config.Config) error {
// Create system info report
report := client.SystemInfoReport{
Timestamp: time.Now(),
CPUModel: sysInfo.CPUInfo.ModelName,
CPUCores: sysInfo.CPUInfo.Cores,
CPUThreads: sysInfo.CPUInfo.Threads,
MemoryTotal: sysInfo.MemoryInfo.Total,
DiskTotal: uint64(0),
DiskUsed: uint64(0),
IPAddress: sysInfo.IPAddress,
Processes: sysInfo.RunningProcesses,
Uptime: sysInfo.Uptime,
Metadata: make(map[string]interface{}),
CPUModel: sysInfo.CPUInfo.ModelName,
CPUCores: sysInfo.CPUInfo.Cores,
CPUThreads: sysInfo.CPUInfo.Threads,
MemoryTotal: sysInfo.MemoryInfo.Total,
DiskTotal: uint64(0),
DiskUsed: uint64(0),
IPAddress: sysInfo.IPAddress,
Processes: sysInfo.RunningProcesses,
Uptime: sysInfo.Uptime,
Metadata: make(map[string]interface{}),
}
// Add primary disk info