package balance import ( "fmt" "html/template" "strconv" "time" "github.com/seaweedfs/seaweedfs/weed/glog" "github.com/seaweedfs/seaweedfs/weed/worker/types" ) // UIProvider provides the UI for balance task configuration type UIProvider struct { detector *BalanceDetector scheduler *BalanceScheduler } // NewUIProvider creates a new balance UI provider func NewUIProvider(detector *BalanceDetector, scheduler *BalanceScheduler) *UIProvider { return &UIProvider{ detector: detector, scheduler: scheduler, } } // GetTaskType returns the task type func (ui *UIProvider) GetTaskType() types.TaskType { return types.TaskTypeBalance } // GetDisplayName returns the human-readable name func (ui *UIProvider) GetDisplayName() string { return "Volume Balance" } // GetDescription returns a description of what this task does func (ui *UIProvider) GetDescription() string { return "Redistributes volumes across volume servers to optimize storage utilization and performance" } // GetIcon returns the icon CSS class for this task type func (ui *UIProvider) GetIcon() string { return "fas fa-balance-scale text-secondary" } // BalanceConfig represents the balance configuration type BalanceConfig struct { Enabled bool `json:"enabled"` ImbalanceThreshold float64 `json:"imbalance_threshold"` ScanIntervalSeconds int `json:"scan_interval_seconds"` MaxConcurrent int `json:"max_concurrent"` MinServerCount int `json:"min_server_count"` MoveDuringOffHours bool `json:"move_during_off_hours"` OffHoursStart string `json:"off_hours_start"` OffHoursEnd string `json:"off_hours_end"` MinIntervalSeconds int `json:"min_interval_seconds"` } // Helper functions for duration conversion func secondsToDuration(seconds int) time.Duration { return time.Duration(seconds) * time.Second } func durationToSeconds(d time.Duration) int { return int(d.Seconds()) } // formatDurationForUser formats seconds as a user-friendly duration string func formatDurationForUser(seconds int) string { d := secondsToDuration(seconds) if d < time.Minute { return fmt.Sprintf("%ds", seconds) } if d < time.Hour { return fmt.Sprintf("%.0fm", d.Minutes()) } if d < 24*time.Hour { return fmt.Sprintf("%.1fh", d.Hours()) } return fmt.Sprintf("%.1fd", d.Hours()/24) } // RenderConfigForm renders the configuration form HTML func (ui *UIProvider) RenderConfigForm(currentConfig interface{}) (template.HTML, error) { config := ui.getCurrentBalanceConfig() // Build form using the FormBuilder helper form := types.NewFormBuilder() // Detection Settings form.AddCheckboxField( "enabled", "Enable Balance Tasks", "Whether balance tasks should be automatically created", config.Enabled, ) form.AddNumberField( "imbalance_threshold", "Imbalance Threshold (%)", "Trigger balance when storage imbalance exceeds this percentage (0.0-1.0)", config.ImbalanceThreshold, true, ) form.AddDurationField("scan_interval", "Scan Interval", "How often to scan for imbalanced volumes", secondsToDuration(config.ScanIntervalSeconds), true) // Scheduling Settings form.AddNumberField( "max_concurrent", "Max Concurrent Tasks", "Maximum number of balance tasks that can run simultaneously", float64(config.MaxConcurrent), true, ) form.AddNumberField( "min_server_count", "Minimum Server Count", "Only balance when at least this many servers are available", float64(config.MinServerCount), true, ) // Timing Settings form.AddCheckboxField( "move_during_off_hours", "Restrict to Off-Hours", "Only perform balance operations during off-peak hours", config.MoveDuringOffHours, ) form.AddTextField( "off_hours_start", "Off-Hours Start Time", "Start time for off-hours window (e.g., 23:00)", config.OffHoursStart, false, ) form.AddTextField( "off_hours_end", "Off-Hours End Time", "End time for off-hours window (e.g., 06:00)", config.OffHoursEnd, false, ) // Timing constraints form.AddDurationField("min_interval", "Min Interval", "Minimum time between balance operations", secondsToDuration(config.MinIntervalSeconds), true) // Generate organized form sections using Bootstrap components html := `
Balance Configuration
` + string(form.Build()) + `
Performance Considerations
` return template.HTML(html), nil } // ParseConfigForm parses form data into configuration func (ui *UIProvider) ParseConfigForm(formData map[string][]string) (interface{}, error) { config := &BalanceConfig{} // Parse enabled config.Enabled = len(formData["enabled"]) > 0 // Parse imbalance threshold if values, ok := formData["imbalance_threshold"]; ok && len(values) > 0 { threshold, err := strconv.ParseFloat(values[0], 64) if err != nil { return nil, fmt.Errorf("invalid imbalance threshold: %v", err) } if threshold < 0 || threshold > 1 { return nil, fmt.Errorf("imbalance threshold must be between 0.0 and 1.0") } config.ImbalanceThreshold = threshold } // Parse scan interval if values, ok := formData["scan_interval"]; ok && len(values) > 0 { duration, err := time.ParseDuration(values[0]) if err != nil { return nil, fmt.Errorf("invalid scan interval: %v", err) } config.ScanIntervalSeconds = int(duration.Seconds()) } // Parse max concurrent if values, ok := formData["max_concurrent"]; ok && len(values) > 0 { maxConcurrent, err := strconv.Atoi(values[0]) if err != nil { return nil, fmt.Errorf("invalid max concurrent: %v", err) } if maxConcurrent < 1 { return nil, fmt.Errorf("max concurrent must be at least 1") } config.MaxConcurrent = maxConcurrent } // Parse min server count if values, ok := formData["min_server_count"]; ok && len(values) > 0 { minServerCount, err := strconv.Atoi(values[0]) if err != nil { return nil, fmt.Errorf("invalid min server count: %v", err) } if minServerCount < 2 { return nil, fmt.Errorf("min server count must be at least 2") } config.MinServerCount = minServerCount } // Parse off-hours settings config.MoveDuringOffHours = len(formData["move_during_off_hours"]) > 0 if values, ok := formData["off_hours_start"]; ok && len(values) > 0 { config.OffHoursStart = values[0] } if values, ok := formData["off_hours_end"]; ok && len(values) > 0 { config.OffHoursEnd = values[0] } // Parse min interval if values, ok := formData["min_interval"]; ok && len(values) > 0 { duration, err := time.ParseDuration(values[0]) if err != nil { return nil, fmt.Errorf("invalid min interval: %v", err) } config.MinIntervalSeconds = int(duration.Seconds()) } return config, nil } // GetCurrentConfig returns the current configuration func (ui *UIProvider) GetCurrentConfig() interface{} { return ui.getCurrentBalanceConfig() } // ApplyConfig applies the new configuration func (ui *UIProvider) ApplyConfig(config interface{}) error { balanceConfig, ok := config.(*BalanceConfig) if !ok { return fmt.Errorf("invalid config type, expected *BalanceConfig") } // Apply to detector if ui.detector != nil { ui.detector.SetEnabled(balanceConfig.Enabled) ui.detector.SetThreshold(balanceConfig.ImbalanceThreshold) ui.detector.SetMinCheckInterval(secondsToDuration(balanceConfig.ScanIntervalSeconds)) } // Apply to scheduler if ui.scheduler != nil { ui.scheduler.SetEnabled(balanceConfig.Enabled) ui.scheduler.SetMaxConcurrent(balanceConfig.MaxConcurrent) ui.scheduler.SetMinServerCount(balanceConfig.MinServerCount) ui.scheduler.SetMoveDuringOffHours(balanceConfig.MoveDuringOffHours) ui.scheduler.SetOffHoursStart(balanceConfig.OffHoursStart) ui.scheduler.SetOffHoursEnd(balanceConfig.OffHoursEnd) } glog.V(1).Infof("Applied balance configuration: enabled=%v, threshold=%.1f%%, max_concurrent=%d, min_servers=%d, off_hours=%v", balanceConfig.Enabled, balanceConfig.ImbalanceThreshold*100, balanceConfig.MaxConcurrent, balanceConfig.MinServerCount, balanceConfig.MoveDuringOffHours) return nil } // getCurrentBalanceConfig gets the current configuration from detector and scheduler func (ui *UIProvider) getCurrentBalanceConfig() *BalanceConfig { config := &BalanceConfig{ // Default values (fallback if detectors/schedulers are nil) Enabled: true, ImbalanceThreshold: 0.1, // 10% imbalance ScanIntervalSeconds: durationToSeconds(4 * time.Hour), MaxConcurrent: 1, MinServerCount: 3, MoveDuringOffHours: true, OffHoursStart: "23:00", OffHoursEnd: "06:00", MinIntervalSeconds: durationToSeconds(1 * time.Hour), } // Get current values from detector if ui.detector != nil { config.Enabled = ui.detector.IsEnabled() config.ImbalanceThreshold = ui.detector.GetThreshold() config.ScanIntervalSeconds = int(ui.detector.ScanInterval().Seconds()) } // Get current values from scheduler if ui.scheduler != nil { config.MaxConcurrent = ui.scheduler.GetMaxConcurrent() config.MinServerCount = ui.scheduler.GetMinServerCount() config.MoveDuringOffHours = ui.scheduler.GetMoveDuringOffHours() config.OffHoursStart = ui.scheduler.GetOffHoursStart() config.OffHoursEnd = ui.scheduler.GetOffHoursEnd() } return config } // RegisterUI registers the balance UI provider with the UI registry func RegisterUI(uiRegistry *types.UIRegistry, detector *BalanceDetector, scheduler *BalanceScheduler) { uiProvider := NewUIProvider(detector, scheduler) uiRegistry.RegisterUI(uiProvider) glog.V(1).Infof("✅ Registered balance task UI provider") } // DefaultBalanceConfig returns default balance configuration func DefaultBalanceConfig() *BalanceConfig { return &BalanceConfig{ Enabled: false, ImbalanceThreshold: 0.3, ScanIntervalSeconds: durationToSeconds(4 * time.Hour), MaxConcurrent: 1, MinServerCount: 3, MoveDuringOffHours: false, OffHoursStart: "22:00", OffHoursEnd: "06:00", MinIntervalSeconds: durationToSeconds(1 * time.Hour), } }