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:
@@ -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
|
||||
|
||||
@@ -122,8 +122,8 @@ func handleScanStorage(apiClient *client.Client, cfg *config.Config, ackTracker
|
||||
}
|
||||
|
||||
// Report storage metrics to server using dedicated endpoint
|
||||
// Get storage scanner and use proper interface
|
||||
storageScanner := orchestrator.NewStorageScanner("unknown") // TODO: Get actual agent version
|
||||
// Use proper StorageMetricReport with clean field names
|
||||
storageScanner := orchestrator.NewStorageScanner(cfg.AgentVersion)
|
||||
if storageScanner.IsAvailable() {
|
||||
metrics, err := storageScanner.ScanStorage()
|
||||
if err != nil {
|
||||
@@ -131,32 +131,38 @@ func handleScanStorage(apiClient *client.Client, cfg *config.Config, ackTracker
|
||||
}
|
||||
|
||||
if len(metrics) > 0 {
|
||||
// Convert StorageMetric to MetricsReportItem for API call
|
||||
metricItems := make([]client.MetricsReportItem, 0, len(metrics))
|
||||
for _, metric := range metrics {
|
||||
item := client.MetricsReportItem{
|
||||
PackageType: "storage",
|
||||
PackageName: metric.Mountpoint,
|
||||
CurrentVersion: fmt.Sprintf("%d bytes used", metric.UsedBytes),
|
||||
AvailableVersion: fmt.Sprintf("%d bytes total", metric.TotalBytes),
|
||||
Severity: metric.Severity,
|
||||
RepositorySource: metric.Filesystem,
|
||||
Metadata: metric.Metadata,
|
||||
// Convert from orchestrator.StorageMetric to models.StorageMetric
|
||||
metricItems := make([]models.StorageMetric, 0, len(metrics))
|
||||
for _, m := range metrics {
|
||||
item := models.StorageMetric{
|
||||
Mountpoint: m.Mountpoint,
|
||||
Device: m.Device,
|
||||
DiskType: m.DiskType,
|
||||
Filesystem: m.Filesystem,
|
||||
TotalBytes: m.TotalBytes,
|
||||
UsedBytes: m.UsedBytes,
|
||||
AvailableBytes: m.AvailableBytes,
|
||||
UsedPercent: m.UsedPercent,
|
||||
IsRoot: m.IsRoot,
|
||||
IsLargest: m.IsLargest,
|
||||
Severity: m.Severity,
|
||||
Metadata: m.Metadata,
|
||||
}
|
||||
metricItems = append(metricItems, item)
|
||||
}
|
||||
|
||||
report := client.MetricsReport{
|
||||
report := models.StorageMetricReport{
|
||||
AgentID: cfg.AgentID,
|
||||
CommandID: commandID,
|
||||
Timestamp: time.Now(),
|
||||
Metrics: metricItems,
|
||||
}
|
||||
|
||||
if err := apiClient.ReportMetrics(cfg.AgentID, report); err != nil {
|
||||
if err := apiClient.ReportStorageMetrics(cfg.AgentID, report); err != nil {
|
||||
return fmt.Errorf("failed to report storage metrics: %w", err)
|
||||
}
|
||||
|
||||
log.Printf("✓ Reported %d storage metrics to server\n", len(metrics))
|
||||
log.Printf("[INFO] [storage] Successfully reported %d storage metrics to server\n", len(metrics))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user