You can not select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
					
					
						
							1235 lines
						
					
					
						
							40 KiB
						
					
					
				
			
		
		
		
			
			
			
		
		
	
	
							1235 lines
						
					
					
						
							40 KiB
						
					
					
				
								package dash
							 | 
						|
								
							 | 
						|
								import (
							 | 
						|
									"encoding/json"
							 | 
						|
									"fmt"
							 | 
						|
									"os"
							 | 
						|
									"path/filepath"
							 | 
						|
									"sort"
							 | 
						|
									"strings"
							 | 
						|
									"time"
							 | 
						|
								
							 | 
						|
									"github.com/seaweedfs/seaweedfs/weed/admin/maintenance"
							 | 
						|
									"github.com/seaweedfs/seaweedfs/weed/glog"
							 | 
						|
									"github.com/seaweedfs/seaweedfs/weed/pb/worker_pb"
							 | 
						|
									"github.com/seaweedfs/seaweedfs/weed/worker/tasks/balance"
							 | 
						|
									"github.com/seaweedfs/seaweedfs/weed/worker/tasks/erasure_coding"
							 | 
						|
									"github.com/seaweedfs/seaweedfs/weed/worker/tasks/vacuum"
							 | 
						|
									"google.golang.org/protobuf/encoding/protojson"
							 | 
						|
									"google.golang.org/protobuf/proto"
							 | 
						|
								)
							 | 
						|
								
							 | 
						|
								const (
							 | 
						|
									// Configuration subdirectory
							 | 
						|
									ConfigSubdir = "conf"
							 | 
						|
								
							 | 
						|
									// Configuration file names (protobuf binary)
							 | 
						|
									MaintenanceConfigFile     = "maintenance.pb"
							 | 
						|
									VacuumTaskConfigFile      = "task_vacuum.pb"
							 | 
						|
									ECTaskConfigFile          = "task_erasure_coding.pb"
							 | 
						|
									BalanceTaskConfigFile     = "task_balance.pb"
							 | 
						|
									ReplicationTaskConfigFile = "task_replication.pb"
							 | 
						|
								
							 | 
						|
									// JSON reference files
							 | 
						|
									MaintenanceConfigJSONFile     = "maintenance.json"
							 | 
						|
									VacuumTaskConfigJSONFile      = "task_vacuum.json"
							 | 
						|
									ECTaskConfigJSONFile          = "task_erasure_coding.json"
							 | 
						|
									BalanceTaskConfigJSONFile     = "task_balance.json"
							 | 
						|
									ReplicationTaskConfigJSONFile = "task_replication.json"
							 | 
						|
								
							 | 
						|
									// Task persistence subdirectories and settings
							 | 
						|
									TasksSubdir       = "tasks"
							 | 
						|
									TaskDetailsSubdir = "task_details"
							 | 
						|
									TaskLogsSubdir    = "task_logs"
							 | 
						|
									MaxCompletedTasks = 10 // Only keep last 10 completed tasks
							 | 
						|
								
							 | 
						|
									ConfigDirPermissions  = 0755
							 | 
						|
									ConfigFilePermissions = 0644
							 | 
						|
								)
							 | 
						|
								
							 | 
						|
								// Task configuration types
							 | 
						|
								type (
							 | 
						|
									VacuumTaskConfig        = worker_pb.VacuumTaskConfig
							 | 
						|
									ErasureCodingTaskConfig = worker_pb.ErasureCodingTaskConfig
							 | 
						|
									BalanceTaskConfig       = worker_pb.BalanceTaskConfig
							 | 
						|
									ReplicationTaskConfig   = worker_pb.ReplicationTaskConfig
							 | 
						|
								)
							 | 
						|
								
							 | 
						|
								// isValidTaskID validates that a task ID is safe for use in file paths
							 | 
						|
								// This prevents path traversal attacks by ensuring the task ID doesn't contain
							 | 
						|
								// path separators or parent directory references
							 | 
						|
								func isValidTaskID(taskID string) bool {
							 | 
						|
									if taskID == "" {
							 | 
						|
										return false
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									// Reject task IDs with leading or trailing whitespace
							 | 
						|
									if strings.TrimSpace(taskID) != taskID {
							 | 
						|
										return false
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									// Check for path traversal patterns
							 | 
						|
									if strings.Contains(taskID, "/") ||
							 | 
						|
										strings.Contains(taskID, "\\") ||
							 | 
						|
										strings.Contains(taskID, "..") ||
							 | 
						|
										strings.Contains(taskID, ":") {
							 | 
						|
										return false
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									// Additional safety: ensure it's not just dots or empty after trim
							 | 
						|
									if taskID == "." || taskID == ".." {
							 | 
						|
										return false
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									return true
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								// ConfigPersistence handles saving and loading configuration files
							 | 
						|
								type ConfigPersistence struct {
							 | 
						|
									dataDir string
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								// NewConfigPersistence creates a new configuration persistence manager
							 | 
						|
								func NewConfigPersistence(dataDir string) *ConfigPersistence {
							 | 
						|
									return &ConfigPersistence{
							 | 
						|
										dataDir: dataDir,
							 | 
						|
									}
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								// SaveMaintenanceConfig saves maintenance configuration to protobuf file and JSON reference
							 | 
						|
								func (cp *ConfigPersistence) SaveMaintenanceConfig(config *MaintenanceConfig) error {
							 | 
						|
									if cp.dataDir == "" {
							 | 
						|
										return fmt.Errorf("no data directory specified, cannot save configuration")
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									confDir := filepath.Join(cp.dataDir, ConfigSubdir)
							 | 
						|
									if err := os.MkdirAll(confDir, ConfigDirPermissions); err != nil {
							 | 
						|
										return fmt.Errorf("failed to create config directory: %w", err)
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									// Save as protobuf (primary format)
							 | 
						|
									pbConfigPath := filepath.Join(confDir, MaintenanceConfigFile)
							 | 
						|
									pbData, err := proto.Marshal(config)
							 | 
						|
									if err != nil {
							 | 
						|
										return fmt.Errorf("failed to marshal maintenance config to protobuf: %w", err)
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									if err := os.WriteFile(pbConfigPath, pbData, ConfigFilePermissions); err != nil {
							 | 
						|
										return fmt.Errorf("failed to write protobuf config file: %w", err)
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									// Save JSON reference copy for debugging
							 | 
						|
									jsonConfigPath := filepath.Join(confDir, MaintenanceConfigJSONFile)
							 | 
						|
									jsonData, err := protojson.MarshalOptions{
							 | 
						|
										Multiline:       true,
							 | 
						|
										Indent:          "  ",
							 | 
						|
										EmitUnpopulated: true,
							 | 
						|
									}.Marshal(config)
							 | 
						|
									if err != nil {
							 | 
						|
										return fmt.Errorf("failed to marshal maintenance config to JSON: %w", err)
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									if err := os.WriteFile(jsonConfigPath, jsonData, ConfigFilePermissions); err != nil {
							 | 
						|
										return fmt.Errorf("failed to write JSON reference file: %w", err)
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									return nil
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								// LoadMaintenanceConfig loads maintenance configuration from protobuf file
							 | 
						|
								func (cp *ConfigPersistence) LoadMaintenanceConfig() (*MaintenanceConfig, error) {
							 | 
						|
									if cp.dataDir == "" {
							 | 
						|
										return DefaultMaintenanceConfig(), nil
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									confDir := filepath.Join(cp.dataDir, ConfigSubdir)
							 | 
						|
									configPath := filepath.Join(confDir, MaintenanceConfigFile)
							 | 
						|
								
							 | 
						|
									// Try to load from protobuf file
							 | 
						|
									if configData, err := os.ReadFile(configPath); err == nil {
							 | 
						|
										var config MaintenanceConfig
							 | 
						|
										if err := proto.Unmarshal(configData, &config); err == nil {
							 | 
						|
											// Always populate policy from separate task configuration files
							 | 
						|
											config.Policy = buildPolicyFromTaskConfigs()
							 | 
						|
											return &config, nil
							 | 
						|
										}
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									// File doesn't exist or failed to load, use defaults
							 | 
						|
									return DefaultMaintenanceConfig(), nil
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								// GetConfigPath returns the path to a configuration file
							 | 
						|
								func (cp *ConfigPersistence) GetConfigPath(filename string) string {
							 | 
						|
									if cp.dataDir == "" {
							 | 
						|
										return ""
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									// All configs go in conf subdirectory
							 | 
						|
									confDir := filepath.Join(cp.dataDir, ConfigSubdir)
							 | 
						|
									return filepath.Join(confDir, filename)
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								// ListConfigFiles returns all configuration files in the conf subdirectory
							 | 
						|
								func (cp *ConfigPersistence) ListConfigFiles() ([]string, error) {
							 | 
						|
									if cp.dataDir == "" {
							 | 
						|
										return nil, fmt.Errorf("no data directory specified")
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									confDir := filepath.Join(cp.dataDir, ConfigSubdir)
							 | 
						|
									files, err := os.ReadDir(confDir)
							 | 
						|
									if err != nil {
							 | 
						|
										// If conf directory doesn't exist, return empty list
							 | 
						|
										if os.IsNotExist(err) {
							 | 
						|
											return []string{}, nil
							 | 
						|
										}
							 | 
						|
										return nil, fmt.Errorf("failed to read config directory: %w", err)
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									var configFiles []string
							 | 
						|
									for _, file := range files {
							 | 
						|
										if !file.IsDir() {
							 | 
						|
											ext := filepath.Ext(file.Name())
							 | 
						|
											if ext == ".json" || ext == ".pb" {
							 | 
						|
												configFiles = append(configFiles, file.Name())
							 | 
						|
											}
							 | 
						|
										}
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									return configFiles, nil
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								// BackupConfig creates a backup of a configuration file
							 | 
						|
								func (cp *ConfigPersistence) BackupConfig(filename string) error {
							 | 
						|
									if cp.dataDir == "" {
							 | 
						|
										return fmt.Errorf("no data directory specified")
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									configPath := cp.GetConfigPath(filename)
							 | 
						|
									if _, err := os.Stat(configPath); os.IsNotExist(err) {
							 | 
						|
										return fmt.Errorf("config file does not exist: %s", filename)
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									// Create backup filename with timestamp
							 | 
						|
									timestamp := time.Now().Format("2006-01-02_15-04-05")
							 | 
						|
									backupName := fmt.Sprintf("%s.backup_%s", filename, timestamp)
							 | 
						|
								
							 | 
						|
									// Determine backup directory (conf subdirectory)
							 | 
						|
									confDir := filepath.Join(cp.dataDir, ConfigSubdir)
							 | 
						|
									backupPath := filepath.Join(confDir, backupName)
							 | 
						|
								
							 | 
						|
									// Copy file
							 | 
						|
									configData, err := os.ReadFile(configPath)
							 | 
						|
									if err != nil {
							 | 
						|
										return fmt.Errorf("failed to read config file: %w", err)
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									if err := os.WriteFile(backupPath, configData, ConfigFilePermissions); err != nil {
							 | 
						|
										return fmt.Errorf("failed to create backup: %w", err)
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									glog.V(1).Infof("Created backup of %s as %s", filename, backupName)
							 | 
						|
									return nil
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								// RestoreConfig restores a configuration file from a backup
							 | 
						|
								func (cp *ConfigPersistence) RestoreConfig(filename, backupName string) error {
							 | 
						|
									if cp.dataDir == "" {
							 | 
						|
										return fmt.Errorf("no data directory specified")
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									// Determine backup path (conf subdirectory)
							 | 
						|
									confDir := filepath.Join(cp.dataDir, ConfigSubdir)
							 | 
						|
									backupPath := filepath.Join(confDir, backupName)
							 | 
						|
								
							 | 
						|
									if _, err := os.Stat(backupPath); os.IsNotExist(err) {
							 | 
						|
										return fmt.Errorf("backup file does not exist: %s", backupName)
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									// Read backup file
							 | 
						|
									backupData, err := os.ReadFile(backupPath)
							 | 
						|
									if err != nil {
							 | 
						|
										return fmt.Errorf("failed to read backup file: %w", err)
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									// Write to config file
							 | 
						|
									configPath := cp.GetConfigPath(filename)
							 | 
						|
									if err := os.WriteFile(configPath, backupData, ConfigFilePermissions); err != nil {
							 | 
						|
										return fmt.Errorf("failed to restore config: %w", err)
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									glog.V(1).Infof("Restored %s from backup %s", filename, backupName)
							 | 
						|
									return nil
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								// SaveVacuumTaskConfig saves vacuum task configuration to protobuf file
							 | 
						|
								func (cp *ConfigPersistence) SaveVacuumTaskConfig(config *VacuumTaskConfig) error {
							 | 
						|
									return cp.saveTaskConfig(VacuumTaskConfigFile, config)
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								// SaveVacuumTaskPolicy saves complete vacuum task policy to protobuf file
							 | 
						|
								func (cp *ConfigPersistence) SaveVacuumTaskPolicy(policy *worker_pb.TaskPolicy) error {
							 | 
						|
									return cp.saveTaskConfig(VacuumTaskConfigFile, policy)
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								// LoadVacuumTaskConfig loads vacuum task configuration from protobuf file
							 | 
						|
								func (cp *ConfigPersistence) LoadVacuumTaskConfig() (*VacuumTaskConfig, error) {
							 | 
						|
									// Load as TaskPolicy and extract vacuum config
							 | 
						|
									if taskPolicy, err := cp.LoadVacuumTaskPolicy(); err == nil && taskPolicy != nil {
							 | 
						|
										if vacuumConfig := taskPolicy.GetVacuumConfig(); vacuumConfig != nil {
							 | 
						|
											return vacuumConfig, nil
							 | 
						|
										}
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									// Return default config if no valid config found
							 | 
						|
									return &VacuumTaskConfig{
							 | 
						|
										GarbageThreshold:   0.3,
							 | 
						|
										MinVolumeAgeHours:  24,
							 | 
						|
										MinIntervalSeconds: 7 * 24 * 60 * 60, // 7 days
							 | 
						|
									}, nil
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								// LoadVacuumTaskPolicy loads complete vacuum task policy from protobuf file
							 | 
						|
								func (cp *ConfigPersistence) LoadVacuumTaskPolicy() (*worker_pb.TaskPolicy, error) {
							 | 
						|
									if cp.dataDir == "" {
							 | 
						|
										// Return default policy if no data directory
							 | 
						|
										return &worker_pb.TaskPolicy{
							 | 
						|
											Enabled:               true,
							 | 
						|
											MaxConcurrent:         2,
							 | 
						|
											RepeatIntervalSeconds: 24 * 3600, // 24 hours in seconds
							 | 
						|
											CheckIntervalSeconds:  6 * 3600,  // 6 hours in seconds
							 | 
						|
											TaskConfig: &worker_pb.TaskPolicy_VacuumConfig{
							 | 
						|
												VacuumConfig: &worker_pb.VacuumTaskConfig{
							 | 
						|
													GarbageThreshold:   0.3,
							 | 
						|
													MinVolumeAgeHours:  24,
							 | 
						|
													MinIntervalSeconds: 7 * 24 * 60 * 60, // 7 days
							 | 
						|
												},
							 | 
						|
											},
							 | 
						|
										}, nil
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									confDir := filepath.Join(cp.dataDir, ConfigSubdir)
							 | 
						|
									configPath := filepath.Join(confDir, VacuumTaskConfigFile)
							 | 
						|
								
							 | 
						|
									// Check if file exists
							 | 
						|
									if _, err := os.Stat(configPath); os.IsNotExist(err) {
							 | 
						|
										// Return default policy if file doesn't exist
							 | 
						|
										return &worker_pb.TaskPolicy{
							 | 
						|
											Enabled:               true,
							 | 
						|
											MaxConcurrent:         2,
							 | 
						|
											RepeatIntervalSeconds: 24 * 3600, // 24 hours in seconds
							 | 
						|
											CheckIntervalSeconds:  6 * 3600,  // 6 hours in seconds
							 | 
						|
											TaskConfig: &worker_pb.TaskPolicy_VacuumConfig{
							 | 
						|
												VacuumConfig: &worker_pb.VacuumTaskConfig{
							 | 
						|
													GarbageThreshold:   0.3,
							 | 
						|
													MinVolumeAgeHours:  24,
							 | 
						|
													MinIntervalSeconds: 7 * 24 * 60 * 60, // 7 days
							 | 
						|
												},
							 | 
						|
											},
							 | 
						|
										}, nil
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									// Read file
							 | 
						|
									configData, err := os.ReadFile(configPath)
							 | 
						|
									if err != nil {
							 | 
						|
										return nil, fmt.Errorf("failed to read vacuum task config file: %w", err)
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									// Try to unmarshal as TaskPolicy
							 | 
						|
									var policy worker_pb.TaskPolicy
							 | 
						|
									if err := proto.Unmarshal(configData, &policy); err == nil {
							 | 
						|
										// Validate that it's actually a TaskPolicy with vacuum config
							 | 
						|
										if policy.GetVacuumConfig() != nil {
							 | 
						|
											glog.V(1).Infof("Loaded vacuum task policy from %s", configPath)
							 | 
						|
											return &policy, nil
							 | 
						|
										}
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									return nil, fmt.Errorf("failed to unmarshal vacuum task configuration")
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								// SaveErasureCodingTaskConfig saves EC task configuration to protobuf file
							 | 
						|
								func (cp *ConfigPersistence) SaveErasureCodingTaskConfig(config *ErasureCodingTaskConfig) error {
							 | 
						|
									return cp.saveTaskConfig(ECTaskConfigFile, config)
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								// SaveErasureCodingTaskPolicy saves complete EC task policy to protobuf file
							 | 
						|
								func (cp *ConfigPersistence) SaveErasureCodingTaskPolicy(policy *worker_pb.TaskPolicy) error {
							 | 
						|
									return cp.saveTaskConfig(ECTaskConfigFile, policy)
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								// LoadErasureCodingTaskConfig loads EC task configuration from protobuf file
							 | 
						|
								func (cp *ConfigPersistence) LoadErasureCodingTaskConfig() (*ErasureCodingTaskConfig, error) {
							 | 
						|
									// Load as TaskPolicy and extract EC config
							 | 
						|
									if taskPolicy, err := cp.LoadErasureCodingTaskPolicy(); err == nil && taskPolicy != nil {
							 | 
						|
										if ecConfig := taskPolicy.GetErasureCodingConfig(); ecConfig != nil {
							 | 
						|
											return ecConfig, nil
							 | 
						|
										}
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									// Return default config if no valid config found
							 | 
						|
									return &ErasureCodingTaskConfig{
							 | 
						|
										FullnessRatio:    0.9,
							 | 
						|
										QuietForSeconds:  3600,
							 | 
						|
										MinVolumeSizeMb:  1024,
							 | 
						|
										CollectionFilter: "",
							 | 
						|
									}, nil
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								// LoadErasureCodingTaskPolicy loads complete EC task policy from protobuf file
							 | 
						|
								func (cp *ConfigPersistence) LoadErasureCodingTaskPolicy() (*worker_pb.TaskPolicy, error) {
							 | 
						|
									if cp.dataDir == "" {
							 | 
						|
										// Return default policy if no data directory
							 | 
						|
										return &worker_pb.TaskPolicy{
							 | 
						|
											Enabled:               true,
							 | 
						|
											MaxConcurrent:         1,
							 | 
						|
											RepeatIntervalSeconds: 168 * 3600, // 1 week in seconds
							 | 
						|
											CheckIntervalSeconds:  24 * 3600,  // 24 hours in seconds
							 | 
						|
											TaskConfig: &worker_pb.TaskPolicy_ErasureCodingConfig{
							 | 
						|
												ErasureCodingConfig: &worker_pb.ErasureCodingTaskConfig{
							 | 
						|
													FullnessRatio:    0.9,
							 | 
						|
													QuietForSeconds:  3600,
							 | 
						|
													MinVolumeSizeMb:  1024,
							 | 
						|
													CollectionFilter: "",
							 | 
						|
												},
							 | 
						|
											},
							 | 
						|
										}, nil
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									confDir := filepath.Join(cp.dataDir, ConfigSubdir)
							 | 
						|
									configPath := filepath.Join(confDir, ECTaskConfigFile)
							 | 
						|
								
							 | 
						|
									// Check if file exists
							 | 
						|
									if _, err := os.Stat(configPath); os.IsNotExist(err) {
							 | 
						|
										// Return default policy if file doesn't exist
							 | 
						|
										return &worker_pb.TaskPolicy{
							 | 
						|
											Enabled:               true,
							 | 
						|
											MaxConcurrent:         1,
							 | 
						|
											RepeatIntervalSeconds: 168 * 3600, // 1 week in seconds
							 | 
						|
											CheckIntervalSeconds:  24 * 3600,  // 24 hours in seconds
							 | 
						|
											TaskConfig: &worker_pb.TaskPolicy_ErasureCodingConfig{
							 | 
						|
												ErasureCodingConfig: &worker_pb.ErasureCodingTaskConfig{
							 | 
						|
													FullnessRatio:    0.9,
							 | 
						|
													QuietForSeconds:  3600,
							 | 
						|
													MinVolumeSizeMb:  1024,
							 | 
						|
													CollectionFilter: "",
							 | 
						|
												},
							 | 
						|
											},
							 | 
						|
										}, nil
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									// Read file
							 | 
						|
									configData, err := os.ReadFile(configPath)
							 | 
						|
									if err != nil {
							 | 
						|
										return nil, fmt.Errorf("failed to read EC task config file: %w", err)
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									// Try to unmarshal as TaskPolicy
							 | 
						|
									var policy worker_pb.TaskPolicy
							 | 
						|
									if err := proto.Unmarshal(configData, &policy); err == nil {
							 | 
						|
										// Validate that it's actually a TaskPolicy with EC config
							 | 
						|
										if policy.GetErasureCodingConfig() != nil {
							 | 
						|
											glog.V(1).Infof("Loaded EC task policy from %s", configPath)
							 | 
						|
											return &policy, nil
							 | 
						|
										}
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									return nil, fmt.Errorf("failed to unmarshal EC task configuration")
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								// SaveBalanceTaskConfig saves balance task configuration to protobuf file
							 | 
						|
								func (cp *ConfigPersistence) SaveBalanceTaskConfig(config *BalanceTaskConfig) error {
							 | 
						|
									return cp.saveTaskConfig(BalanceTaskConfigFile, config)
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								// SaveBalanceTaskPolicy saves complete balance task policy to protobuf file
							 | 
						|
								func (cp *ConfigPersistence) SaveBalanceTaskPolicy(policy *worker_pb.TaskPolicy) error {
							 | 
						|
									return cp.saveTaskConfig(BalanceTaskConfigFile, policy)
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								// LoadBalanceTaskConfig loads balance task configuration from protobuf file
							 | 
						|
								func (cp *ConfigPersistence) LoadBalanceTaskConfig() (*BalanceTaskConfig, error) {
							 | 
						|
									// Load as TaskPolicy and extract balance config
							 | 
						|
									if taskPolicy, err := cp.LoadBalanceTaskPolicy(); err == nil && taskPolicy != nil {
							 | 
						|
										if balanceConfig := taskPolicy.GetBalanceConfig(); balanceConfig != nil {
							 | 
						|
											return balanceConfig, nil
							 | 
						|
										}
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									// Return default config if no valid config found
							 | 
						|
									return &BalanceTaskConfig{
							 | 
						|
										ImbalanceThreshold: 0.1,
							 | 
						|
										MinServerCount:     2,
							 | 
						|
									}, nil
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								// LoadBalanceTaskPolicy loads complete balance task policy from protobuf file
							 | 
						|
								func (cp *ConfigPersistence) LoadBalanceTaskPolicy() (*worker_pb.TaskPolicy, error) {
							 | 
						|
									if cp.dataDir == "" {
							 | 
						|
										// Return default policy if no data directory
							 | 
						|
										return &worker_pb.TaskPolicy{
							 | 
						|
											Enabled:               true,
							 | 
						|
											MaxConcurrent:         1,
							 | 
						|
											RepeatIntervalSeconds: 6 * 3600,  // 6 hours in seconds
							 | 
						|
											CheckIntervalSeconds:  12 * 3600, // 12 hours in seconds
							 | 
						|
											TaskConfig: &worker_pb.TaskPolicy_BalanceConfig{
							 | 
						|
												BalanceConfig: &worker_pb.BalanceTaskConfig{
							 | 
						|
													ImbalanceThreshold: 0.1,
							 | 
						|
													MinServerCount:     2,
							 | 
						|
												},
							 | 
						|
											},
							 | 
						|
										}, nil
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									confDir := filepath.Join(cp.dataDir, ConfigSubdir)
							 | 
						|
									configPath := filepath.Join(confDir, BalanceTaskConfigFile)
							 | 
						|
								
							 | 
						|
									// Check if file exists
							 | 
						|
									if _, err := os.Stat(configPath); os.IsNotExist(err) {
							 | 
						|
										// Return default policy if file doesn't exist
							 | 
						|
										return &worker_pb.TaskPolicy{
							 | 
						|
											Enabled:               true,
							 | 
						|
											MaxConcurrent:         1,
							 | 
						|
											RepeatIntervalSeconds: 6 * 3600,  // 6 hours in seconds
							 | 
						|
											CheckIntervalSeconds:  12 * 3600, // 12 hours in seconds
							 | 
						|
											TaskConfig: &worker_pb.TaskPolicy_BalanceConfig{
							 | 
						|
												BalanceConfig: &worker_pb.BalanceTaskConfig{
							 | 
						|
													ImbalanceThreshold: 0.1,
							 | 
						|
													MinServerCount:     2,
							 | 
						|
												},
							 | 
						|
											},
							 | 
						|
										}, nil
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									// Read file
							 | 
						|
									configData, err := os.ReadFile(configPath)
							 | 
						|
									if err != nil {
							 | 
						|
										return nil, fmt.Errorf("failed to read balance task config file: %w", err)
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									// Try to unmarshal as TaskPolicy
							 | 
						|
									var policy worker_pb.TaskPolicy
							 | 
						|
									if err := proto.Unmarshal(configData, &policy); err == nil {
							 | 
						|
										// Validate that it's actually a TaskPolicy with balance config
							 | 
						|
										if policy.GetBalanceConfig() != nil {
							 | 
						|
											glog.V(1).Infof("Loaded balance task policy from %s", configPath)
							 | 
						|
											return &policy, nil
							 | 
						|
										}
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									return nil, fmt.Errorf("failed to unmarshal balance task configuration")
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								// SaveReplicationTaskConfig saves replication task configuration to protobuf file
							 | 
						|
								func (cp *ConfigPersistence) SaveReplicationTaskConfig(config *ReplicationTaskConfig) error {
							 | 
						|
									return cp.saveTaskConfig(ReplicationTaskConfigFile, config)
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								// LoadReplicationTaskConfig loads replication task configuration from protobuf file
							 | 
						|
								func (cp *ConfigPersistence) LoadReplicationTaskConfig() (*ReplicationTaskConfig, error) {
							 | 
						|
									var config ReplicationTaskConfig
							 | 
						|
									err := cp.loadTaskConfig(ReplicationTaskConfigFile, &config)
							 | 
						|
									if err != nil {
							 | 
						|
										// Return default config if file doesn't exist
							 | 
						|
										if os.IsNotExist(err) {
							 | 
						|
											return &ReplicationTaskConfig{
							 | 
						|
												TargetReplicaCount: 1,
							 | 
						|
											}, nil
							 | 
						|
										}
							 | 
						|
										return nil, err
							 | 
						|
									}
							 | 
						|
									return &config, nil
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								// saveTaskConfig is a generic helper for saving task configurations with both protobuf and JSON reference
							 | 
						|
								func (cp *ConfigPersistence) saveTaskConfig(filename string, config proto.Message) error {
							 | 
						|
									if cp.dataDir == "" {
							 | 
						|
										return fmt.Errorf("no data directory specified, cannot save task configuration")
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									// Create conf subdirectory path
							 | 
						|
									confDir := filepath.Join(cp.dataDir, ConfigSubdir)
							 | 
						|
									configPath := filepath.Join(confDir, filename)
							 | 
						|
								
							 | 
						|
									// Generate JSON reference filename
							 | 
						|
									jsonFilename := filename[:len(filename)-3] + ".json" // Replace .pb with .json
							 | 
						|
									jsonPath := filepath.Join(confDir, jsonFilename)
							 | 
						|
								
							 | 
						|
									// Create conf directory if it doesn't exist
							 | 
						|
									if err := os.MkdirAll(confDir, ConfigDirPermissions); err != nil {
							 | 
						|
										return fmt.Errorf("failed to create config directory: %w", err)
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									// Marshal configuration to protobuf binary format
							 | 
						|
									configData, err := proto.Marshal(config)
							 | 
						|
									if err != nil {
							 | 
						|
										return fmt.Errorf("failed to marshal task config: %w", err)
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									// Write protobuf file
							 | 
						|
									if err := os.WriteFile(configPath, configData, ConfigFilePermissions); err != nil {
							 | 
						|
										return fmt.Errorf("failed to write task config file: %w", err)
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									// Marshal configuration to JSON for reference
							 | 
						|
									marshaler := protojson.MarshalOptions{
							 | 
						|
										Multiline:       true,
							 | 
						|
										Indent:          "  ",
							 | 
						|
										EmitUnpopulated: true,
							 | 
						|
									}
							 | 
						|
									jsonData, err := marshaler.Marshal(config)
							 | 
						|
									if err != nil {
							 | 
						|
										glog.Warningf("Failed to marshal task config to JSON reference: %v", err)
							 | 
						|
									} else {
							 | 
						|
										// Write JSON reference file
							 | 
						|
										if err := os.WriteFile(jsonPath, jsonData, ConfigFilePermissions); err != nil {
							 | 
						|
											glog.Warningf("Failed to write task config JSON reference: %v", err)
							 | 
						|
										}
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									glog.V(1).Infof("Saved task configuration to %s (with JSON reference)", configPath)
							 | 
						|
									return nil
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								// loadTaskConfig is a generic helper for loading task configurations from conf subdirectory
							 | 
						|
								func (cp *ConfigPersistence) loadTaskConfig(filename string, config proto.Message) error {
							 | 
						|
									if cp.dataDir == "" {
							 | 
						|
										return os.ErrNotExist // Will trigger default config return
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									confDir := filepath.Join(cp.dataDir, ConfigSubdir)
							 | 
						|
									configPath := filepath.Join(confDir, filename)
							 | 
						|
								
							 | 
						|
									// Check if file exists
							 | 
						|
									if _, err := os.Stat(configPath); os.IsNotExist(err) {
							 | 
						|
										return err // Will trigger default config return
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									// Read file
							 | 
						|
									configData, err := os.ReadFile(configPath)
							 | 
						|
									if err != nil {
							 | 
						|
										return fmt.Errorf("failed to read task config file: %w", err)
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									// Unmarshal protobuf binary data
							 | 
						|
									if err := proto.Unmarshal(configData, config); err != nil {
							 | 
						|
										return fmt.Errorf("failed to unmarshal task config: %w", err)
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									glog.V(1).Infof("Loaded task configuration from %s", configPath)
							 | 
						|
									return nil
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								// GetDataDir returns the data directory path
							 | 
						|
								func (cp *ConfigPersistence) GetDataDir() string {
							 | 
						|
									return cp.dataDir
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								// IsConfigured returns true if a data directory is configured
							 | 
						|
								func (cp *ConfigPersistence) IsConfigured() bool {
							 | 
						|
									return cp.dataDir != ""
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								// GetConfigInfo returns information about the configuration storage
							 | 
						|
								func (cp *ConfigPersistence) GetConfigInfo() map[string]interface{} {
							 | 
						|
									info := map[string]interface{}{
							 | 
						|
										"data_dir_configured": cp.IsConfigured(),
							 | 
						|
										"data_dir":            cp.dataDir,
							 | 
						|
										"config_subdir":       ConfigSubdir,
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									if cp.IsConfigured() {
							 | 
						|
										// Check if data directory exists
							 | 
						|
										if _, err := os.Stat(cp.dataDir); err == nil {
							 | 
						|
											info["data_dir_exists"] = true
							 | 
						|
								
							 | 
						|
											// Check if conf subdirectory exists
							 | 
						|
											confDir := filepath.Join(cp.dataDir, ConfigSubdir)
							 | 
						|
											if _, err := os.Stat(confDir); err == nil {
							 | 
						|
												info["conf_dir_exists"] = true
							 | 
						|
								
							 | 
						|
												// List config files
							 | 
						|
												configFiles, err := cp.ListConfigFiles()
							 | 
						|
												if err == nil {
							 | 
						|
													info["config_files"] = configFiles
							 | 
						|
												}
							 | 
						|
											} else {
							 | 
						|
												info["conf_dir_exists"] = false
							 | 
						|
											}
							 | 
						|
										} else {
							 | 
						|
											info["data_dir_exists"] = false
							 | 
						|
										}
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									return info
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								// buildPolicyFromTaskConfigs loads task configurations from separate files and builds a MaintenancePolicy
							 | 
						|
								func buildPolicyFromTaskConfigs() *worker_pb.MaintenancePolicy {
							 | 
						|
									policy := &worker_pb.MaintenancePolicy{
							 | 
						|
										GlobalMaxConcurrent:          4,
							 | 
						|
										DefaultRepeatIntervalSeconds: 6 * 3600,  // 6 hours in seconds
							 | 
						|
										DefaultCheckIntervalSeconds:  12 * 3600, // 12 hours in seconds
							 | 
						|
										TaskPolicies:                 make(map[string]*worker_pb.TaskPolicy),
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									// Load vacuum task configuration
							 | 
						|
									if vacuumConfig := vacuum.LoadConfigFromPersistence(nil); vacuumConfig != nil {
							 | 
						|
										policy.TaskPolicies["vacuum"] = &worker_pb.TaskPolicy{
							 | 
						|
											Enabled:               vacuumConfig.Enabled,
							 | 
						|
											MaxConcurrent:         int32(vacuumConfig.MaxConcurrent),
							 | 
						|
											RepeatIntervalSeconds: int32(vacuumConfig.ScanIntervalSeconds),
							 | 
						|
											CheckIntervalSeconds:  int32(vacuumConfig.ScanIntervalSeconds),
							 | 
						|
											TaskConfig: &worker_pb.TaskPolicy_VacuumConfig{
							 | 
						|
												VacuumConfig: &worker_pb.VacuumTaskConfig{
							 | 
						|
													GarbageThreshold:   float64(vacuumConfig.GarbageThreshold),
							 | 
						|
													MinVolumeAgeHours:  int32(vacuumConfig.MinVolumeAgeSeconds / 3600), // Convert seconds to hours
							 | 
						|
													MinIntervalSeconds: int32(vacuumConfig.MinIntervalSeconds),
							 | 
						|
												},
							 | 
						|
											},
							 | 
						|
										}
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									// Load erasure coding task configuration
							 | 
						|
									if ecConfig := erasure_coding.LoadConfigFromPersistence(nil); ecConfig != nil {
							 | 
						|
										policy.TaskPolicies["erasure_coding"] = &worker_pb.TaskPolicy{
							 | 
						|
											Enabled:               ecConfig.Enabled,
							 | 
						|
											MaxConcurrent:         int32(ecConfig.MaxConcurrent),
							 | 
						|
											RepeatIntervalSeconds: int32(ecConfig.ScanIntervalSeconds),
							 | 
						|
											CheckIntervalSeconds:  int32(ecConfig.ScanIntervalSeconds),
							 | 
						|
											TaskConfig: &worker_pb.TaskPolicy_ErasureCodingConfig{
							 | 
						|
												ErasureCodingConfig: &worker_pb.ErasureCodingTaskConfig{
							 | 
						|
													FullnessRatio:    float64(ecConfig.FullnessRatio),
							 | 
						|
													QuietForSeconds:  int32(ecConfig.QuietForSeconds),
							 | 
						|
													MinVolumeSizeMb:  int32(ecConfig.MinSizeMB),
							 | 
						|
													CollectionFilter: ecConfig.CollectionFilter,
							 | 
						|
												},
							 | 
						|
											},
							 | 
						|
										}
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									// Load balance task configuration
							 | 
						|
									if balanceConfig := balance.LoadConfigFromPersistence(nil); balanceConfig != nil {
							 | 
						|
										policy.TaskPolicies["balance"] = &worker_pb.TaskPolicy{
							 | 
						|
											Enabled:               balanceConfig.Enabled,
							 | 
						|
											MaxConcurrent:         int32(balanceConfig.MaxConcurrent),
							 | 
						|
											RepeatIntervalSeconds: int32(balanceConfig.ScanIntervalSeconds),
							 | 
						|
											CheckIntervalSeconds:  int32(balanceConfig.ScanIntervalSeconds),
							 | 
						|
											TaskConfig: &worker_pb.TaskPolicy_BalanceConfig{
							 | 
						|
												BalanceConfig: &worker_pb.BalanceTaskConfig{
							 | 
						|
													ImbalanceThreshold: float64(balanceConfig.ImbalanceThreshold),
							 | 
						|
													MinServerCount:     int32(balanceConfig.MinServerCount),
							 | 
						|
												},
							 | 
						|
											},
							 | 
						|
										}
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									glog.V(1).Infof("Built maintenance policy from separate task configs - %d task policies loaded", len(policy.TaskPolicies))
							 | 
						|
									return policy
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								// SaveTaskDetail saves detailed task information to disk
							 | 
						|
								func (cp *ConfigPersistence) SaveTaskDetail(taskID string, detail *maintenance.TaskDetailData) error {
							 | 
						|
									if cp.dataDir == "" {
							 | 
						|
										return fmt.Errorf("no data directory specified, cannot save task detail")
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									// Validate task ID to prevent path traversal
							 | 
						|
									if !isValidTaskID(taskID) {
							 | 
						|
										return fmt.Errorf("invalid task ID: %q contains illegal path characters", taskID)
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									taskDetailDir := filepath.Join(cp.dataDir, TaskDetailsSubdir)
							 | 
						|
									if err := os.MkdirAll(taskDetailDir, ConfigDirPermissions); err != nil {
							 | 
						|
										return fmt.Errorf("failed to create task details directory: %w", err)
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									// Save task detail as JSON for easy reading and debugging
							 | 
						|
									taskDetailPath := filepath.Join(taskDetailDir, fmt.Sprintf("%s.json", taskID))
							 | 
						|
									jsonData, err := json.MarshalIndent(detail, "", "  ")
							 | 
						|
									if err != nil {
							 | 
						|
										return fmt.Errorf("failed to marshal task detail to JSON: %w", err)
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									if err := os.WriteFile(taskDetailPath, jsonData, ConfigFilePermissions); err != nil {
							 | 
						|
										return fmt.Errorf("failed to write task detail file: %w", err)
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									glog.V(2).Infof("Saved task detail for task %s to %s", taskID, taskDetailPath)
							 | 
						|
									return nil
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								// LoadTaskDetail loads detailed task information from disk
							 | 
						|
								func (cp *ConfigPersistence) LoadTaskDetail(taskID string) (*maintenance.TaskDetailData, error) {
							 | 
						|
									if cp.dataDir == "" {
							 | 
						|
										return nil, fmt.Errorf("no data directory specified, cannot load task detail")
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									// Validate task ID to prevent path traversal
							 | 
						|
									if !isValidTaskID(taskID) {
							 | 
						|
										return nil, fmt.Errorf("invalid task ID: %q contains illegal path characters", taskID)
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									taskDetailPath := filepath.Join(cp.dataDir, TaskDetailsSubdir, fmt.Sprintf("%s.json", taskID))
							 | 
						|
									if _, err := os.Stat(taskDetailPath); os.IsNotExist(err) {
							 | 
						|
										return nil, fmt.Errorf("task detail file not found: %s", taskID)
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									jsonData, err := os.ReadFile(taskDetailPath)
							 | 
						|
									if err != nil {
							 | 
						|
										return nil, fmt.Errorf("failed to read task detail file: %w", err)
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									var detail maintenance.TaskDetailData
							 | 
						|
									if err := json.Unmarshal(jsonData, &detail); err != nil {
							 | 
						|
										return nil, fmt.Errorf("failed to unmarshal task detail JSON: %w", err)
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									glog.V(2).Infof("Loaded task detail for task %s from %s", taskID, taskDetailPath)
							 | 
						|
									return &detail, nil
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								// SaveTaskExecutionLogs saves execution logs for a task
							 | 
						|
								func (cp *ConfigPersistence) SaveTaskExecutionLogs(taskID string, logs []*maintenance.TaskExecutionLog) error {
							 | 
						|
									if cp.dataDir == "" {
							 | 
						|
										return fmt.Errorf("no data directory specified, cannot save task logs")
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									// Validate task ID to prevent path traversal
							 | 
						|
									if !isValidTaskID(taskID) {
							 | 
						|
										return fmt.Errorf("invalid task ID: %q contains illegal path characters", taskID)
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									taskLogsDir := filepath.Join(cp.dataDir, TaskLogsSubdir)
							 | 
						|
									if err := os.MkdirAll(taskLogsDir, ConfigDirPermissions); err != nil {
							 | 
						|
										return fmt.Errorf("failed to create task logs directory: %w", err)
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									// Save logs as JSON for easy reading
							 | 
						|
									taskLogsPath := filepath.Join(taskLogsDir, fmt.Sprintf("%s.json", taskID))
							 | 
						|
									logsData := struct {
							 | 
						|
										TaskID string                          `json:"task_id"`
							 | 
						|
										Logs   []*maintenance.TaskExecutionLog `json:"logs"`
							 | 
						|
									}{
							 | 
						|
										TaskID: taskID,
							 | 
						|
										Logs:   logs,
							 | 
						|
									}
							 | 
						|
									jsonData, err := json.MarshalIndent(logsData, "", "  ")
							 | 
						|
									if err != nil {
							 | 
						|
										return fmt.Errorf("failed to marshal task logs to JSON: %w", err)
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									if err := os.WriteFile(taskLogsPath, jsonData, ConfigFilePermissions); err != nil {
							 | 
						|
										return fmt.Errorf("failed to write task logs file: %w", err)
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									glog.V(2).Infof("Saved %d execution logs for task %s to %s", len(logs), taskID, taskLogsPath)
							 | 
						|
									return nil
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								// LoadTaskExecutionLogs loads execution logs for a task
							 | 
						|
								func (cp *ConfigPersistence) LoadTaskExecutionLogs(taskID string) ([]*maintenance.TaskExecutionLog, error) {
							 | 
						|
									if cp.dataDir == "" {
							 | 
						|
										return nil, fmt.Errorf("no data directory specified, cannot load task logs")
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									// Validate task ID to prevent path traversal
							 | 
						|
									if !isValidTaskID(taskID) {
							 | 
						|
										return nil, fmt.Errorf("invalid task ID: %q contains illegal path characters", taskID)
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									taskLogsPath := filepath.Join(cp.dataDir, TaskLogsSubdir, fmt.Sprintf("%s.json", taskID))
							 | 
						|
									if _, err := os.Stat(taskLogsPath); os.IsNotExist(err) {
							 | 
						|
										// Return empty slice if logs don't exist yet
							 | 
						|
										return []*maintenance.TaskExecutionLog{}, nil
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									jsonData, err := os.ReadFile(taskLogsPath)
							 | 
						|
									if err != nil {
							 | 
						|
										return nil, fmt.Errorf("failed to read task logs file: %w", err)
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									var logsData struct {
							 | 
						|
										TaskID string                          `json:"task_id"`
							 | 
						|
										Logs   []*maintenance.TaskExecutionLog `json:"logs"`
							 | 
						|
									}
							 | 
						|
									if err := json.Unmarshal(jsonData, &logsData); err != nil {
							 | 
						|
										return nil, fmt.Errorf("failed to unmarshal task logs JSON: %w", err)
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									glog.V(2).Infof("Loaded %d execution logs for task %s from %s", len(logsData.Logs), taskID, taskLogsPath)
							 | 
						|
									return logsData.Logs, nil
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								// DeleteTaskDetail removes task detail and logs from disk
							 | 
						|
								func (cp *ConfigPersistence) DeleteTaskDetail(taskID string) error {
							 | 
						|
									if cp.dataDir == "" {
							 | 
						|
										return fmt.Errorf("no data directory specified, cannot delete task detail")
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									// Validate task ID to prevent path traversal
							 | 
						|
									if !isValidTaskID(taskID) {
							 | 
						|
										return fmt.Errorf("invalid task ID: %q contains illegal path characters", taskID)
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									// Delete task detail file
							 | 
						|
									taskDetailPath := filepath.Join(cp.dataDir, TaskDetailsSubdir, fmt.Sprintf("%s.json", taskID))
							 | 
						|
									if err := os.Remove(taskDetailPath); err != nil && !os.IsNotExist(err) {
							 | 
						|
										return fmt.Errorf("failed to delete task detail file: %w", err)
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									// Delete task logs file
							 | 
						|
									taskLogsPath := filepath.Join(cp.dataDir, TaskLogsSubdir, fmt.Sprintf("%s.json", taskID))
							 | 
						|
									if err := os.Remove(taskLogsPath); err != nil && !os.IsNotExist(err) {
							 | 
						|
										return fmt.Errorf("failed to delete task logs file: %w", err)
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									glog.V(2).Infof("Deleted task detail and logs for task %s", taskID)
							 | 
						|
									return nil
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								// ListTaskDetails returns a list of all task IDs that have stored details
							 | 
						|
								func (cp *ConfigPersistence) ListTaskDetails() ([]string, error) {
							 | 
						|
									if cp.dataDir == "" {
							 | 
						|
										return nil, fmt.Errorf("no data directory specified, cannot list task details")
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									taskDetailDir := filepath.Join(cp.dataDir, TaskDetailsSubdir)
							 | 
						|
									if _, err := os.Stat(taskDetailDir); os.IsNotExist(err) {
							 | 
						|
										return []string{}, nil
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									entries, err := os.ReadDir(taskDetailDir)
							 | 
						|
									if err != nil {
							 | 
						|
										return nil, fmt.Errorf("failed to read task details directory: %w", err)
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									var taskIDs []string
							 | 
						|
									for _, entry := range entries {
							 | 
						|
										if !entry.IsDir() && filepath.Ext(entry.Name()) == ".json" {
							 | 
						|
											taskID := entry.Name()[:len(entry.Name())-5] // Remove .json extension
							 | 
						|
											taskIDs = append(taskIDs, taskID)
							 | 
						|
										}
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									return taskIDs, nil
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								// CleanupCompletedTasks removes old completed tasks beyond the retention limit
							 | 
						|
								func (cp *ConfigPersistence) CleanupCompletedTasks() error {
							 | 
						|
									if cp.dataDir == "" {
							 | 
						|
										return fmt.Errorf("no data directory specified, cannot cleanup completed tasks")
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									tasksDir := filepath.Join(cp.dataDir, TasksSubdir)
							 | 
						|
									if _, err := os.Stat(tasksDir); os.IsNotExist(err) {
							 | 
						|
										return nil // No tasks directory, nothing to cleanup
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									// Load all tasks and find completed/failed ones
							 | 
						|
									allTasks, err := cp.LoadAllTaskStates()
							 | 
						|
									if err != nil {
							 | 
						|
										return fmt.Errorf("failed to load tasks for cleanup: %w", err)
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									// Filter completed and failed tasks, sort by completion time
							 | 
						|
									var completedTasks []*maintenance.MaintenanceTask
							 | 
						|
									for _, task := range allTasks {
							 | 
						|
										if (task.Status == maintenance.TaskStatusCompleted || task.Status == maintenance.TaskStatusFailed) && task.CompletedAt != nil {
							 | 
						|
											completedTasks = append(completedTasks, task)
							 | 
						|
										}
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									// Sort by completion time (most recent first)
							 | 
						|
									sort.Slice(completedTasks, func(i, j int) bool {
							 | 
						|
										return completedTasks[i].CompletedAt.After(*completedTasks[j].CompletedAt)
							 | 
						|
									})
							 | 
						|
								
							 | 
						|
									// Keep only the most recent MaxCompletedTasks, delete the rest
							 | 
						|
									if len(completedTasks) > MaxCompletedTasks {
							 | 
						|
										tasksToDelete := completedTasks[MaxCompletedTasks:]
							 | 
						|
										for _, task := range tasksToDelete {
							 | 
						|
											if err := cp.DeleteTaskState(task.ID); err != nil {
							 | 
						|
												glog.Warningf("Failed to delete old completed task %s: %v", task.ID, err)
							 | 
						|
											} else {
							 | 
						|
												glog.V(2).Infof("Cleaned up old completed task %s (completed: %v)", task.ID, task.CompletedAt)
							 | 
						|
											}
							 | 
						|
										}
							 | 
						|
										glog.V(1).Infof("Cleaned up %d old completed tasks (keeping %d most recent)", len(tasksToDelete), MaxCompletedTasks)
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									return nil
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								// SaveTaskState saves a task state to protobuf file
							 | 
						|
								func (cp *ConfigPersistence) SaveTaskState(task *maintenance.MaintenanceTask) error {
							 | 
						|
									if cp.dataDir == "" {
							 | 
						|
										return fmt.Errorf("no data directory specified, cannot save task state")
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									// Validate task ID to prevent path traversal
							 | 
						|
									if !isValidTaskID(task.ID) {
							 | 
						|
										return fmt.Errorf("invalid task ID: %q contains illegal path characters", task.ID)
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									tasksDir := filepath.Join(cp.dataDir, TasksSubdir)
							 | 
						|
									if err := os.MkdirAll(tasksDir, ConfigDirPermissions); err != nil {
							 | 
						|
										return fmt.Errorf("failed to create tasks directory: %w", err)
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									taskFilePath := filepath.Join(tasksDir, fmt.Sprintf("%s.pb", task.ID))
							 | 
						|
								
							 | 
						|
									// Convert task to protobuf
							 | 
						|
									pbTask := cp.maintenanceTaskToProtobuf(task)
							 | 
						|
									taskStateFile := &worker_pb.TaskStateFile{
							 | 
						|
										Task:         pbTask,
							 | 
						|
										LastUpdated:  time.Now().Unix(),
							 | 
						|
										AdminVersion: "unknown", // TODO: add version info
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									pbData, err := proto.Marshal(taskStateFile)
							 | 
						|
									if err != nil {
							 | 
						|
										return fmt.Errorf("failed to marshal task state protobuf: %w", err)
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									if err := os.WriteFile(taskFilePath, pbData, ConfigFilePermissions); err != nil {
							 | 
						|
										return fmt.Errorf("failed to write task state file: %w", err)
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									glog.V(2).Infof("Saved task state for task %s to %s", task.ID, taskFilePath)
							 | 
						|
									return nil
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								// LoadTaskState loads a task state from protobuf file
							 | 
						|
								func (cp *ConfigPersistence) LoadTaskState(taskID string) (*maintenance.MaintenanceTask, error) {
							 | 
						|
									if cp.dataDir == "" {
							 | 
						|
										return nil, fmt.Errorf("no data directory specified, cannot load task state")
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									// Validate task ID to prevent path traversal
							 | 
						|
									if !isValidTaskID(taskID) {
							 | 
						|
										return nil, fmt.Errorf("invalid task ID: %q contains illegal path characters", taskID)
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									taskFilePath := filepath.Join(cp.dataDir, TasksSubdir, fmt.Sprintf("%s.pb", taskID))
							 | 
						|
									if _, err := os.Stat(taskFilePath); os.IsNotExist(err) {
							 | 
						|
										return nil, fmt.Errorf("task state file not found: %s", taskID)
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									pbData, err := os.ReadFile(taskFilePath)
							 | 
						|
									if err != nil {
							 | 
						|
										return nil, fmt.Errorf("failed to read task state file: %w", err)
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									var taskStateFile worker_pb.TaskStateFile
							 | 
						|
									if err := proto.Unmarshal(pbData, &taskStateFile); err != nil {
							 | 
						|
										return nil, fmt.Errorf("failed to unmarshal task state protobuf: %w", err)
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									// Convert protobuf to maintenance task
							 | 
						|
									task := cp.protobufToMaintenanceTask(taskStateFile.Task)
							 | 
						|
								
							 | 
						|
									glog.V(2).Infof("Loaded task state for task %s from %s", taskID, taskFilePath)
							 | 
						|
									return task, nil
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								// LoadAllTaskStates loads all task states from disk
							 | 
						|
								func (cp *ConfigPersistence) LoadAllTaskStates() ([]*maintenance.MaintenanceTask, error) {
							 | 
						|
									if cp.dataDir == "" {
							 | 
						|
										return []*maintenance.MaintenanceTask{}, nil
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									tasksDir := filepath.Join(cp.dataDir, TasksSubdir)
							 | 
						|
									if _, err := os.Stat(tasksDir); os.IsNotExist(err) {
							 | 
						|
										return []*maintenance.MaintenanceTask{}, nil
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									entries, err := os.ReadDir(tasksDir)
							 | 
						|
									if err != nil {
							 | 
						|
										return nil, fmt.Errorf("failed to read tasks directory: %w", err)
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									var tasks []*maintenance.MaintenanceTask
							 | 
						|
									for _, entry := range entries {
							 | 
						|
										if !entry.IsDir() && filepath.Ext(entry.Name()) == ".pb" {
							 | 
						|
											taskID := entry.Name()[:len(entry.Name())-3] // Remove .pb extension
							 | 
						|
											task, err := cp.LoadTaskState(taskID)
							 | 
						|
											if err != nil {
							 | 
						|
												glog.Warningf("Failed to load task state for %s: %v", taskID, err)
							 | 
						|
												continue
							 | 
						|
											}
							 | 
						|
											tasks = append(tasks, task)
							 | 
						|
										}
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									glog.V(1).Infof("Loaded %d task states from disk", len(tasks))
							 | 
						|
									return tasks, nil
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								// DeleteTaskState removes a task state file from disk
							 | 
						|
								func (cp *ConfigPersistence) DeleteTaskState(taskID string) error {
							 | 
						|
									if cp.dataDir == "" {
							 | 
						|
										return fmt.Errorf("no data directory specified, cannot delete task state")
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									// Validate task ID to prevent path traversal
							 | 
						|
									if !isValidTaskID(taskID) {
							 | 
						|
										return fmt.Errorf("invalid task ID: %q contains illegal path characters", taskID)
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									taskFilePath := filepath.Join(cp.dataDir, TasksSubdir, fmt.Sprintf("%s.pb", taskID))
							 | 
						|
									if err := os.Remove(taskFilePath); err != nil && !os.IsNotExist(err) {
							 | 
						|
										return fmt.Errorf("failed to delete task state file: %w", err)
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									glog.V(2).Infof("Deleted task state for task %s", taskID)
							 | 
						|
									return nil
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								// maintenanceTaskToProtobuf converts a MaintenanceTask to protobuf format
							 | 
						|
								func (cp *ConfigPersistence) maintenanceTaskToProtobuf(task *maintenance.MaintenanceTask) *worker_pb.MaintenanceTaskData {
							 | 
						|
									pbTask := &worker_pb.MaintenanceTaskData{
							 | 
						|
										Id:              task.ID,
							 | 
						|
										Type:            string(task.Type),
							 | 
						|
										Priority:        cp.priorityToString(task.Priority),
							 | 
						|
										Status:          string(task.Status),
							 | 
						|
										VolumeId:        task.VolumeID,
							 | 
						|
										Server:          task.Server,
							 | 
						|
										Collection:      task.Collection,
							 | 
						|
										Reason:          task.Reason,
							 | 
						|
										CreatedAt:       task.CreatedAt.Unix(),
							 | 
						|
										ScheduledAt:     task.ScheduledAt.Unix(),
							 | 
						|
										WorkerId:        task.WorkerID,
							 | 
						|
										Error:           task.Error,
							 | 
						|
										Progress:        task.Progress,
							 | 
						|
										RetryCount:      int32(task.RetryCount),
							 | 
						|
										MaxRetries:      int32(task.MaxRetries),
							 | 
						|
										CreatedBy:       task.CreatedBy,
							 | 
						|
										CreationContext: task.CreationContext,
							 | 
						|
										DetailedReason:  task.DetailedReason,
							 | 
						|
										Tags:            task.Tags,
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									// Handle optional timestamps
							 | 
						|
									if task.StartedAt != nil {
							 | 
						|
										pbTask.StartedAt = task.StartedAt.Unix()
							 | 
						|
									}
							 | 
						|
									if task.CompletedAt != nil {
							 | 
						|
										pbTask.CompletedAt = task.CompletedAt.Unix()
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									// Convert assignment history
							 | 
						|
									if task.AssignmentHistory != nil {
							 | 
						|
										for _, record := range task.AssignmentHistory {
							 | 
						|
											pbRecord := &worker_pb.TaskAssignmentRecord{
							 | 
						|
												WorkerId:      record.WorkerID,
							 | 
						|
												WorkerAddress: record.WorkerAddress,
							 | 
						|
												AssignedAt:    record.AssignedAt.Unix(),
							 | 
						|
												Reason:        record.Reason,
							 | 
						|
											}
							 | 
						|
											if record.UnassignedAt != nil {
							 | 
						|
												pbRecord.UnassignedAt = record.UnassignedAt.Unix()
							 | 
						|
											}
							 | 
						|
											pbTask.AssignmentHistory = append(pbTask.AssignmentHistory, pbRecord)
							 | 
						|
										}
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									// Convert typed parameters if available
							 | 
						|
									if task.TypedParams != nil {
							 | 
						|
										pbTask.TypedParams = task.TypedParams
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									return pbTask
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								// protobufToMaintenanceTask converts protobuf format to MaintenanceTask
							 | 
						|
								func (cp *ConfigPersistence) protobufToMaintenanceTask(pbTask *worker_pb.MaintenanceTaskData) *maintenance.MaintenanceTask {
							 | 
						|
									task := &maintenance.MaintenanceTask{
							 | 
						|
										ID:              pbTask.Id,
							 | 
						|
										Type:            maintenance.MaintenanceTaskType(pbTask.Type),
							 | 
						|
										Priority:        cp.stringToPriority(pbTask.Priority),
							 | 
						|
										Status:          maintenance.MaintenanceTaskStatus(pbTask.Status),
							 | 
						|
										VolumeID:        pbTask.VolumeId,
							 | 
						|
										Server:          pbTask.Server,
							 | 
						|
										Collection:      pbTask.Collection,
							 | 
						|
										Reason:          pbTask.Reason,
							 | 
						|
										CreatedAt:       time.Unix(pbTask.CreatedAt, 0),
							 | 
						|
										ScheduledAt:     time.Unix(pbTask.ScheduledAt, 0),
							 | 
						|
										WorkerID:        pbTask.WorkerId,
							 | 
						|
										Error:           pbTask.Error,
							 | 
						|
										Progress:        pbTask.Progress,
							 | 
						|
										RetryCount:      int(pbTask.RetryCount),
							 | 
						|
										MaxRetries:      int(pbTask.MaxRetries),
							 | 
						|
										CreatedBy:       pbTask.CreatedBy,
							 | 
						|
										CreationContext: pbTask.CreationContext,
							 | 
						|
										DetailedReason:  pbTask.DetailedReason,
							 | 
						|
										Tags:            pbTask.Tags,
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									// Handle optional timestamps
							 | 
						|
									if pbTask.StartedAt > 0 {
							 | 
						|
										startTime := time.Unix(pbTask.StartedAt, 0)
							 | 
						|
										task.StartedAt = &startTime
							 | 
						|
									}
							 | 
						|
									if pbTask.CompletedAt > 0 {
							 | 
						|
										completedTime := time.Unix(pbTask.CompletedAt, 0)
							 | 
						|
										task.CompletedAt = &completedTime
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									// Convert assignment history
							 | 
						|
									if pbTask.AssignmentHistory != nil {
							 | 
						|
										task.AssignmentHistory = make([]*maintenance.TaskAssignmentRecord, 0, len(pbTask.AssignmentHistory))
							 | 
						|
										for _, pbRecord := range pbTask.AssignmentHistory {
							 | 
						|
											record := &maintenance.TaskAssignmentRecord{
							 | 
						|
												WorkerID:      pbRecord.WorkerId,
							 | 
						|
												WorkerAddress: pbRecord.WorkerAddress,
							 | 
						|
												AssignedAt:    time.Unix(pbRecord.AssignedAt, 0),
							 | 
						|
												Reason:        pbRecord.Reason,
							 | 
						|
											}
							 | 
						|
											if pbRecord.UnassignedAt > 0 {
							 | 
						|
												unassignedTime := time.Unix(pbRecord.UnassignedAt, 0)
							 | 
						|
												record.UnassignedAt = &unassignedTime
							 | 
						|
											}
							 | 
						|
											task.AssignmentHistory = append(task.AssignmentHistory, record)
							 | 
						|
										}
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									// Convert typed parameters if available
							 | 
						|
									if pbTask.TypedParams != nil {
							 | 
						|
										task.TypedParams = pbTask.TypedParams
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									return task
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								// priorityToString converts MaintenanceTaskPriority to string for protobuf storage
							 | 
						|
								func (cp *ConfigPersistence) priorityToString(priority maintenance.MaintenanceTaskPriority) string {
							 | 
						|
									switch priority {
							 | 
						|
									case maintenance.PriorityLow:
							 | 
						|
										return "low"
							 | 
						|
									case maintenance.PriorityNormal:
							 | 
						|
										return "normal"
							 | 
						|
									case maintenance.PriorityHigh:
							 | 
						|
										return "high"
							 | 
						|
									case maintenance.PriorityCritical:
							 | 
						|
										return "critical"
							 | 
						|
									default:
							 | 
						|
										return "normal"
							 | 
						|
									}
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								// stringToPriority converts string from protobuf to MaintenanceTaskPriority
							 | 
						|
								func (cp *ConfigPersistence) stringToPriority(priorityStr string) maintenance.MaintenanceTaskPriority {
							 | 
						|
									switch priorityStr {
							 | 
						|
									case "low":
							 | 
						|
										return maintenance.PriorityLow
							 | 
						|
									case "normal":
							 | 
						|
										return maintenance.PriorityNormal
							 | 
						|
									case "high":
							 | 
						|
										return maintenance.PriorityHigh
							 | 
						|
									case "critical":
							 | 
						|
										return maintenance.PriorityCritical
							 | 
						|
									default:
							 | 
						|
										return maintenance.PriorityNormal
							 | 
						|
									}
							 | 
						|
								}
							 |