diff options
Diffstat (limited to 'weed/admin/handlers/maintenance_handlers.go')
| -rw-r--r-- | weed/admin/handlers/maintenance_handlers.go | 502 |
1 files changed, 331 insertions, 171 deletions
diff --git a/weed/admin/handlers/maintenance_handlers.go b/weed/admin/handlers/maintenance_handlers.go index 4b1f91387..1e2337272 100644 --- a/weed/admin/handlers/maintenance_handlers.go +++ b/weed/admin/handlers/maintenance_handlers.go @@ -1,16 +1,24 @@ package handlers import ( + "fmt" "net/http" + "reflect" + "strconv" + "strings" "time" "github.com/gin-gonic/gin" + "github.com/seaweedfs/seaweedfs/weed/admin/config" "github.com/seaweedfs/seaweedfs/weed/admin/dash" "github.com/seaweedfs/seaweedfs/weed/admin/maintenance" "github.com/seaweedfs/seaweedfs/weed/admin/view/app" - "github.com/seaweedfs/seaweedfs/weed/admin/view/components" "github.com/seaweedfs/seaweedfs/weed/admin/view/layout" + "github.com/seaweedfs/seaweedfs/weed/glog" "github.com/seaweedfs/seaweedfs/weed/worker/tasks" + "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" "github.com/seaweedfs/seaweedfs/weed/worker/types" ) @@ -30,19 +38,31 @@ func NewMaintenanceHandlers(adminServer *dash.AdminServer) *MaintenanceHandlers func (h *MaintenanceHandlers) ShowMaintenanceQueue(c *gin.Context) { data, err := h.getMaintenanceQueueData() if err != nil { + glog.Infof("DEBUG ShowMaintenanceQueue: error getting data: %v", err) c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } + glog.Infof("DEBUG ShowMaintenanceQueue: got data with %d tasks", len(data.Tasks)) + if data.Stats != nil { + glog.Infof("DEBUG ShowMaintenanceQueue: stats = {pending: %d, running: %d, completed: %d}", + data.Stats.PendingTasks, data.Stats.RunningTasks, data.Stats.CompletedToday) + } else { + glog.Infof("DEBUG ShowMaintenanceQueue: stats is nil") + } + // Render HTML template c.Header("Content-Type", "text/html") maintenanceComponent := app.MaintenanceQueue(data) layoutComponent := layout.Layout(c, maintenanceComponent) err = layoutComponent.Render(c.Request.Context(), c.Writer) if err != nil { + glog.Infof("DEBUG ShowMaintenanceQueue: render error: %v", err) c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to render template: " + err.Error()}) return } + + glog.Infof("DEBUG ShowMaintenanceQueue: template rendered successfully") } // ShowMaintenanceWorkers displays the maintenance workers page @@ -72,9 +92,12 @@ func (h *MaintenanceHandlers) ShowMaintenanceConfig(c *gin.Context) { return } - // Render HTML template + // Get the schema for dynamic form rendering + schema := maintenance.GetMaintenanceConfigSchema() + + // Render HTML template using schema-driven approach c.Header("Content-Type", "text/html") - configComponent := app.MaintenanceConfig(config) + configComponent := app.MaintenanceConfigSchema(config, schema) layoutComponent := layout.Layout(c, configComponent) err = layoutComponent.Render(c.Request.Context(), c.Writer) if err != nil { @@ -87,20 +110,20 @@ func (h *MaintenanceHandlers) ShowMaintenanceConfig(c *gin.Context) { func (h *MaintenanceHandlers) ShowTaskConfig(c *gin.Context) { taskTypeName := c.Param("taskType") - // Get the task type - taskType := maintenance.GetMaintenanceTaskType(taskTypeName) - if taskType == "" { - c.JSON(http.StatusNotFound, gin.H{"error": "Task type not found"}) + // Get the schema for this task type + schema := tasks.GetTaskConfigSchema(taskTypeName) + if schema == nil { + c.JSON(http.StatusNotFound, gin.H{"error": "Task type not found or no schema available"}) return } - // Get the UI provider for this task type + // Get the UI provider for current configuration uiRegistry := tasks.GetGlobalUIRegistry() typesRegistry := tasks.GetGlobalTypesRegistry() var provider types.TaskUIProvider for workerTaskType := range typesRegistry.GetAllDetectors() { - if string(workerTaskType) == string(taskType) { + if string(workerTaskType) == taskTypeName { provider = uiRegistry.GetProvider(workerTaskType) break } @@ -111,73 +134,23 @@ func (h *MaintenanceHandlers) ShowTaskConfig(c *gin.Context) { return } - // Try to get templ UI provider first - temporarily disabled - // templUIProvider := getTemplUIProvider(taskType) - var configSections []components.ConfigSectionData - - // Temporarily disabled templ UI provider - // if templUIProvider != nil { - // // Use the new templ-based UI provider - // currentConfig := templUIProvider.GetCurrentConfig() - // sections, err := templUIProvider.RenderConfigSections(currentConfig) - // if err != nil { - // c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to render configuration sections: " + err.Error()}) - // return - // } - // configSections = sections - // } else { - // Fallback to basic configuration for providers that haven't been migrated yet - configSections = []components.ConfigSectionData{ - { - Title: "Configuration Settings", - Icon: "fas fa-cogs", - Description: "Configure task detection and scheduling parameters", - Fields: []interface{}{ - components.CheckboxFieldData{ - FormFieldData: components.FormFieldData{ - Name: "enabled", - Label: "Enable Task", - Description: "Whether this task type should be enabled", - }, - Checked: true, - }, - components.NumberFieldData{ - FormFieldData: components.FormFieldData{ - Name: "max_concurrent", - Label: "Max Concurrent Tasks", - Description: "Maximum number of concurrent tasks", - Required: true, - }, - Value: 2, - Step: "1", - Min: floatPtr(1), - }, - components.DurationFieldData{ - FormFieldData: components.FormFieldData{ - Name: "scan_interval", - Label: "Scan Interval", - Description: "How often to scan for tasks", - Required: true, - }, - Value: "30m", - }, - }, - }, - } - // } // End of disabled templ UI provider else block - - // Create task configuration data using templ components - configData := &app.TaskConfigTemplData{ - TaskType: taskType, - TaskName: provider.GetDisplayName(), - TaskIcon: provider.GetIcon(), - Description: provider.GetDescription(), - ConfigSections: configSections, - } - - // Render HTML template using templ components + // Get current configuration + currentConfig := provider.GetCurrentConfig() + + // Note: Do NOT apply schema defaults to current config as it overrides saved values + // Only apply defaults when creating new configs, not when displaying existing ones + + // Create task configuration data + configData := &maintenance.TaskConfigData{ + TaskType: maintenance.MaintenanceTaskType(taskTypeName), + TaskName: schema.DisplayName, + TaskIcon: schema.Icon, + Description: schema.Description, + } + + // Render HTML template using schema-based approach c.Header("Content-Type", "text/html") - taskConfigComponent := app.TaskConfigTempl(configData) + taskConfigComponent := app.TaskConfigSchema(configData, schema, currentConfig) layoutComponent := layout.Layout(c, taskConfigComponent) err := layoutComponent.Render(c.Request.Context(), c.Writer) if err != nil { @@ -186,19 +159,10 @@ func (h *MaintenanceHandlers) ShowTaskConfig(c *gin.Context) { } } -// UpdateTaskConfig updates configuration for a specific task type +// UpdateTaskConfig updates task configuration from form func (h *MaintenanceHandlers) UpdateTaskConfig(c *gin.Context) { taskTypeName := c.Param("taskType") - - // Get the task type - taskType := maintenance.GetMaintenanceTaskType(taskTypeName) - if taskType == "" { - c.JSON(http.StatusNotFound, gin.H{"error": "Task type not found"}) - return - } - - // Try to get templ UI provider first - temporarily disabled - // templUIProvider := getTemplUIProvider(taskType) + taskType := types.TaskType(taskTypeName) // Parse form data err := c.Request.ParseForm() @@ -207,31 +171,100 @@ func (h *MaintenanceHandlers) UpdateTaskConfig(c *gin.Context) { return } - // Convert form data to map - formData := make(map[string][]string) + // Debug logging - show received form data + glog.V(1).Infof("Received form data for task type %s:", taskTypeName) for key, values := range c.Request.PostForm { - formData[key] = values - } - - var config interface{} - - // Temporarily disabled templ UI provider - // if templUIProvider != nil { - // // Use the new templ-based UI provider - // config, err = templUIProvider.ParseConfigForm(formData) - // if err != nil { - // c.JSON(http.StatusBadRequest, gin.H{"error": "Failed to parse configuration: " + err.Error()}) - // return - // } - // // Apply configuration using templ provider - // err = templUIProvider.ApplyConfig(config) - // if err != nil { - // c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to apply configuration: " + err.Error()}) - // return - // } - // } else { - // Fallback to old UI provider for tasks that haven't been migrated yet - // Fallback to old UI provider for tasks that haven't been migrated yet + glog.V(1).Infof(" %s: %v", key, values) + } + + // Get the task configuration schema + schema := tasks.GetTaskConfigSchema(taskTypeName) + if schema == nil { + c.JSON(http.StatusNotFound, gin.H{"error": "Schema not found for task type: " + taskTypeName}) + return + } + + // Create a new config instance based on task type and apply schema defaults + var config TaskConfig + switch taskType { + case types.TaskTypeVacuum: + config = &vacuum.Config{} + case types.TaskTypeBalance: + config = &balance.Config{} + case types.TaskTypeErasureCoding: + config = &erasure_coding.Config{} + default: + c.JSON(http.StatusBadRequest, gin.H{"error": "Unsupported task type: " + taskTypeName}) + return + } + + // Apply schema defaults first using type-safe method + if err := schema.ApplyDefaultsToConfig(config); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to apply defaults: " + err.Error()}) + return + } + + // First, get the current configuration to preserve existing values + currentUIRegistry := tasks.GetGlobalUIRegistry() + currentTypesRegistry := tasks.GetGlobalTypesRegistry() + + var currentProvider types.TaskUIProvider + for workerTaskType := range currentTypesRegistry.GetAllDetectors() { + if string(workerTaskType) == string(taskType) { + currentProvider = currentUIRegistry.GetProvider(workerTaskType) + break + } + } + + if currentProvider != nil { + // Copy current config values to the new config + currentConfig := currentProvider.GetCurrentConfig() + if currentConfigProtobuf, ok := currentConfig.(TaskConfig); ok { + // Apply current values using protobuf directly - no map conversion needed! + currentPolicy := currentConfigProtobuf.ToTaskPolicy() + if err := config.FromTaskPolicy(currentPolicy); err != nil { + glog.Warningf("Failed to load current config for %s: %v", taskTypeName, err) + } + } + } + + // Parse form data using schema-based approach (this will override with new values) + err = h.parseTaskConfigFromForm(c.Request.PostForm, schema, config) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "Failed to parse configuration: " + err.Error()}) + return + } + + // Debug logging - show parsed config values + switch taskType { + case types.TaskTypeVacuum: + if vacuumConfig, ok := config.(*vacuum.Config); ok { + glog.V(1).Infof("Parsed vacuum config - GarbageThreshold: %f, MinVolumeAgeSeconds: %d, MinIntervalSeconds: %d", + vacuumConfig.GarbageThreshold, vacuumConfig.MinVolumeAgeSeconds, vacuumConfig.MinIntervalSeconds) + } + case types.TaskTypeErasureCoding: + if ecConfig, ok := config.(*erasure_coding.Config); ok { + glog.V(1).Infof("Parsed EC config - FullnessRatio: %f, QuietForSeconds: %d, MinSizeMB: %d, CollectionFilter: '%s'", + ecConfig.FullnessRatio, ecConfig.QuietForSeconds, ecConfig.MinSizeMB, ecConfig.CollectionFilter) + } + case types.TaskTypeBalance: + if balanceConfig, ok := config.(*balance.Config); ok { + glog.V(1).Infof("Parsed balance config - Enabled: %v, MaxConcurrent: %d, ScanIntervalSeconds: %d, ImbalanceThreshold: %f, MinServerCount: %d", + balanceConfig.Enabled, balanceConfig.MaxConcurrent, balanceConfig.ScanIntervalSeconds, balanceConfig.ImbalanceThreshold, balanceConfig.MinServerCount) + } + } + + // Validate the configuration + if validationErrors := schema.ValidateConfig(config); len(validationErrors) > 0 { + errorMessages := make([]string, len(validationErrors)) + for i, err := range validationErrors { + errorMessages[i] = err.Error() + } + c.JSON(http.StatusBadRequest, gin.H{"error": "Configuration validation failed", "details": errorMessages}) + return + } + + // Apply configuration using UIProvider uiRegistry := tasks.GetGlobalUIRegistry() typesRegistry := tasks.GetGlobalTypesRegistry() @@ -248,25 +281,153 @@ func (h *MaintenanceHandlers) UpdateTaskConfig(c *gin.Context) { return } - // Parse configuration from form using old provider - config, err = provider.ParseConfigForm(formData) + // Apply configuration using provider + err = provider.ApplyTaskConfig(config) if err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": "Failed to parse configuration: " + err.Error()}) + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to apply configuration: " + err.Error()}) return } - // Apply configuration using old provider - err = provider.ApplyConfig(config) - if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to apply configuration: " + err.Error()}) - return + // Save task configuration to protobuf file using ConfigPersistence + if h.adminServer != nil && h.adminServer.GetConfigPersistence() != nil { + err = h.saveTaskConfigToProtobuf(taskType, config) + if err != nil { + glog.Warningf("Failed to save task config to protobuf file: %v", err) + // Don't fail the request, just log the warning + } + } + + // Trigger a configuration reload in the maintenance manager + if h.adminServer != nil { + if manager := h.adminServer.GetMaintenanceManager(); manager != nil { + err = manager.ReloadTaskConfigurations() + if err != nil { + glog.Warningf("Failed to reload task configurations: %v", err) + } else { + glog.V(1).Infof("Successfully reloaded task configurations after updating %s", taskTypeName) + } + } } - // } // End of disabled templ UI provider else block // Redirect back to task configuration page c.Redirect(http.StatusSeeOther, "/maintenance/config/"+taskTypeName) } +// parseTaskConfigFromForm parses form data using schema definitions +func (h *MaintenanceHandlers) parseTaskConfigFromForm(formData map[string][]string, schema *tasks.TaskConfigSchema, config interface{}) error { + configValue := reflect.ValueOf(config) + if configValue.Kind() == reflect.Ptr { + configValue = configValue.Elem() + } + + if configValue.Kind() != reflect.Struct { + return fmt.Errorf("config must be a struct or pointer to struct") + } + + configType := configValue.Type() + + for i := 0; i < configValue.NumField(); i++ { + field := configValue.Field(i) + fieldType := configType.Field(i) + + // Handle embedded structs recursively + if fieldType.Anonymous && field.Kind() == reflect.Struct { + err := h.parseTaskConfigFromForm(formData, schema, field.Addr().Interface()) + if err != nil { + return fmt.Errorf("error parsing embedded struct %s: %w", fieldType.Name, err) + } + continue + } + + // Get JSON tag name + jsonTag := fieldType.Tag.Get("json") + if jsonTag == "" { + continue + } + + // Remove options like ",omitempty" + if commaIdx := strings.Index(jsonTag, ","); commaIdx > 0 { + jsonTag = jsonTag[:commaIdx] + } + + // Find corresponding schema field + schemaField := schema.GetFieldByName(jsonTag) + if schemaField == nil { + continue + } + + // Parse value based on field type + if err := h.parseFieldFromForm(formData, schemaField, field); err != nil { + return fmt.Errorf("error parsing field %s: %w", schemaField.DisplayName, err) + } + } + + return nil +} + +// parseFieldFromForm parses a single field value from form data +func (h *MaintenanceHandlers) parseFieldFromForm(formData map[string][]string, schemaField *config.Field, fieldValue reflect.Value) error { + if !fieldValue.CanSet() { + return nil + } + + switch schemaField.Type { + case config.FieldTypeBool: + // Checkbox fields - present means true, absent means false + _, exists := formData[schemaField.JSONName] + fieldValue.SetBool(exists) + + case config.FieldTypeInt: + if values, ok := formData[schemaField.JSONName]; ok && len(values) > 0 { + if intVal, err := strconv.Atoi(values[0]); err != nil { + return fmt.Errorf("invalid integer value: %s", values[0]) + } else { + fieldValue.SetInt(int64(intVal)) + } + } + + case config.FieldTypeFloat: + if values, ok := formData[schemaField.JSONName]; ok && len(values) > 0 { + if floatVal, err := strconv.ParseFloat(values[0], 64); err != nil { + return fmt.Errorf("invalid float value: %s", values[0]) + } else { + fieldValue.SetFloat(floatVal) + } + } + + case config.FieldTypeString: + if values, ok := formData[schemaField.JSONName]; ok && len(values) > 0 { + fieldValue.SetString(values[0]) + } + + case config.FieldTypeInterval: + // Parse interval fields with value + unit + valueKey := schemaField.JSONName + "_value" + unitKey := schemaField.JSONName + "_unit" + + if valueStrs, ok := formData[valueKey]; ok && len(valueStrs) > 0 { + value, err := strconv.Atoi(valueStrs[0]) + if err != nil { + return fmt.Errorf("invalid interval value: %s", valueStrs[0]) + } + + unit := "minutes" // default + if unitStrs, ok := formData[unitKey]; ok && len(unitStrs) > 0 { + unit = unitStrs[0] + } + + // Convert to seconds + seconds := config.IntervalValueUnitToSeconds(value, unit) + fieldValue.SetInt(int64(seconds)) + } + + default: + return fmt.Errorf("unsupported field type: %s", schemaField.Type) + } + + return nil +} + // UpdateMaintenanceConfig updates maintenance configuration from form func (h *MaintenanceHandlers) UpdateMaintenanceConfig(c *gin.Context) { var config maintenance.MaintenanceConfig @@ -302,36 +463,50 @@ func (h *MaintenanceHandlers) getMaintenanceQueueData() (*maintenance.Maintenanc return nil, err } - return &maintenance.MaintenanceQueueData{ + data := &maintenance.MaintenanceQueueData{ Tasks: tasks, Workers: workers, Stats: stats, LastUpdated: time.Now(), - }, nil + } + + return data, nil } func (h *MaintenanceHandlers) getMaintenanceQueueStats() (*maintenance.QueueStats, error) { - // This would integrate with the maintenance queue to get real statistics - // For now, return mock data - return &maintenance.QueueStats{ - PendingTasks: 5, - RunningTasks: 2, - CompletedToday: 15, - FailedToday: 1, - TotalTasks: 23, - }, nil + // Use the exported method from AdminServer + return h.adminServer.GetMaintenanceQueueStats() } func (h *MaintenanceHandlers) getMaintenanceTasks() ([]*maintenance.MaintenanceTask, error) { - // This would integrate with the maintenance queue to get real tasks - // For now, return mock data - return []*maintenance.MaintenanceTask{}, nil + // Call the maintenance manager directly to get all tasks + if h.adminServer == nil { + return []*maintenance.MaintenanceTask{}, nil + } + + manager := h.adminServer.GetMaintenanceManager() + if manager == nil { + return []*maintenance.MaintenanceTask{}, nil + } + + // Get ALL tasks using empty parameters - this should match what the API returns + allTasks := manager.GetTasks("", "", 0) + return allTasks, nil } func (h *MaintenanceHandlers) getMaintenanceWorkers() ([]*maintenance.MaintenanceWorker, error) { - // This would integrate with the maintenance system to get real workers - // For now, return mock data - return []*maintenance.MaintenanceWorker{}, nil + // Get workers from the admin server's maintenance manager + if h.adminServer == nil { + return []*maintenance.MaintenanceWorker{}, nil + } + + if h.adminServer.GetMaintenanceManager() == nil { + return []*maintenance.MaintenanceWorker{}, nil + } + + // Get workers from the maintenance manager + workers := h.adminServer.GetMaintenanceManager().GetWorkers() + return workers, nil } func (h *MaintenanceHandlers) getMaintenanceConfig() (*maintenance.MaintenanceConfigData, error) { @@ -344,40 +519,25 @@ func (h *MaintenanceHandlers) updateMaintenanceConfig(config *maintenance.Mainte return h.adminServer.UpdateMaintenanceConfigData(config) } -// floatPtr is a helper function to create float64 pointers -func floatPtr(f float64) *float64 { - return &f -} - -// Global templ UI registry - temporarily disabled -// var globalTemplUIRegistry *types.UITemplRegistry - -// initTemplUIRegistry initializes the global templ UI registry - temporarily disabled -func initTemplUIRegistry() { - // Temporarily disabled due to missing types - // if globalTemplUIRegistry == nil { - // globalTemplUIRegistry = types.NewUITemplRegistry() - // // Register vacuum templ UI provider using shared instances - // vacuumDetector, vacuumScheduler := vacuum.GetSharedInstances() - // vacuum.RegisterUITempl(globalTemplUIRegistry, vacuumDetector, vacuumScheduler) - // // Register erasure coding templ UI provider using shared instances - // erasureCodingDetector, erasureCodingScheduler := erasure_coding.GetSharedInstances() - // erasure_coding.RegisterUITempl(globalTemplUIRegistry, erasureCodingDetector, erasureCodingScheduler) - // // Register balance templ UI provider using shared instances - // balanceDetector, balanceScheduler := balance.GetSharedInstances() - // balance.RegisterUITempl(globalTemplUIRegistry, balanceDetector, balanceScheduler) - // } -} +// saveTaskConfigToProtobuf saves task configuration to protobuf file +func (h *MaintenanceHandlers) saveTaskConfigToProtobuf(taskType types.TaskType, config TaskConfig) error { + configPersistence := h.adminServer.GetConfigPersistence() + if configPersistence == nil { + return fmt.Errorf("config persistence not available") + } -// getTemplUIProvider gets the templ UI provider for a task type - temporarily disabled -func getTemplUIProvider(taskType maintenance.MaintenanceTaskType) interface{} { - // initTemplUIRegistry() - // Convert maintenance task type to worker task type - // typesRegistry := tasks.GetGlobalTypesRegistry() - // for workerTaskType := range typesRegistry.GetAllDetectors() { - // if string(workerTaskType) == string(taskType) { - // return globalTemplUIRegistry.GetProvider(workerTaskType) - // } - // } - return nil + // Use the new ToTaskPolicy method - much simpler and more maintainable! + taskPolicy := config.ToTaskPolicy() + + // Save using task-specific methods + switch taskType { + case types.TaskTypeVacuum: + return configPersistence.SaveVacuumTaskPolicy(taskPolicy) + case types.TaskTypeErasureCoding: + return configPersistence.SaveErasureCodingTaskPolicy(taskPolicy) + case types.TaskTypeBalance: + return configPersistence.SaveBalanceTaskPolicy(taskPolicy) + default: + return fmt.Errorf("unsupported task type for protobuf persistence: %s", taskType) + } } |
