yoohoo
This commit is contained in:
347
internal/ai/client.go
Normal file
347
internal/ai/client.go
Normal file
@@ -0,0 +1,347 @@
|
||||
package ai
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.gostacks.org/iwasforcedtobehere/WiseTLP/autotlp/internal/config"
|
||||
"git.gostacks.org/iwasforcedtobehere/WiseTLP/autotlp/pkg/types"
|
||||
"git.gostacks.org/iwasforcedtobehere/WiseTLP/autotlp/pkg/utils"
|
||||
)
|
||||
|
||||
type Client struct {
|
||||
config *types.AIConfig
|
||||
httpClient *http.Client
|
||||
logger *utils.Logger
|
||||
}
|
||||
|
||||
func NewClient(config *types.AIConfig, logger *utils.Logger) *Client {
|
||||
return &Client{
|
||||
config: config,
|
||||
httpClient: &http.Client{
|
||||
Timeout: 30 * time.Second,
|
||||
},
|
||||
logger: logger.WithComponent("ai"),
|
||||
}
|
||||
}
|
||||
|
||||
func ConfigureClient(logger *utils.Logger) (*Client, error) {
|
||||
fmt.Println("\n" + strings.Repeat("=", 50))
|
||||
fmt.Println("AI SERVICE CONFIGURATION")
|
||||
fmt.Println(strings.Repeat("=", 50))
|
||||
|
||||
config := &types.AIConfig{}
|
||||
|
||||
fmt.Println("\nAvailable AI providers:")
|
||||
fmt.Println("1. Groq (Fast inference)")
|
||||
fmt.Println("2. OpenRouter (Multiple models)")
|
||||
fmt.Println("3. Gemini (Google)")
|
||||
fmt.Println("4. Custom (OpenAI-compatible)")
|
||||
|
||||
choice := utils.GetUserInput("Select provider (1-4)", "1")
|
||||
|
||||
switch choice {
|
||||
case "1":
|
||||
config.Provider = types.AIProviderGroq
|
||||
config.Endpoint = "https://api.groq.com/openai/v1/chat/completions"
|
||||
config.Model = "openai/gpt-oss-20b"
|
||||
case "2":
|
||||
config.Provider = types.AIProviderOpenRouter
|
||||
config.Endpoint = "https://openrouter.ai/api/v1/chat/completions"
|
||||
config.Model = utils.GetUserInput("Model name", "meta-llama/llama-3.1-8b-instruct:free")
|
||||
case "3":
|
||||
config.Provider = types.AIProviderGemini
|
||||
config.Endpoint = "https://generativelanguage.googleapis.com/v1beta/models/gemini-pro:generateContent"
|
||||
config.Model = "gemini-pro"
|
||||
case "4":
|
||||
config.Provider = types.AIProviderCustom
|
||||
config.Endpoint = utils.GetUserInput("API endpoint", "")
|
||||
config.Model = utils.GetUserInput("Model name", "")
|
||||
default:
|
||||
return nil, fmt.Errorf("invalid provider selection")
|
||||
}
|
||||
|
||||
fmt.Printf("\nEnter your API key for %s: ", config.Provider)
|
||||
var apiKey string
|
||||
fmt.Scanln(&apiKey)
|
||||
config.APIKey = strings.TrimSpace(apiKey)
|
||||
|
||||
if config.APIKey == "" {
|
||||
return nil, fmt.Errorf("API key is required")
|
||||
}
|
||||
|
||||
config.MaxTokens = 1500
|
||||
config.Temperature = 0.3
|
||||
|
||||
client := NewClient(config, logger)
|
||||
if err := client.validateConnection(context.Background()); err != nil {
|
||||
return nil, fmt.Errorf("failed to validate AI connection: %w", err)
|
||||
}
|
||||
|
||||
fmt.Println("✓ AI service configured successfully!")
|
||||
return client, nil
|
||||
}
|
||||
|
||||
func (c *Client) GenerateConfig(ctx context.Context, sysInfo *types.SystemInfo, preferences *types.UserPreferences) (*types.TLPConfiguration, error) {
|
||||
c.logger.Info("Generating TLP configuration using AI", "provider", c.config.Provider, "model", c.config.Model)
|
||||
|
||||
prompt := c.buildPrompt(sysInfo, preferences)
|
||||
response, err := c.makeRequest(ctx, prompt)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("AI request failed: %w", err)
|
||||
}
|
||||
|
||||
config, err := c.parseResponse(response, sysInfo, preferences)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse AI response: %w", err)
|
||||
}
|
||||
|
||||
c.logger.Info("TLP configuration generated successfully", "settings_count", len(config.Settings))
|
||||
return config, nil
|
||||
}
|
||||
|
||||
func (c *Client) buildPrompt(sysInfo *types.SystemInfo, preferences *types.UserPreferences) string {
|
||||
var prompt strings.Builder
|
||||
|
||||
prompt.WriteString("Generate TLP configuration in JSON format.\n\n")
|
||||
|
||||
prompt.WriteString("System: ")
|
||||
if sysInfo.Battery != nil && sysInfo.Battery.Present {
|
||||
prompt.WriteString("Laptop")
|
||||
} else {
|
||||
prompt.WriteString("Desktop")
|
||||
}
|
||||
prompt.WriteString(fmt.Sprintf(", %s, %d cores\n", sysInfo.Distribution.ID, sysInfo.CPU.Cores))
|
||||
|
||||
prompt.WriteString(fmt.Sprintf("Profile: %s, Use: %s, Mode: %s\n\n", preferences.PowerProfile, preferences.UseCase, preferences.PerformanceMode))
|
||||
|
||||
prompt.WriteString("Return JSON with this structure:\n")
|
||||
prompt.WriteString(`{"description": "Config description", "settings": {"TLP_ENABLE": "1", "CPU_SCALING_GOVERNOR_ON_AC": "performance", "CPU_SCALING_GOVERNOR_ON_BAT": "powersave", "DISK_APM_LEVEL_ON_AC": "254", "DISK_APM_LEVEL_ON_BAT": "128", "WIFI_PWR_ON_AC": "off", "WIFI_PWR_ON_BAT": "on", "USB_AUTOSUSPEND": "1"}, "rationale": {"CPU_SCALING_GOVERNOR_ON_AC": "Max performance on AC"}, "warnings": ["Monitor temperatures"]}` + "\n\n")
|
||||
|
||||
prompt.WriteString("Generate 8-10 TLP settings for this system.")
|
||||
|
||||
return prompt.String()
|
||||
}
|
||||
|
||||
func (c *Client) makeRequest(ctx context.Context, prompt string) (string, error) {
|
||||
c.logger.Debug("Making AI request", "prompt_length", len(prompt))
|
||||
c.logger.Debug("Full prompt being sent", "prompt", prompt)
|
||||
|
||||
var requestBody interface{}
|
||||
var endpoint string
|
||||
|
||||
switch c.config.Provider {
|
||||
case types.AIProviderGemini:
|
||||
requestBody = map[string]interface{}{
|
||||
"contents": []map[string]interface{}{
|
||||
{
|
||||
"parts": []map[string]string{
|
||||
{"text": prompt},
|
||||
},
|
||||
},
|
||||
},
|
||||
"generationConfig": map[string]interface{}{
|
||||
"temperature": c.config.Temperature,
|
||||
"maxOutputTokens": c.config.MaxTokens,
|
||||
},
|
||||
}
|
||||
endpoint = c.config.Endpoint + "?key=" + c.config.APIKey
|
||||
default:
|
||||
requestBody = map[string]interface{}{
|
||||
"model": c.config.Model,
|
||||
"messages": []map[string]interface{}{
|
||||
{
|
||||
"role": "user",
|
||||
"content": prompt,
|
||||
},
|
||||
},
|
||||
"max_tokens": c.config.MaxTokens,
|
||||
"temperature": c.config.Temperature,
|
||||
"response_format": map[string]interface{}{
|
||||
"type": "json_object",
|
||||
},
|
||||
}
|
||||
endpoint = c.config.Endpoint
|
||||
}
|
||||
|
||||
jsonBody, err := json.Marshal(requestBody)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to marshal request: %w", err)
|
||||
}
|
||||
|
||||
c.logger.Debug("Request body", "json", string(jsonBody))
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, "POST", endpoint, bytes.NewBuffer(jsonBody))
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to create request: %w", err)
|
||||
}
|
||||
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
if c.config.Provider != types.AIProviderGemini {
|
||||
req.Header.Set("Authorization", "Bearer "+c.config.APIKey)
|
||||
}
|
||||
|
||||
resp, err := c.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("request failed: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to read response: %w", err)
|
||||
}
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return "", fmt.Errorf("API request failed with status %d: %s", resp.StatusCode, string(body))
|
||||
}
|
||||
|
||||
return c.extractContent(body)
|
||||
}
|
||||
|
||||
func (c *Client) extractContent(responseBody []byte) (string, error) {
|
||||
var response map[string]interface{}
|
||||
if err := json.Unmarshal(responseBody, &response); err != nil {
|
||||
return "", fmt.Errorf("failed to parse response JSON: %w", err)
|
||||
}
|
||||
|
||||
switch c.config.Provider {
|
||||
case types.AIProviderGemini:
|
||||
if candidates, ok := response["candidates"].([]interface{}); ok && len(candidates) > 0 {
|
||||
if candidate, ok := candidates[0].(map[string]interface{}); ok {
|
||||
if content, ok := candidate["content"].(map[string]interface{}); ok {
|
||||
if parts, ok := content["parts"].([]interface{}); ok && len(parts) > 0 {
|
||||
if part, ok := parts[0].(map[string]interface{}); ok {
|
||||
if text, ok := part["text"].(string); ok {
|
||||
return text, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
default:
|
||||
if choices, ok := response["choices"].([]interface{}); ok && len(choices) > 0 {
|
||||
if choice, ok := choices[0].(map[string]interface{}); ok {
|
||||
if message, ok := choice["message"].(map[string]interface{}); ok {
|
||||
if content, ok := message["content"].(string); ok {
|
||||
return content, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return "", fmt.Errorf("unexpected response format")
|
||||
}
|
||||
|
||||
func (c *Client) parseResponse(response string, sysInfo *types.SystemInfo, preferences *types.UserPreferences) (*types.TLPConfiguration, error) {
|
||||
c.logger.Debug("Raw AI response", "response", response)
|
||||
|
||||
start := strings.Index(response, "{")
|
||||
end := strings.LastIndex(response, "}") + 1
|
||||
|
||||
if start == -1 || end == 0 {
|
||||
c.logger.Error("No JSON found in AI response", "response_length", len(response), "response_preview", response[:min(200, len(response))])
|
||||
c.logger.Info("Generating fallback TLP configuration")
|
||||
return c.generateFallbackConfig(sysInfo, preferences), nil
|
||||
}
|
||||
|
||||
jsonStr := response[start:end]
|
||||
|
||||
var aiResponse struct {
|
||||
Description string `json:"description"`
|
||||
Settings map[string]string `json:"settings"`
|
||||
Rationale map[string]string `json:"rationale"`
|
||||
Warnings []string `json:"warnings"`
|
||||
}
|
||||
|
||||
if err := json.Unmarshal([]byte(jsonStr), &aiResponse); err != nil {
|
||||
c.logger.Error("Failed to parse AI JSON response", "error", err, "json_str", jsonStr)
|
||||
c.logger.Info("Generating fallback TLP configuration due to JSON parse error")
|
||||
return c.generateFallbackConfig(sysInfo, preferences), nil
|
||||
}
|
||||
|
||||
if len(aiResponse.Settings) == 0 {
|
||||
return nil, fmt.Errorf("AI response contains no settings")
|
||||
}
|
||||
|
||||
config := &types.TLPConfiguration{
|
||||
Settings: aiResponse.Settings,
|
||||
Description: aiResponse.Description,
|
||||
Rationale: aiResponse.Rationale,
|
||||
Warnings: aiResponse.Warnings,
|
||||
Generated: time.Now(),
|
||||
SystemInfo: sysInfo,
|
||||
Preferences: preferences,
|
||||
}
|
||||
|
||||
if _, exists := config.Settings["TLP_ENABLE"]; !exists {
|
||||
config.Settings["TLP_ENABLE"] = "1"
|
||||
}
|
||||
|
||||
if config.Rationale == nil {
|
||||
config.Rationale = make(map[string]string)
|
||||
}
|
||||
|
||||
return config, nil
|
||||
}
|
||||
|
||||
func (c *Client) validateConnection(ctx context.Context) error {
|
||||
testPrompt := "Respond with a JSON object containing 'status': 'OK' to confirm the connection is working."
|
||||
|
||||
response, err := c.makeRequest(ctx, testPrompt)
|
||||
if err != nil {
|
||||
return fmt.Errorf("connection test failed: %w", err)
|
||||
}
|
||||
|
||||
if !strings.Contains(strings.ToUpper(response), "OK") {
|
||||
return fmt.Errorf("unexpected response from AI service: %s", response)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func min(a, b int) int {
|
||||
if a < b {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
func (c *Client) generateFallbackConfig(sysInfo *types.SystemInfo, preferences *types.UserPreferences) *types.TLPConfiguration {
|
||||
c.logger.Info("Generating fallback configuration based on user preferences")
|
||||
|
||||
settings := config.GetRecommendedSettings(preferences)
|
||||
|
||||
rationale := make(map[string]string)
|
||||
rationale["TLP_ENABLE"] = "Enable TLP power management"
|
||||
rationale["CPU_SCALING_GOVERNOR_ON_AC"] = "CPU governor for AC power based on user preferences"
|
||||
rationale["CPU_SCALING_GOVERNOR_ON_BAT"] = "CPU governor for battery power based on user preferences"
|
||||
|
||||
warnings := []string{
|
||||
"This is a fallback configuration generated when AI service was unavailable",
|
||||
"Configuration is based on user preferences and common best practices",
|
||||
"Consider running WiseTLP again when AI service is available for optimized settings",
|
||||
}
|
||||
|
||||
description := fmt.Sprintf("Fallback TLP configuration for %s use case with %s power profile",
|
||||
preferences.UseCase, preferences.PowerProfile)
|
||||
|
||||
return &types.TLPConfiguration{
|
||||
Settings: settings,
|
||||
Description: description,
|
||||
Rationale: rationale,
|
||||
Warnings: warnings,
|
||||
Generated: time.Now(),
|
||||
SystemInfo: sysInfo,
|
||||
Preferences: preferences,
|
||||
}
|
||||
}
|
346
internal/config/preferences.go
Normal file
346
internal/config/preferences.go
Normal file
@@ -0,0 +1,346 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"git.gostacks.org/iwasforcedtobehere/WiseTLP/autotlp/pkg/types"
|
||||
"git.gostacks.org/iwasforcedtobehere/WiseTLP/autotlp/pkg/utils"
|
||||
)
|
||||
|
||||
// Config represents the application configuration
|
||||
type Config struct {
|
||||
// Application-wide configuration settings
|
||||
}
|
||||
|
||||
// New creates a new configuration instance
|
||||
func New() *Config {
|
||||
return &Config{}
|
||||
}
|
||||
|
||||
// GatherUserPreferences interactively collects user preferences
|
||||
func GatherUserPreferences() (*types.UserPreferences, error) {
|
||||
fmt.Println("\n" + strings.Repeat("=", 60))
|
||||
fmt.Println("USER PREFERENCES CONFIGURATION")
|
||||
fmt.Println(strings.Repeat("=", 60))
|
||||
|
||||
preferences := &types.UserPreferences{
|
||||
CustomSettings: make(map[string]interface{}),
|
||||
SpecialRequirements: []string{},
|
||||
}
|
||||
|
||||
// Power Profile
|
||||
fmt.Println("\n1. Power Profile Selection")
|
||||
fmt.Println("Choose your preferred power management approach:")
|
||||
fmt.Println(" a) Balanced - Good compromise between performance and power saving")
|
||||
fmt.Println(" b) Performance - Maximum performance, higher power consumption")
|
||||
fmt.Println(" c) Power Saving - Maximum battery life, reduced performance")
|
||||
fmt.Println(" d) Custom - I'll specify custom requirements")
|
||||
|
||||
profileChoice := utils.GetUserInput("Select power profile (a/b/c/d)", "a")
|
||||
switch strings.ToLower(profileChoice) {
|
||||
case "a", "balanced":
|
||||
preferences.PowerProfile = types.PowerProfileBalanced
|
||||
case "b", "performance":
|
||||
preferences.PowerProfile = types.PowerProfilePerformance
|
||||
case "c", "power saving", "powersaving":
|
||||
preferences.PowerProfile = types.PowerProfilePowerSaving
|
||||
case "d", "custom":
|
||||
preferences.PowerProfile = types.PowerProfileCustom
|
||||
default:
|
||||
preferences.PowerProfile = types.PowerProfileBalanced
|
||||
}
|
||||
|
||||
// Use Case
|
||||
fmt.Println("\n2. Primary Use Case")
|
||||
fmt.Println("What do you primarily use this system for?")
|
||||
fmt.Println(" a) General use - Web browsing, office work, media consumption")
|
||||
fmt.Println(" b) Development - Programming, compiling, development tools")
|
||||
fmt.Println(" c) Gaming - Gaming and graphics-intensive applications")
|
||||
fmt.Println(" d) Server - Server workloads, always-on services")
|
||||
fmt.Println(" e) Multimedia - Video editing, rendering, content creation")
|
||||
fmt.Println(" f) Office - Primarily office applications and productivity")
|
||||
|
||||
useCaseChoice := utils.GetUserInput("Select use case (a/b/c/d/e/f)", "a")
|
||||
switch strings.ToLower(useCaseChoice) {
|
||||
case "a", "general":
|
||||
preferences.UseCase = types.UseCaseGeneral
|
||||
case "b", "development", "dev":
|
||||
preferences.UseCase = types.UseCaseDevelopment
|
||||
case "c", "gaming":
|
||||
preferences.UseCase = types.UseCaseGaming
|
||||
case "d", "server":
|
||||
preferences.UseCase = types.UseCaseServer
|
||||
case "e", "multimedia", "media":
|
||||
preferences.UseCase = types.UseCaseMultimedia
|
||||
case "f", "office":
|
||||
preferences.UseCase = types.UseCaseOffice
|
||||
default:
|
||||
preferences.UseCase = types.UseCaseGeneral
|
||||
}
|
||||
|
||||
// Battery Priority (only for systems with batteries)
|
||||
fmt.Println("\n3. Battery Optimization Priority")
|
||||
fmt.Println("How would you like to optimize battery usage?")
|
||||
fmt.Println(" a) Balanced - Balance between battery life and longevity")
|
||||
fmt.Println(" b) Runtime - Maximize single-charge runtime")
|
||||
fmt.Println(" c) Longevity - Maximize battery lifespan over years")
|
||||
|
||||
batteryChoice := utils.GetUserInput("Select battery priority (a/b/c)", "a")
|
||||
switch strings.ToLower(batteryChoice) {
|
||||
case "a", "balanced":
|
||||
preferences.BatteryPriority = types.BatteryPriorityBalanced
|
||||
case "b", "runtime":
|
||||
preferences.BatteryPriority = types.BatteryPriorityRuntime
|
||||
case "c", "longevity":
|
||||
preferences.BatteryPriority = types.BatteryPriorityLongevity
|
||||
default:
|
||||
preferences.BatteryPriority = types.BatteryPriorityBalanced
|
||||
}
|
||||
|
||||
// Performance Mode
|
||||
fmt.Println("\n4. Performance Mode")
|
||||
fmt.Println("How should the system handle performance scaling?")
|
||||
fmt.Println(" a) Adaptive - Automatically adjust based on workload")
|
||||
fmt.Println(" b) Maximum - Always prefer maximum performance")
|
||||
fmt.Println(" c) Efficient - Always prefer energy efficiency")
|
||||
|
||||
perfChoice := utils.GetUserInput("Select performance mode (a/b/c)", "a")
|
||||
switch strings.ToLower(perfChoice) {
|
||||
case "a", "adaptive":
|
||||
preferences.PerformanceMode = types.PerformanceModeAdaptive
|
||||
case "b", "maximum", "max":
|
||||
preferences.PerformanceMode = types.PerformanceModeMaximum
|
||||
case "c", "efficient", "efficiency":
|
||||
preferences.PerformanceMode = types.PerformanceModeEfficient
|
||||
default:
|
||||
preferences.PerformanceMode = types.PerformanceModeAdaptive
|
||||
}
|
||||
|
||||
// Special Requirements
|
||||
fmt.Println("\n5. Special Requirements")
|
||||
fmt.Println("Do you have any special requirements? (Select all that apply)")
|
||||
fmt.Println(" a) Minimize fan noise")
|
||||
fmt.Println(" b) Prevent thermal throttling")
|
||||
fmt.Println(" c) Optimize for external displays")
|
||||
fmt.Println(" d) Gaming performance priority")
|
||||
fmt.Println(" e) Maximum WiFi performance")
|
||||
fmt.Println(" f) Minimize disk wear")
|
||||
fmt.Println(" g) Fast system wake/sleep")
|
||||
fmt.Println(" h) None of the above")
|
||||
|
||||
requirements := utils.GetUserInput("Enter letters separated by spaces (e.g., 'a c f')", "h")
|
||||
if strings.ToLower(requirements) != "h" {
|
||||
reqMap := map[string]string{
|
||||
"a": "Minimize fan noise",
|
||||
"b": "Prevent thermal throttling",
|
||||
"c": "Optimize for external displays",
|
||||
"d": "Gaming performance priority",
|
||||
"e": "Maximum WiFi performance",
|
||||
"f": "Minimize disk wear",
|
||||
"g": "Fast system wake/sleep",
|
||||
}
|
||||
|
||||
reqLetters := strings.Fields(strings.ToLower(requirements))
|
||||
for _, letter := range reqLetters {
|
||||
if req, exists := reqMap[letter]; exists {
|
||||
preferences.SpecialRequirements = append(preferences.SpecialRequirements, req)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Custom Settings (for advanced users)
|
||||
if preferences.PowerProfile == types.PowerProfileCustom {
|
||||
fmt.Println("\n6. Custom Settings")
|
||||
fmt.Println("You selected custom power profile. You can specify additional settings.")
|
||||
|
||||
if utils.GetUserConfirmation("Do you want to specify custom CPU governor settings?") {
|
||||
acGovernor := utils.GetUserInput("CPU governor on AC power (performance/powersave/ondemand/conservative/schedutil)", "performance")
|
||||
batGovernor := utils.GetUserInput("CPU governor on battery (performance/powersave/ondemand/conservative/schedutil)", "powersave")
|
||||
|
||||
preferences.CustomSettings["cpu_governor_ac"] = acGovernor
|
||||
preferences.CustomSettings["cpu_governor_battery"] = batGovernor
|
||||
}
|
||||
|
||||
if utils.GetUserConfirmation("Do you want to specify custom disk settings?") {
|
||||
diskAPMAC := utils.GetUserInput("Disk APM level on AC (1-255, higher = more aggressive)", "254")
|
||||
diskAPMBat := utils.GetUserInput("Disk APM level on battery (1-255, higher = more aggressive)", "128")
|
||||
|
||||
preferences.CustomSettings["disk_apm_ac"] = diskAPMAC
|
||||
preferences.CustomSettings["disk_apm_battery"] = diskAPMBat
|
||||
}
|
||||
}
|
||||
|
||||
// Summary
|
||||
fmt.Println("\n" + strings.Repeat("-", 60))
|
||||
fmt.Println("PREFERENCES SUMMARY")
|
||||
fmt.Println(strings.Repeat("-", 60))
|
||||
fmt.Printf("Power Profile: %s\n", preferences.PowerProfile)
|
||||
fmt.Printf("Use Case: %s\n", preferences.UseCase)
|
||||
fmt.Printf("Battery Priority: %s\n", preferences.BatteryPriority)
|
||||
fmt.Printf("Performance Mode: %s\n", preferences.PerformanceMode)
|
||||
|
||||
if len(preferences.SpecialRequirements) > 0 {
|
||||
fmt.Printf("Special Requirements: %s\n", strings.Join(preferences.SpecialRequirements, ", "))
|
||||
}
|
||||
|
||||
if len(preferences.CustomSettings) > 0 {
|
||||
fmt.Println("Custom Settings:")
|
||||
for key, value := range preferences.CustomSettings {
|
||||
fmt.Printf(" %s: %v\n", key, value)
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Println(strings.Repeat("-", 60))
|
||||
|
||||
if !utils.GetUserConfirmation("Are these preferences correct?") {
|
||||
fmt.Println("Let's try again...")
|
||||
return GatherUserPreferences()
|
||||
}
|
||||
|
||||
return preferences, nil
|
||||
}
|
||||
|
||||
// ValidatePreferences validates user preferences for consistency
|
||||
func ValidatePreferences(preferences *types.UserPreferences) error {
|
||||
if preferences == nil {
|
||||
return fmt.Errorf("preferences cannot be nil")
|
||||
}
|
||||
|
||||
// Validate power profile
|
||||
validProfiles := []types.PowerProfile{
|
||||
types.PowerProfileBalanced,
|
||||
types.PowerProfilePerformance,
|
||||
types.PowerProfilePowerSaving,
|
||||
types.PowerProfileCustom,
|
||||
}
|
||||
|
||||
validProfile := false
|
||||
for _, profile := range validProfiles {
|
||||
if preferences.PowerProfile == profile {
|
||||
validProfile = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !validProfile {
|
||||
return fmt.Errorf("invalid power profile: %s", preferences.PowerProfile)
|
||||
}
|
||||
|
||||
// Validate use case
|
||||
validUseCases := []types.UseCase{
|
||||
types.UseCaseGeneral,
|
||||
types.UseCaseDevelopment,
|
||||
types.UseCaseGaming,
|
||||
types.UseCaseServer,
|
||||
types.UseCaseMultimedia,
|
||||
types.UseCaseOffice,
|
||||
}
|
||||
|
||||
validUseCase := false
|
||||
for _, useCase := range validUseCases {
|
||||
if preferences.UseCase == useCase {
|
||||
validUseCase = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !validUseCase {
|
||||
return fmt.Errorf("invalid use case: %s", preferences.UseCase)
|
||||
}
|
||||
|
||||
// Check for conflicting preferences
|
||||
if preferences.PowerProfile == types.PowerProfilePowerSaving &&
|
||||
preferences.PerformanceMode == types.PerformanceModeMaximum {
|
||||
return fmt.Errorf("conflicting preferences: power saving profile with maximum performance mode")
|
||||
}
|
||||
|
||||
if preferences.UseCase == types.UseCaseGaming &&
|
||||
preferences.PowerProfile == types.PowerProfilePowerSaving {
|
||||
fmt.Println("⚠️ Warning: Gaming use case with power saving profile may impact performance")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetRecommendedSettings provides recommended settings based on preferences
|
||||
func GetRecommendedSettings(preferences *types.UserPreferences) map[string]string {
|
||||
settings := make(map[string]string)
|
||||
|
||||
// Base settings
|
||||
settings["TLP_ENABLE"] = "1"
|
||||
settings["TLP_WARN_LEVEL"] = "3"
|
||||
|
||||
// CPU settings based on power profile and use case
|
||||
switch preferences.PowerProfile {
|
||||
case types.PowerProfilePerformance:
|
||||
settings["CPU_SCALING_GOVERNOR_ON_AC"] = "performance"
|
||||
settings["CPU_SCALING_GOVERNOR_ON_BAT"] = "performance"
|
||||
case types.PowerProfilePowerSaving:
|
||||
settings["CPU_SCALING_GOVERNOR_ON_AC"] = "powersave"
|
||||
settings["CPU_SCALING_GOVERNOR_ON_BAT"] = "powersave"
|
||||
default: // Balanced
|
||||
settings["CPU_SCALING_GOVERNOR_ON_AC"] = "performance"
|
||||
settings["CPU_SCALING_GOVERNOR_ON_BAT"] = "powersave"
|
||||
}
|
||||
|
||||
// Adjust for use case
|
||||
switch preferences.UseCase {
|
||||
case types.UseCaseGaming:
|
||||
settings["CPU_SCALING_GOVERNOR_ON_AC"] = "performance"
|
||||
settings["PLATFORM_PROFILE_ON_AC"] = "performance"
|
||||
settings["PLATFORM_PROFILE_ON_BAT"] = "balanced"
|
||||
case types.UseCaseServer:
|
||||
settings["CPU_SCALING_GOVERNOR_ON_AC"] = "performance"
|
||||
settings["CPU_SCALING_GOVERNOR_ON_BAT"] = "performance"
|
||||
settings["PLATFORM_PROFILE_ON_AC"] = "performance"
|
||||
settings["PLATFORM_PROFILE_ON_BAT"] = "performance"
|
||||
case types.UseCaseDevelopment:
|
||||
settings["CPU_SCALING_GOVERNOR_ON_AC"] = "ondemand"
|
||||
settings["CPU_SCALING_GOVERNOR_ON_BAT"] = "powersave"
|
||||
}
|
||||
|
||||
// Disk settings based on battery priority
|
||||
switch preferences.BatteryPriority {
|
||||
case types.BatteryPriorityRuntime:
|
||||
settings["DISK_APM_LEVEL_ON_BAT"] = "1"
|
||||
settings["DISK_SPINDOWN_TIMEOUT_ON_BAT"] = "60"
|
||||
case types.BatteryPriorityLongevity:
|
||||
settings["DISK_APM_LEVEL_ON_BAT"] = "128"
|
||||
settings["DISK_SPINDOWN_TIMEOUT_ON_BAT"] = "0"
|
||||
default: // Balanced
|
||||
settings["DISK_APM_LEVEL_ON_BAT"] = "128"
|
||||
settings["DISK_SPINDOWN_TIMEOUT_ON_BAT"] = "120"
|
||||
}
|
||||
|
||||
settings["DISK_APM_LEVEL_ON_AC"] = "254"
|
||||
settings["DISK_SPINDOWN_TIMEOUT_ON_AC"] = "0"
|
||||
|
||||
// Network settings
|
||||
if utils.Contains(preferences.SpecialRequirements, "Maximum WiFi performance") {
|
||||
settings["WIFI_PWR_ON_AC"] = "off"
|
||||
settings["WIFI_PWR_ON_BAT"] = "off"
|
||||
} else {
|
||||
settings["WIFI_PWR_ON_AC"] = "off"
|
||||
settings["WIFI_PWR_ON_BAT"] = "on"
|
||||
}
|
||||
|
||||
// USB settings
|
||||
settings["USB_AUTOSUSPEND"] = "1"
|
||||
|
||||
// Apply custom settings
|
||||
for key, value := range preferences.CustomSettings {
|
||||
switch key {
|
||||
case "cpu_governor_ac":
|
||||
settings["CPU_SCALING_GOVERNOR_ON_AC"] = fmt.Sprintf("%v", value)
|
||||
case "cpu_governor_battery":
|
||||
settings["CPU_SCALING_GOVERNOR_ON_BAT"] = fmt.Sprintf("%v", value)
|
||||
case "disk_apm_ac":
|
||||
settings["DISK_APM_LEVEL_ON_AC"] = fmt.Sprintf("%v", value)
|
||||
case "disk_apm_battery":
|
||||
settings["DISK_APM_LEVEL_ON_BAT"] = fmt.Sprintf("%v", value)
|
||||
}
|
||||
}
|
||||
|
||||
return settings
|
||||
}
|
162
internal/config/preferences_test.go
Normal file
162
internal/config/preferences_test.go
Normal file
@@ -0,0 +1,162 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"git.gostacks.org/iwasforcedtobehere/WiseTLP/autotlp/pkg/types"
|
||||
)
|
||||
|
||||
func TestValidatePreferences(t *testing.T) {
|
||||
validPrefs := &types.UserPreferences{
|
||||
PowerProfile: types.PowerProfileBalanced,
|
||||
UseCase: types.UseCaseGeneral,
|
||||
BatteryPriority: types.BatteryPriorityBalanced,
|
||||
PerformanceMode: types.PerformanceModeAdaptive,
|
||||
CustomSettings: make(map[string]interface{}),
|
||||
SpecialRequirements: []string{},
|
||||
}
|
||||
|
||||
if err := ValidatePreferences(validPrefs); err != nil {
|
||||
t.Errorf("ValidatePreferences() with valid preferences failed: %v", err)
|
||||
}
|
||||
|
||||
if err := ValidatePreferences(nil); err == nil {
|
||||
t.Error("ValidatePreferences() with nil preferences should fail")
|
||||
}
|
||||
|
||||
invalidPrefs := &types.UserPreferences{
|
||||
PowerProfile: "invalid_profile",
|
||||
UseCase: types.UseCaseGeneral,
|
||||
BatteryPriority: types.BatteryPriorityBalanced,
|
||||
PerformanceMode: types.PerformanceModeAdaptive,
|
||||
CustomSettings: make(map[string]interface{}),
|
||||
SpecialRequirements: []string{},
|
||||
}
|
||||
|
||||
if err := ValidatePreferences(invalidPrefs); err == nil {
|
||||
t.Error("ValidatePreferences() with invalid power profile should fail")
|
||||
}
|
||||
|
||||
invalidPrefs2 := &types.UserPreferences{
|
||||
PowerProfile: types.PowerProfileBalanced,
|
||||
UseCase: "invalid_usecase",
|
||||
BatteryPriority: types.BatteryPriorityBalanced,
|
||||
PerformanceMode: types.PerformanceModeAdaptive,
|
||||
CustomSettings: make(map[string]interface{}),
|
||||
SpecialRequirements: []string{},
|
||||
}
|
||||
|
||||
if err := ValidatePreferences(invalidPrefs2); err == nil {
|
||||
t.Error("ValidatePreferences() with invalid use case should fail")
|
||||
}
|
||||
|
||||
// Test conflicting preferences
|
||||
conflictingPrefs := &types.UserPreferences{
|
||||
PowerProfile: types.PowerProfilePowerSaving,
|
||||
UseCase: types.UseCaseGeneral,
|
||||
BatteryPriority: types.BatteryPriorityBalanced,
|
||||
PerformanceMode: types.PerformanceModeMaximum,
|
||||
CustomSettings: make(map[string]interface{}),
|
||||
SpecialRequirements: []string{},
|
||||
}
|
||||
|
||||
if err := ValidatePreferences(conflictingPrefs); err == nil {
|
||||
t.Error("ValidatePreferences() with conflicting preferences should fail")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetRecommendedSettings(t *testing.T) {
|
||||
// Test performance profile
|
||||
perfPrefs := &types.UserPreferences{
|
||||
PowerProfile: types.PowerProfilePerformance,
|
||||
UseCase: types.UseCaseGaming,
|
||||
BatteryPriority: types.BatteryPriorityBalanced,
|
||||
PerformanceMode: types.PerformanceModeMaximum,
|
||||
CustomSettings: make(map[string]interface{}),
|
||||
SpecialRequirements: []string{},
|
||||
}
|
||||
|
||||
settings := GetRecommendedSettings(perfPrefs)
|
||||
|
||||
// Should have TLP_ENABLE
|
||||
if settings["TLP_ENABLE"] != "1" {
|
||||
t.Error("TLP_ENABLE should be set to '1'")
|
||||
}
|
||||
|
||||
// Performance profile should use performance governor
|
||||
if settings["CPU_SCALING_GOVERNOR_ON_AC"] != "performance" {
|
||||
t.Error("Performance profile should use performance governor on AC")
|
||||
}
|
||||
|
||||
// Test power saving profile
|
||||
powerSavePrefs := &types.UserPreferences{
|
||||
PowerProfile: types.PowerProfilePowerSaving,
|
||||
UseCase: types.UseCaseOffice,
|
||||
BatteryPriority: types.BatteryPriorityRuntime,
|
||||
PerformanceMode: types.PerformanceModeEfficient,
|
||||
CustomSettings: make(map[string]interface{}),
|
||||
SpecialRequirements: []string{},
|
||||
}
|
||||
|
||||
settings2 := GetRecommendedSettings(powerSavePrefs)
|
||||
|
||||
// Power saving should use powersave governor
|
||||
if settings2["CPU_SCALING_GOVERNOR_ON_AC"] != "powersave" {
|
||||
t.Error("Power saving profile should use powersave governor on AC")
|
||||
}
|
||||
|
||||
if settings2["CPU_SCALING_GOVERNOR_ON_BAT"] != "powersave" {
|
||||
t.Error("Power saving profile should use powersave governor on battery")
|
||||
}
|
||||
|
||||
// Test custom settings
|
||||
customPrefs := &types.UserPreferences{
|
||||
PowerProfile: types.PowerProfileCustom,
|
||||
UseCase: types.UseCaseGeneral,
|
||||
BatteryPriority: types.BatteryPriorityBalanced,
|
||||
PerformanceMode: types.PerformanceModeAdaptive,
|
||||
CustomSettings: map[string]interface{}{
|
||||
"cpu_governor_ac": "ondemand",
|
||||
"cpu_governor_battery": "conservative",
|
||||
"disk_apm_ac": "200",
|
||||
"disk_apm_battery": "100",
|
||||
},
|
||||
SpecialRequirements: []string{},
|
||||
}
|
||||
|
||||
settings3 := GetRecommendedSettings(customPrefs)
|
||||
|
||||
// Custom settings should override defaults
|
||||
if settings3["CPU_SCALING_GOVERNOR_ON_AC"] != "ondemand" {
|
||||
t.Error("Custom CPU governor AC setting should be applied")
|
||||
}
|
||||
|
||||
if settings3["CPU_SCALING_GOVERNOR_ON_BAT"] != "conservative" {
|
||||
t.Error("Custom CPU governor battery setting should be applied")
|
||||
}
|
||||
|
||||
if settings3["DISK_APM_LEVEL_ON_AC"] != "200" {
|
||||
t.Error("Custom disk APM AC setting should be applied")
|
||||
}
|
||||
|
||||
if settings3["DISK_APM_LEVEL_ON_BAT"] != "100" {
|
||||
t.Error("Custom disk APM battery setting should be applied")
|
||||
}
|
||||
|
||||
// Test special requirements
|
||||
wifiPrefs := &types.UserPreferences{
|
||||
PowerProfile: types.PowerProfileBalanced,
|
||||
UseCase: types.UseCaseGeneral,
|
||||
BatteryPriority: types.BatteryPriorityBalanced,
|
||||
PerformanceMode: types.PerformanceModeAdaptive,
|
||||
CustomSettings: make(map[string]interface{}),
|
||||
SpecialRequirements: []string{"Maximum WiFi performance"},
|
||||
}
|
||||
|
||||
settings4 := GetRecommendedSettings(wifiPrefs)
|
||||
|
||||
// WiFi performance requirement should disable power saving
|
||||
if settings4["WIFI_PWR_ON_AC"] != "off" || settings4["WIFI_PWR_ON_BAT"] != "off" {
|
||||
t.Error("Maximum WiFi performance should disable WiFi power saving")
|
||||
}
|
||||
}
|
267
internal/security/keystore.go
Normal file
267
internal/security/keystore.go
Normal file
@@ -0,0 +1,267 @@
|
||||
package security
|
||||
|
||||
import (
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"syscall"
|
||||
|
||||
"golang.org/x/term"
|
||||
)
|
||||
|
||||
// KeyStore handles secure storage and retrieval of API keys
|
||||
type KeyStore struct {
|
||||
configDir string
|
||||
}
|
||||
|
||||
// NewKeyStore creates a new KeyStore instance
|
||||
func NewKeyStore() (*KeyStore, error) {
|
||||
homeDir, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get user home directory: %w", err)
|
||||
}
|
||||
|
||||
configDir := filepath.Join(homeDir, ".config", "autotlp")
|
||||
if err := os.MkdirAll(configDir, 0700); err != nil {
|
||||
return nil, fmt.Errorf("failed to create config directory: %w", err)
|
||||
}
|
||||
|
||||
return &KeyStore{
|
||||
configDir: configDir,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// StoreAPIKey securely stores an API key
|
||||
func (ks *KeyStore) StoreAPIKey(provider, apiKey string) error {
|
||||
if apiKey == "" {
|
||||
return fmt.Errorf("API key cannot be empty")
|
||||
}
|
||||
|
||||
// Get or create master password
|
||||
masterPassword, err := ks.getMasterPassword()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get master password: %w", err)
|
||||
}
|
||||
|
||||
// Encrypt the API key
|
||||
encryptedKey, err := ks.encrypt(apiKey, masterPassword)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to encrypt API key: %w", err)
|
||||
}
|
||||
|
||||
// Store encrypted key
|
||||
keyFile := filepath.Join(ks.configDir, provider+".key")
|
||||
if err := os.WriteFile(keyFile, []byte(encryptedKey), 0600); err != nil {
|
||||
return fmt.Errorf("failed to write key file: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// RetrieveAPIKey retrieves and decrypts an API key
|
||||
func (ks *KeyStore) RetrieveAPIKey(provider string) (string, error) {
|
||||
keyFile := filepath.Join(ks.configDir, provider+".key")
|
||||
|
||||
// Check if key file exists
|
||||
if _, err := os.Stat(keyFile); os.IsNotExist(err) {
|
||||
return "", fmt.Errorf("API key not found for provider: %s", provider)
|
||||
}
|
||||
|
||||
// Read encrypted key
|
||||
encryptedKey, err := os.ReadFile(keyFile)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to read key file: %w", err)
|
||||
}
|
||||
|
||||
// Get master password
|
||||
masterPassword, err := ks.getMasterPassword()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to get master password: %w", err)
|
||||
}
|
||||
|
||||
// Decrypt the API key
|
||||
apiKey, err := ks.decrypt(string(encryptedKey), masterPassword)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to decrypt API key: %w", err)
|
||||
}
|
||||
|
||||
return apiKey, nil
|
||||
}
|
||||
|
||||
// DeleteAPIKey removes a stored API key
|
||||
func (ks *KeyStore) DeleteAPIKey(provider string) error {
|
||||
keyFile := filepath.Join(ks.configDir, provider+".key")
|
||||
|
||||
if err := os.Remove(keyFile); err != nil && !os.IsNotExist(err) {
|
||||
return fmt.Errorf("failed to delete key file: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ListProviders returns a list of providers with stored keys
|
||||
func (ks *KeyStore) ListProviders() ([]string, error) {
|
||||
files, err := os.ReadDir(ks.configDir)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read config directory: %w", err)
|
||||
}
|
||||
|
||||
var providers []string
|
||||
for _, file := range files {
|
||||
if !file.IsDir() && filepath.Ext(file.Name()) == ".key" {
|
||||
provider := file.Name()[:len(file.Name())-4] // Remove .key extension
|
||||
providers = append(providers, provider)
|
||||
}
|
||||
}
|
||||
|
||||
return providers, nil
|
||||
}
|
||||
|
||||
// getMasterPassword gets or creates a master password for encryption
|
||||
func (ks *KeyStore) getMasterPassword() (string, error) {
|
||||
passwordFile := filepath.Join(ks.configDir, ".master")
|
||||
|
||||
// Check if master password file exists
|
||||
if _, err := os.Stat(passwordFile); os.IsNotExist(err) {
|
||||
// Create new master password
|
||||
fmt.Print("Create a master password to secure your API keys: ")
|
||||
password, err := ks.readPassword()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to read password: %w", err)
|
||||
}
|
||||
|
||||
fmt.Print("Confirm master password: ")
|
||||
confirmPassword, err := ks.readPassword()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to read confirmation password: %w", err)
|
||||
}
|
||||
|
||||
if password != confirmPassword {
|
||||
return "", fmt.Errorf("passwords do not match")
|
||||
}
|
||||
|
||||
// Hash and store the password
|
||||
hashedPassword := ks.hashPassword(password)
|
||||
if err := os.WriteFile(passwordFile, []byte(hashedPassword), 0600); err != nil {
|
||||
return "", fmt.Errorf("failed to store master password: %w", err)
|
||||
}
|
||||
|
||||
return password, nil
|
||||
}
|
||||
|
||||
// Master password exists, prompt for it
|
||||
fmt.Print("Enter master password: ")
|
||||
password, err := ks.readPassword()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to read password: %w", err)
|
||||
}
|
||||
|
||||
// Verify password
|
||||
storedHash, err := os.ReadFile(passwordFile)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to read stored password hash: %w", err)
|
||||
}
|
||||
|
||||
if ks.hashPassword(password) != string(storedHash) {
|
||||
return "", fmt.Errorf("incorrect master password")
|
||||
}
|
||||
|
||||
return password, nil
|
||||
}
|
||||
|
||||
// readPassword reads a password from stdin without echoing
|
||||
func (ks *KeyStore) readPassword() (string, error) {
|
||||
password, err := term.ReadPassword(int(syscall.Stdin))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
fmt.Println() // Add newline after password input
|
||||
return string(password), nil
|
||||
}
|
||||
|
||||
// hashPassword creates a hash of the password for verification
|
||||
func (ks *KeyStore) hashPassword(password string) string {
|
||||
hash := sha256.Sum256([]byte(password))
|
||||
return base64.StdEncoding.EncodeToString(hash[:])
|
||||
}
|
||||
|
||||
// encrypt encrypts data using AES-GCM
|
||||
func (ks *KeyStore) encrypt(plaintext, password string) (string, error) {
|
||||
// Create cipher
|
||||
key := sha256.Sum256([]byte(password))
|
||||
block, err := aes.NewCipher(key[:])
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// Create GCM
|
||||
gcm, err := cipher.NewGCM(block)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// Generate nonce
|
||||
nonce := make([]byte, gcm.NonceSize())
|
||||
if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// Encrypt
|
||||
ciphertext := gcm.Seal(nonce, nonce, []byte(plaintext), nil)
|
||||
return base64.StdEncoding.EncodeToString(ciphertext), nil
|
||||
}
|
||||
|
||||
// decrypt decrypts data using AES-GCM
|
||||
func (ks *KeyStore) decrypt(ciphertext, password string) (string, error) {
|
||||
// Decode base64
|
||||
data, err := base64.StdEncoding.DecodeString(ciphertext)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// Create cipher
|
||||
key := sha256.Sum256([]byte(password))
|
||||
block, err := aes.NewCipher(key[:])
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// Create GCM
|
||||
gcm, err := cipher.NewGCM(block)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// Extract nonce
|
||||
nonceSize := gcm.NonceSize()
|
||||
if len(data) < nonceSize {
|
||||
return "", fmt.Errorf("ciphertext too short")
|
||||
}
|
||||
|
||||
nonce, ciphertext_bytes := data[:nonceSize], data[nonceSize:]
|
||||
|
||||
// Decrypt
|
||||
plaintext, err := gcm.Open(nil, nonce, ciphertext_bytes, nil)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return string(plaintext), nil
|
||||
}
|
||||
|
||||
// SecureInput prompts for secure input without echoing
|
||||
func SecureInput(prompt string) (string, error) {
|
||||
fmt.Print(prompt)
|
||||
input, err := term.ReadPassword(int(syscall.Stdin))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
fmt.Println() // Add newline
|
||||
return string(input), nil
|
||||
}
|
249
internal/security/privileges.go
Normal file
249
internal/security/privileges.go
Normal file
@@ -0,0 +1,249 @@
|
||||
package security
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"syscall"
|
||||
|
||||
"git.gostacks.org/iwasforcedtobehere/WiseTLP/autotlp/pkg/utils"
|
||||
)
|
||||
|
||||
// PrivilegeManager handles privilege escalation and security operations
|
||||
type PrivilegeManager struct {
|
||||
logger *utils.Logger
|
||||
}
|
||||
|
||||
// NewPrivilegeManager creates a new privilege manager
|
||||
func NewPrivilegeManager(logger *utils.Logger) *PrivilegeManager {
|
||||
return &PrivilegeManager{
|
||||
logger: logger.WithComponent("security"),
|
||||
}
|
||||
}
|
||||
|
||||
// RequireRoot ensures the current operation has root privileges
|
||||
func (pm *PrivilegeManager) RequireRoot(operation string) error {
|
||||
if os.Geteuid() == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
pm.logger.Info("Root privileges required", "operation", operation)
|
||||
return fmt.Errorf("operation '%s' requires root privileges", operation)
|
||||
}
|
||||
|
||||
// EscalateIfNeeded escalates privileges if not already running as root
|
||||
func (pm *PrivilegeManager) EscalateIfNeeded(args []string, operation string) error {
|
||||
if os.Geteuid() == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
pm.logger.Info("Escalating privileges", "operation", operation)
|
||||
|
||||
// Check if sudo is available
|
||||
if !utils.CommandExists("sudo") {
|
||||
return fmt.Errorf("sudo is required for privilege escalation but not available")
|
||||
}
|
||||
|
||||
// Inform user about privilege escalation
|
||||
fmt.Printf("\nPrivilege escalation required for: %s\n", operation)
|
||||
fmt.Println("This operation needs administrative privileges to:")
|
||||
|
||||
switch operation {
|
||||
case "install_tlp":
|
||||
fmt.Println("- Install TLP packages using system package manager")
|
||||
fmt.Println("- Enable and start TLP systemd service")
|
||||
fmt.Println("- Mask conflicting power management services")
|
||||
case "apply_config":
|
||||
fmt.Println("- Write TLP configuration to system directories")
|
||||
fmt.Println("- Reload TLP service with new configuration")
|
||||
case "system_info":
|
||||
fmt.Println("- Access hardware information from system files")
|
||||
fmt.Println("- Read power management settings")
|
||||
default:
|
||||
fmt.Printf("- Perform system operation: %s\n", operation)
|
||||
}
|
||||
|
||||
if !utils.GetUserConfirmation("Continue with privilege escalation?") {
|
||||
return fmt.Errorf("privilege escalation declined by user")
|
||||
}
|
||||
|
||||
// Execute with sudo
|
||||
return pm.executeSudo(args)
|
||||
}
|
||||
|
||||
// executeSudo executes the current program with sudo
|
||||
func (pm *PrivilegeManager) executeSudo(args []string) error {
|
||||
// Get current executable path
|
||||
executable, err := os.Executable()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get executable path: %w", err)
|
||||
}
|
||||
|
||||
// Prepare sudo command
|
||||
sudoArgs := append([]string{executable}, args...)
|
||||
cmd := exec.Command("sudo", sudoArgs...)
|
||||
|
||||
// Preserve environment variables that might be needed
|
||||
cmd.Env = os.Environ()
|
||||
|
||||
// Connect stdio
|
||||
cmd.Stdin = os.Stdin
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
|
||||
pm.logger.Info("Executing with sudo", "command", cmd.String())
|
||||
|
||||
// Execute the command
|
||||
err = cmd.Run()
|
||||
if err != nil {
|
||||
if exitError, ok := err.(*exec.ExitError); ok {
|
||||
// Get exit code from the elevated process
|
||||
if status, ok := exitError.Sys().(syscall.WaitStatus); ok {
|
||||
os.Exit(status.ExitStatus())
|
||||
}
|
||||
}
|
||||
return fmt.Errorf("sudo execution failed: %w", err)
|
||||
}
|
||||
|
||||
// If we reach here, the elevated process completed successfully
|
||||
os.Exit(0)
|
||||
return nil // This line will never be reached
|
||||
}
|
||||
|
||||
// CheckSudoAccess verifies that the user can use sudo
|
||||
func (pm *PrivilegeManager) CheckSudoAccess() error {
|
||||
if os.Geteuid() == 0 {
|
||||
return nil // Already root
|
||||
}
|
||||
|
||||
if !utils.CommandExists("sudo") {
|
||||
return fmt.Errorf("sudo command not available")
|
||||
}
|
||||
|
||||
// Test sudo access with a harmless command
|
||||
cmd := exec.Command("sudo", "-n", "true")
|
||||
if err := cmd.Run(); err != nil {
|
||||
// -n flag failed, user needs to authenticate
|
||||
pm.logger.Debug("Sudo authentication required")
|
||||
return nil // This is expected for most users
|
||||
}
|
||||
|
||||
pm.logger.Debug("Sudo access confirmed without authentication")
|
||||
return nil
|
||||
}
|
||||
|
||||
// DropPrivileges drops root privileges if running as root
|
||||
func (pm *PrivilegeManager) DropPrivileges() error {
|
||||
if os.Geteuid() != 0 {
|
||||
return nil // Not running as root
|
||||
}
|
||||
|
||||
// Get the original user info from environment
|
||||
sudoUID := os.Getenv("SUDO_UID")
|
||||
sudoGID := os.Getenv("SUDO_GID")
|
||||
|
||||
if sudoUID == "" || sudoGID == "" {
|
||||
pm.logger.Warn("Cannot drop privileges: SUDO_UID/SUDO_GID not set")
|
||||
return nil // Don't fail, just log warning
|
||||
}
|
||||
|
||||
pm.logger.Info("Dropping root privileges", "uid", sudoUID, "gid", sudoGID)
|
||||
|
||||
// This is a placeholder - actual privilege dropping requires careful handling
|
||||
// and is typically done at the start of the program, not mid-execution
|
||||
pm.logger.Debug("Privilege dropping not implemented for mid-execution")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ValidateSystemAccess validates that the program has necessary system access
|
||||
func (pm *PrivilegeManager) ValidateSystemAccess() error {
|
||||
// Check read access to system information
|
||||
systemPaths := []string{
|
||||
"/proc/cpuinfo",
|
||||
"/proc/meminfo",
|
||||
"/sys/class/power_supply",
|
||||
"/sys/devices/system/cpu",
|
||||
}
|
||||
|
||||
for _, path := range systemPaths {
|
||||
if utils.FileExists(path) {
|
||||
// Try to read the file/directory
|
||||
if file, err := os.Open(path); err != nil {
|
||||
pm.logger.Warn("Limited access to system path", "path", path, "error", err)
|
||||
} else {
|
||||
file.Close()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// SecureFilePermissions sets secure permissions on configuration files
|
||||
func (pm *PrivilegeManager) SecureFilePermissions(filePath string) error {
|
||||
// Set file permissions to be readable only by owner and group
|
||||
if err := os.Chmod(filePath, 0640); err != nil {
|
||||
return fmt.Errorf("failed to set secure permissions on %s: %w", filePath, err)
|
||||
}
|
||||
|
||||
pm.logger.Debug("Set secure file permissions", "file", filePath, "mode", "0640")
|
||||
return nil
|
||||
}
|
||||
|
||||
// ValidateInput performs basic input validation for security
|
||||
func ValidateInput(input string, maxLength int, allowedChars string) error {
|
||||
if len(input) > maxLength {
|
||||
return fmt.Errorf("input too long: %d characters (max %d)", len(input), maxLength)
|
||||
}
|
||||
|
||||
if len(input) == 0 {
|
||||
return fmt.Errorf("input cannot be empty")
|
||||
}
|
||||
|
||||
// Basic validation against null bytes and control characters
|
||||
for i, r := range input {
|
||||
if r == 0 {
|
||||
return fmt.Errorf("null byte at position %d", i)
|
||||
}
|
||||
if r < 32 && r != 9 && r != 10 && r != 13 { // Allow tab, LF, CR
|
||||
return fmt.Errorf("control character at position %d", i)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// SanitizeFilePath sanitizes file paths to prevent directory traversal
|
||||
func SanitizeFilePath(path string) (string, error) {
|
||||
// Basic path validation
|
||||
if path == "" {
|
||||
return "", fmt.Errorf("empty path")
|
||||
}
|
||||
|
||||
// Check for directory traversal attempts
|
||||
if len(path) > 1 && (path[0] == '/' || path[1] == ':') {
|
||||
// Absolute path - this might be intentional, so we allow it
|
||||
// but log it for security awareness
|
||||
}
|
||||
|
||||
// Check for dangerous patterns
|
||||
dangerousPatterns := []string{
|
||||
"../",
|
||||
"..\\",
|
||||
"./",
|
||||
".\\",
|
||||
}
|
||||
|
||||
for _, pattern := range dangerousPatterns {
|
||||
if len(path) >= len(pattern) {
|
||||
for i := 0; i <= len(path)-len(pattern); i++ {
|
||||
if path[i:i+len(pattern)] == pattern {
|
||||
return "", fmt.Errorf("potentially dangerous path pattern: %s", pattern)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return path, nil
|
||||
}
|
487
internal/system/detector.go
Normal file
487
internal/system/detector.go
Normal file
@@ -0,0 +1,487 @@
|
||||
package system
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"git.gostacks.org/iwasforcedtobehere/WiseTLP/autotlp/pkg/types"
|
||||
"git.gostacks.org/iwasforcedtobehere/WiseTLP/autotlp/pkg/utils"
|
||||
)
|
||||
|
||||
type Info struct {
|
||||
Distribution string
|
||||
Version string
|
||||
Architecture string
|
||||
PackageManager string
|
||||
}
|
||||
|
||||
func DetectSystem(ctx context.Context) (*Info, error) {
|
||||
if runtime.GOOS != "linux" {
|
||||
return nil, fmt.Errorf("unsupported operating system: %s", runtime.GOOS)
|
||||
}
|
||||
|
||||
info := &Info{
|
||||
Architecture: runtime.GOARCH,
|
||||
}
|
||||
|
||||
if err := detectFromOSRelease(info); err != nil {
|
||||
if err := detectFromLSBRelease(info); err != nil {
|
||||
return nil, fmt.Errorf("failed to detect Linux distribution: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
info.PackageManager = detectPackageManager(info.Distribution)
|
||||
|
||||
return info, nil
|
||||
}
|
||||
|
||||
func detectFromOSRelease(info *Info) error {
|
||||
const osReleasePath = "/etc/os-release"
|
||||
|
||||
if !utils.FileExists(osReleasePath) {
|
||||
return fmt.Errorf("os-release file not found")
|
||||
}
|
||||
|
||||
lines, err := utils.ReadFileLines(osReleasePath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read os-release: %w", err)
|
||||
}
|
||||
|
||||
osInfo := make(map[string]string)
|
||||
for _, line := range lines {
|
||||
if key, value, ok := utils.ParseKeyValue(line); ok {
|
||||
osInfo[key] = value
|
||||
}
|
||||
}
|
||||
|
||||
if id, exists := osInfo["ID"]; exists {
|
||||
info.Distribution = id
|
||||
}
|
||||
|
||||
if version, exists := osInfo["VERSION_ID"]; exists {
|
||||
info.Version = version
|
||||
} else if version, exists := osInfo["VERSION"]; exists {
|
||||
info.Version = version
|
||||
}
|
||||
|
||||
if info.Distribution == "" {
|
||||
return fmt.Errorf("could not determine distribution from os-release")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func detectFromLSBRelease(info *Info) error {
|
||||
if !utils.CommandExists("lsb_release") {
|
||||
return fmt.Errorf("lsb_release command not available")
|
||||
}
|
||||
|
||||
distro, err := utils.RunCommand("lsb_release", "-si")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get distribution ID: %w", err)
|
||||
}
|
||||
info.Distribution = strings.ToLower(distro)
|
||||
|
||||
version, err := utils.RunCommand("lsb_release", "-sr")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get distribution version: %w", err)
|
||||
}
|
||||
info.Version = version
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func detectPackageManager(distro string) string {
|
||||
switch strings.ToLower(distro) {
|
||||
case "ubuntu", "debian", "linuxmint", "elementary", "pop":
|
||||
return "apt"
|
||||
case "fedora", "rhel", "centos", "rocky", "almalinux":
|
||||
return "dnf"
|
||||
case "opensuse", "suse", "opensuse-leap", "opensuse-tumbleweed":
|
||||
return "zypper"
|
||||
case "arch", "manjaro", "endeavouros", "garuda":
|
||||
return "pacman"
|
||||
case "gentoo":
|
||||
return "portage"
|
||||
case "alpine":
|
||||
return "apk"
|
||||
default:
|
||||
if utils.CommandExists("apt") {
|
||||
return "apt"
|
||||
} else if utils.CommandExists("dnf") {
|
||||
return "dnf"
|
||||
} else if utils.CommandExists("yum") {
|
||||
return "yum"
|
||||
} else if utils.CommandExists("zypper") {
|
||||
return "zypper"
|
||||
} else if utils.CommandExists("pacman") {
|
||||
return "pacman"
|
||||
} else if utils.CommandExists("apk") {
|
||||
return "apk"
|
||||
}
|
||||
return "unknown"
|
||||
}
|
||||
}
|
||||
|
||||
func GatherSystemInfo(ctx context.Context) (*types.SystemInfo, error) {
|
||||
sysInfo := &types.SystemInfo{}
|
||||
|
||||
basicInfo, err := DetectSystem(ctx)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to detect system: %w", err)
|
||||
}
|
||||
|
||||
sysInfo.Distribution = types.DistributionInfo{
|
||||
ID: basicInfo.Distribution,
|
||||
Version: basicInfo.Version,
|
||||
PackageManager: basicInfo.PackageManager,
|
||||
}
|
||||
|
||||
if cpuInfo, err := gatherCPUInfo(); err == nil {
|
||||
sysInfo.CPU = *cpuInfo
|
||||
}
|
||||
|
||||
if memInfo, err := gatherMemoryInfo(); err == nil {
|
||||
sysInfo.Memory = *memInfo
|
||||
}
|
||||
|
||||
if batteryInfo, err := gatherBatteryInfo(); err == nil {
|
||||
sysInfo.Battery = batteryInfo
|
||||
}
|
||||
|
||||
if powerInfo, err := gatherPowerSupplyInfo(); err == nil {
|
||||
sysInfo.PowerSupply = *powerInfo
|
||||
}
|
||||
|
||||
if kernelInfo, err := gatherKernelInfo(); err == nil {
|
||||
sysInfo.Kernel = *kernelInfo
|
||||
}
|
||||
|
||||
if hwInfo, err := gatherHardwareInfo(); err == nil {
|
||||
sysInfo.Hardware = *hwInfo
|
||||
}
|
||||
|
||||
return sysInfo, nil
|
||||
}
|
||||
|
||||
func gatherCPUInfo() (*types.CPUInfo, error) {
|
||||
cpuInfo := &types.CPUInfo{}
|
||||
|
||||
if utils.FileExists("/proc/cpuinfo") {
|
||||
lines, err := utils.ReadFileLines("/proc/cpuinfo")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, line := range lines {
|
||||
if key, value, ok := utils.ParseKeyValue(line); ok {
|
||||
switch strings.ToLower(key) {
|
||||
case "model name":
|
||||
if cpuInfo.Model == "" {
|
||||
cpuInfo.Model = value
|
||||
}
|
||||
case "vendor_id":
|
||||
if cpuInfo.Vendor == "" {
|
||||
cpuInfo.Vendor = value
|
||||
}
|
||||
case "processor":
|
||||
cpuInfo.Cores++
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if utils.FileExists("/sys/devices/system/cpu/cpu0/cpufreq/scaling_governor") {
|
||||
governor, err := utils.ReadFirstLine("/sys/devices/system/cpu/cpu0/cpufreq/scaling_governor")
|
||||
if err == nil {
|
||||
cpuInfo.Governor = governor
|
||||
}
|
||||
}
|
||||
|
||||
if utils.FileExists("/sys/devices/system/cpu/cpu0/cpufreq/cpuinfo_max_freq") {
|
||||
maxFreq, err := utils.ReadFirstLine("/sys/devices/system/cpu/cpu0/cpufreq/cpuinfo_max_freq")
|
||||
if err == nil {
|
||||
cpuInfo.MaxFrequency = utils.ParseInt64(maxFreq) / 1000
|
||||
}
|
||||
}
|
||||
|
||||
if utils.FileExists("/sys/devices/system/cpu/cpu0/cpufreq/cpuinfo_min_freq") {
|
||||
minFreq, err := utils.ReadFirstLine("/sys/devices/system/cpu/cpu0/cpufreq/cpuinfo_min_freq")
|
||||
if err == nil {
|
||||
cpuInfo.MinFrequency = utils.ParseInt64(minFreq) / 1000
|
||||
}
|
||||
}
|
||||
|
||||
cpuInfo.Architecture = runtime.GOARCH
|
||||
|
||||
return cpuInfo, nil
|
||||
}
|
||||
|
||||
func gatherMemoryInfo() (*types.MemoryInfo, error) {
|
||||
memInfo := &types.MemoryInfo{}
|
||||
|
||||
if !utils.FileExists("/proc/meminfo") {
|
||||
return nil, fmt.Errorf("/proc/meminfo not found")
|
||||
}
|
||||
|
||||
lines, err := utils.ReadFileLines("/proc/meminfo")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, line := range lines {
|
||||
parts := strings.Fields(line)
|
||||
if len(parts) < 2 {
|
||||
continue
|
||||
}
|
||||
|
||||
key := strings.TrimSuffix(parts[0], ":")
|
||||
value := utils.ParseInt64(parts[1])
|
||||
|
||||
switch key {
|
||||
case "MemTotal":
|
||||
memInfo.Total = value / 1024
|
||||
case "MemAvailable":
|
||||
memInfo.Available = value / 1024
|
||||
case "SwapTotal":
|
||||
memInfo.SwapTotal = value / 1024
|
||||
case "SwapFree":
|
||||
swapFree := value / 1024
|
||||
memInfo.SwapUsed = memInfo.SwapTotal - swapFree
|
||||
}
|
||||
}
|
||||
|
||||
memInfo.Used = memInfo.Total - memInfo.Available
|
||||
|
||||
return memInfo, nil
|
||||
}
|
||||
|
||||
func gatherBatteryInfo() (*types.BatteryInfo, error) {
|
||||
batteryPath := "/sys/class/power_supply/BAT0"
|
||||
if !utils.FileExists(batteryPath) {
|
||||
batteryPath = "/sys/class/power_supply/BAT1"
|
||||
if !utils.FileExists(batteryPath) {
|
||||
return nil, fmt.Errorf("no battery found")
|
||||
}
|
||||
}
|
||||
|
||||
batteryInfo := &types.BatteryInfo{Present: true}
|
||||
|
||||
properties := map[string]*string{
|
||||
"status": &batteryInfo.Status,
|
||||
"manufacturer": &batteryInfo.Manufacturer,
|
||||
"model_name": &batteryInfo.Model,
|
||||
"technology": &batteryInfo.Technology,
|
||||
}
|
||||
|
||||
for prop, field := range properties {
|
||||
if value, err := utils.ReadFirstLine(batteryPath + "/" + prop); err == nil {
|
||||
*field = value
|
||||
}
|
||||
}
|
||||
|
||||
if value, err := utils.ReadFirstLine(batteryPath + "/capacity"); err == nil {
|
||||
batteryInfo.Capacity = utils.ParseInt(value)
|
||||
}
|
||||
|
||||
if value, err := utils.ReadFirstLine(batteryPath + "/energy_full"); err == nil {
|
||||
batteryInfo.EnergyFull = utils.ParseInt64(value) / 1000000
|
||||
}
|
||||
|
||||
if value, err := utils.ReadFirstLine(batteryPath + "/energy_now"); err == nil {
|
||||
batteryInfo.EnergyNow = utils.ParseInt64(value) / 1000000
|
||||
}
|
||||
|
||||
if value, err := utils.ReadFirstLine(batteryPath + "/power_now"); err == nil {
|
||||
batteryInfo.PowerNow = utils.ParseInt64(value) / 1000000
|
||||
}
|
||||
|
||||
if value, err := utils.ReadFirstLine(batteryPath + "/cycle_count"); err == nil {
|
||||
batteryInfo.CycleCount = utils.ParseInt(value)
|
||||
}
|
||||
|
||||
if value, err := utils.ReadFirstLine(batteryPath + "/energy_full_design"); err == nil {
|
||||
batteryInfo.DesignCapacity = utils.ParseInt64(value) / 1000000
|
||||
}
|
||||
|
||||
return batteryInfo, nil
|
||||
}
|
||||
|
||||
func gatherPowerSupplyInfo() (*types.PowerSupplyInfo, error) {
|
||||
powerInfo := &types.PowerSupplyInfo{}
|
||||
|
||||
acPaths := []string{
|
||||
"/sys/class/power_supply/ADP0",
|
||||
"/sys/class/power_supply/ADP1",
|
||||
"/sys/class/power_supply/AC",
|
||||
"/sys/class/power_supply/ACAD",
|
||||
}
|
||||
|
||||
for _, path := range acPaths {
|
||||
if utils.FileExists(path + "/online") {
|
||||
if online, err := utils.ReadFirstLine(path + "/online"); err == nil {
|
||||
powerInfo.ACConnected = online == "1"
|
||||
powerInfo.Online = powerInfo.ACConnected
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
powerInfo.Type = "AC"
|
||||
|
||||
return powerInfo, nil
|
||||
}
|
||||
|
||||
func gatherKernelInfo() (*types.KernelInfo, error) {
|
||||
kernelInfo := &types.KernelInfo{
|
||||
Parameters: make(map[string]string),
|
||||
}
|
||||
|
||||
if version, err := utils.ReadFirstLine("/proc/version"); err == nil {
|
||||
parts := strings.Fields(version)
|
||||
if len(parts) >= 3 {
|
||||
kernelInfo.Version = parts[2]
|
||||
}
|
||||
}
|
||||
|
||||
if release, err := utils.ReadFirstLine("/proc/sys/kernel/osrelease"); err == nil {
|
||||
kernelInfo.Release = release
|
||||
}
|
||||
|
||||
if cmdline, err := utils.ReadFirstLine("/proc/cmdline"); err == nil {
|
||||
params := strings.Fields(cmdline)
|
||||
for _, param := range params {
|
||||
if key, value, ok := utils.ParseKeyValue(param); ok {
|
||||
kernelInfo.Parameters[key] = value
|
||||
} else {
|
||||
kernelInfo.Parameters[param] = ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return kernelInfo, nil
|
||||
}
|
||||
|
||||
func gatherHardwareInfo() (*types.HardwareInfo, error) {
|
||||
hwInfo := &types.HardwareInfo{}
|
||||
|
||||
if utils.FileExists("/sys/class/dmi/id/chassis_type") {
|
||||
if chassis, err := utils.ReadFirstLine("/sys/class/dmi/id/chassis_type"); err == nil {
|
||||
hwInfo.Chassis = getChassisType(utils.ParseInt(chassis))
|
||||
}
|
||||
}
|
||||
|
||||
if utils.FileExists("/sys/class/dmi/id/sys_vendor") {
|
||||
if vendor, err := utils.ReadFirstLine("/sys/class/dmi/id/sys_vendor"); err == nil {
|
||||
hwInfo.Manufacturer = vendor
|
||||
}
|
||||
}
|
||||
|
||||
if utils.FileExists("/sys/class/dmi/id/product_name") {
|
||||
if product, err := utils.ReadFirstLine("/sys/class/dmi/id/product_name"); err == nil {
|
||||
hwInfo.ProductName = product
|
||||
}
|
||||
}
|
||||
|
||||
hwInfo.StorageDevices = gatherStorageInfo()
|
||||
|
||||
return hwInfo, nil
|
||||
}
|
||||
|
||||
func getChassisType(chassisType int) string {
|
||||
types := map[int]string{
|
||||
1: "Other",
|
||||
2: "Unknown",
|
||||
3: "Desktop",
|
||||
4: "Low Profile Desktop",
|
||||
5: "Pizza Box",
|
||||
6: "Mini Tower",
|
||||
7: "Tower",
|
||||
8: "Portable",
|
||||
9: "Laptop",
|
||||
10: "Notebook",
|
||||
11: "Hand Held",
|
||||
12: "Docking Station",
|
||||
13: "All In One",
|
||||
14: "Sub Notebook",
|
||||
15: "Space-saving",
|
||||
16: "Lunch Box",
|
||||
17: "Main Server Chassis",
|
||||
18: "Expansion Chassis",
|
||||
19: "Sub Chassis",
|
||||
20: "Bus Expansion Chassis",
|
||||
21: "Peripheral Chassis",
|
||||
22: "RAID Chassis",
|
||||
23: "Rack Mount Chassis",
|
||||
24: "Sealed-case PC",
|
||||
25: "Multi-system",
|
||||
26: "CompactPCI",
|
||||
27: "AdvancedTCA",
|
||||
28: "Blade",
|
||||
29: "Blade Enclosing",
|
||||
}
|
||||
|
||||
if desc, exists := types[chassisType]; exists {
|
||||
return desc
|
||||
}
|
||||
return "Unknown"
|
||||
}
|
||||
|
||||
func gatherStorageInfo() []types.StorageInfo {
|
||||
var devices []types.StorageInfo
|
||||
|
||||
if !utils.FileExists("/proc/partitions") {
|
||||
return devices
|
||||
}
|
||||
|
||||
lines, err := utils.ReadFileLines("/proc/partitions")
|
||||
if err != nil {
|
||||
return devices
|
||||
}
|
||||
|
||||
for _, line := range lines {
|
||||
fields := strings.Fields(line)
|
||||
if len(fields) < 4 {
|
||||
continue
|
||||
}
|
||||
|
||||
deviceName := fields[3]
|
||||
if strings.HasPrefix(line, "major") ||
|
||||
strings.Contains(deviceName, "loop") ||
|
||||
len(deviceName) > 3 && (deviceName[len(deviceName)-1] >= '0' && deviceName[len(deviceName)-1] <= '9') {
|
||||
continue
|
||||
}
|
||||
|
||||
device := types.StorageInfo{
|
||||
Device: "/dev/" + deviceName,
|
||||
Size: utils.ParseInt64(fields[2]) / 1024 / 1024,
|
||||
}
|
||||
|
||||
rotationalPath := fmt.Sprintf("/sys/block/%s/queue/rotational", deviceName)
|
||||
if utils.FileExists(rotationalPath) {
|
||||
if rotational, err := utils.ReadFirstLine(rotationalPath); err == nil {
|
||||
device.Rotational = rotational == "1"
|
||||
if device.Rotational {
|
||||
device.Type = "HDD"
|
||||
} else if strings.HasPrefix(deviceName, "nvme") {
|
||||
device.Type = "NVMe"
|
||||
} else {
|
||||
device.Type = "SSD"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
modelPath := fmt.Sprintf("/sys/block/%s/device/model", deviceName)
|
||||
if utils.FileExists(modelPath) {
|
||||
if model, err := utils.ReadFirstLine(modelPath); err == nil {
|
||||
device.Model = strings.TrimSpace(model)
|
||||
}
|
||||
}
|
||||
|
||||
devices = append(devices, device)
|
||||
}
|
||||
|
||||
return devices
|
||||
}
|
121
internal/system/detector_test.go
Normal file
121
internal/system/detector_test.go
Normal file
@@ -0,0 +1,121 @@
|
||||
package system
|
||||
|
||||
import (
|
||||
"context"
|
||||
"runtime"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestDetectPackageManager(t *testing.T) {
|
||||
tests := []struct {
|
||||
distro string
|
||||
expected string
|
||||
}{
|
||||
{"ubuntu", "apt"},
|
||||
{"debian", "apt"},
|
||||
{"fedora", "dnf"},
|
||||
{"centos", "dnf"},
|
||||
{"arch", "pacman"},
|
||||
{"manjaro", "pacman"},
|
||||
{"opensuse", "zypper"},
|
||||
{"alpine", "apk"},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
result := detectPackageManager(test.distro)
|
||||
if result != test.expected {
|
||||
t.Errorf("detectPackageManager(%q) = %q, want %q", test.distro, result, test.expected)
|
||||
}
|
||||
}
|
||||
|
||||
unknownResult := detectPackageManager("totally_unknown_distro")
|
||||
if unknownResult == "" {
|
||||
t.Error("detectPackageManager with unknown distro should return something")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDetectSystem(t *testing.T) {
|
||||
if runtime.GOOS != "linux" {
|
||||
t.Skip("Skipping Linux-specific test on non-Linux system")
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
info, err := DetectSystem(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("DetectSystem() failed: %v", err)
|
||||
}
|
||||
|
||||
if info.Distribution == "" {
|
||||
t.Error("Distribution should not be empty")
|
||||
}
|
||||
|
||||
if info.Architecture == "" {
|
||||
t.Error("Architecture should not be empty")
|
||||
}
|
||||
|
||||
if info.PackageManager == "" {
|
||||
t.Error("PackageManager should not be empty")
|
||||
}
|
||||
|
||||
// Architecture should match runtime.GOARCH
|
||||
if info.Architecture != runtime.GOARCH {
|
||||
t.Errorf("Architecture = %q, want %q", info.Architecture, runtime.GOARCH)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetChassisType(t *testing.T) {
|
||||
tests := []struct {
|
||||
input int
|
||||
expected string
|
||||
}{
|
||||
{1, "Other"},
|
||||
{3, "Desktop"},
|
||||
{9, "Laptop"},
|
||||
{10, "Notebook"},
|
||||
{999, "Unknown"}, // Invalid type
|
||||
{0, "Unknown"}, // Invalid type
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
result := getChassisType(test.input)
|
||||
if result != test.expected {
|
||||
t.Errorf("getChassisType(%d) = %q, want %q", test.input, result, test.expected)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestGatherSystemInfo(t *testing.T) {
|
||||
if runtime.GOOS != "linux" {
|
||||
t.Skip("Skipping Linux-specific test on non-Linux system")
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
sysInfo, err := GatherSystemInfo(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("GatherSystemInfo() failed: %v", err)
|
||||
}
|
||||
|
||||
// Basic validation
|
||||
if sysInfo.Distribution.ID == "" {
|
||||
t.Error("Distribution ID should not be empty")
|
||||
}
|
||||
|
||||
if sysInfo.CPU.Architecture == "" {
|
||||
t.Error("CPU Architecture should not be empty")
|
||||
}
|
||||
|
||||
if sysInfo.Memory.Total <= 0 {
|
||||
t.Error("Memory Total should be greater than 0")
|
||||
}
|
||||
|
||||
// CPU should have at least 1 core (but may be 0 if /proc/cpuinfo is not accessible in test environment)
|
||||
if sysInfo.CPU.Cores < 0 {
|
||||
t.Error("CPU Cores should not be negative")
|
||||
}
|
||||
|
||||
// If we can read CPU info, we should have at least 1 core
|
||||
// In test environments, this might not be available, so we just log it
|
||||
if sysInfo.CPU.Cores == 0 {
|
||||
t.Logf("Warning: CPU cores is 0, possibly due to test environment limitations")
|
||||
}
|
||||
}
|
358
internal/tlp/manager.go
Normal file
358
internal/tlp/manager.go
Normal file
@@ -0,0 +1,358 @@
|
||||
package tlp
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.gostacks.org/iwasforcedtobehere/WiseTLP/autotlp/internal/system"
|
||||
"git.gostacks.org/iwasforcedtobehere/WiseTLP/autotlp/pkg/types"
|
||||
"git.gostacks.org/iwasforcedtobehere/WiseTLP/autotlp/pkg/utils"
|
||||
)
|
||||
|
||||
type Manager struct {
|
||||
logger *utils.Logger
|
||||
}
|
||||
|
||||
func NewManager(logger *utils.Logger) *Manager {
|
||||
return &Manager{
|
||||
logger: logger.WithComponent("tlp"),
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Manager) GetStatus(ctx context.Context) (*types.TLPStatus, error) {
|
||||
status := &types.TLPStatus{
|
||||
CurrentConfig: make(map[string]string),
|
||||
}
|
||||
|
||||
if utils.CommandExists("tlp") {
|
||||
status.Installed = true
|
||||
|
||||
if version, err := utils.RunCommand("tlp", "--version"); err == nil {
|
||||
parts := strings.Fields(version)
|
||||
if len(parts) >= 2 {
|
||||
status.Version = parts[1]
|
||||
}
|
||||
}
|
||||
|
||||
if output, err := utils.RunCommand("systemctl", "is-active", "tlp"); err == nil {
|
||||
status.Active = strings.TrimSpace(output) == "active"
|
||||
}
|
||||
}
|
||||
|
||||
configPaths := []string{
|
||||
"/etc/tlp.conf",
|
||||
"/etc/default/tlp",
|
||||
}
|
||||
|
||||
for _, path := range configPaths {
|
||||
if utils.FileExists(path) {
|
||||
status.ConfigPath = path
|
||||
status.ConfigExists = true
|
||||
|
||||
if info, err := os.Stat(path); err == nil {
|
||||
modTime := info.ModTime()
|
||||
status.LastModified = &modTime
|
||||
}
|
||||
|
||||
if config, err := m.readConfig(path); err == nil {
|
||||
status.CurrentConfig = config
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return status, nil
|
||||
}
|
||||
|
||||
func (m *Manager) Install(ctx context.Context, sysInfo *system.Info) error {
|
||||
m.logger.Info("Installing TLP", "distro", sysInfo.Distribution, "package_manager", sysInfo.PackageManager)
|
||||
|
||||
if utils.CommandExists("tlp") {
|
||||
return fmt.Errorf("TLP is already installed")
|
||||
}
|
||||
|
||||
if !utils.IsRoot() {
|
||||
return fmt.Errorf("root privileges required for TLP installation")
|
||||
}
|
||||
|
||||
var installCmd []string
|
||||
var updateCmd []string
|
||||
|
||||
switch sysInfo.PackageManager {
|
||||
case "apt":
|
||||
updateCmd = []string{"apt", "update"}
|
||||
installCmd = []string{"apt", "install", "-y", "tlp", "tlp-rdw"}
|
||||
case "dnf":
|
||||
installCmd = []string{"dnf", "install", "-y", "tlp", "tlp-rdw"}
|
||||
case "yum":
|
||||
installCmd = []string{"yum", "install", "-y", "tlp", "tlp-rdw"}
|
||||
case "zypper":
|
||||
installCmd = []string{"zypper", "install", "-y", "tlp", "tlp-rdw"}
|
||||
case "pacman":
|
||||
updateCmd = []string{"pacman", "-Sy"}
|
||||
installCmd = []string{"pacman", "-S", "--noconfirm", "tlp", "tlp-rdw"}
|
||||
case "apk":
|
||||
installCmd = []string{"apk", "add", "tlp"}
|
||||
default:
|
||||
return fmt.Errorf("unsupported package manager: %s", sysInfo.PackageManager)
|
||||
}
|
||||
|
||||
if len(updateCmd) > 0 {
|
||||
m.logger.Info("Updating package lists")
|
||||
if _, err := utils.RunCommand(updateCmd[0], updateCmd[1:]...); err != nil {
|
||||
m.logger.Warn("Failed to update package lists", "error", err)
|
||||
}
|
||||
}
|
||||
|
||||
m.logger.Info("Installing TLP packages", "command", strings.Join(installCmd, " "))
|
||||
if _, err := utils.RunCommand(installCmd[0], installCmd[1:]...); err != nil {
|
||||
return fmt.Errorf("failed to install TLP: %w", err)
|
||||
}
|
||||
|
||||
m.logger.Info("Enabling TLP service")
|
||||
if _, err := utils.RunCommand("systemctl", "enable", "tlp"); err != nil {
|
||||
m.logger.Warn("Failed to enable TLP service", "error", err)
|
||||
}
|
||||
|
||||
if _, err := utils.RunCommand("systemctl", "start", "tlp"); err != nil {
|
||||
m.logger.Warn("Failed to start TLP service", "error", err)
|
||||
}
|
||||
|
||||
if utils.CommandExists("systemctl") {
|
||||
if _, err := utils.RunCommand("systemctl", "mask", "power-profiles-daemon"); err != nil {
|
||||
m.logger.Debug("power-profiles-daemon not found or already masked")
|
||||
}
|
||||
}
|
||||
|
||||
m.logger.Info("TLP installation completed successfully")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Manager) ApplyConfig(ctx context.Context, config *types.TLPConfiguration) error {
|
||||
m.logger.Info("Applying TLP configuration")
|
||||
|
||||
if err := m.ValidateConfig(config); err != nil {
|
||||
return fmt.Errorf("configuration validation failed: %w", err)
|
||||
}
|
||||
|
||||
configPath := "/etc/tlp.conf"
|
||||
if !utils.FileExists(configPath) {
|
||||
configPath = "/etc/default/tlp"
|
||||
}
|
||||
|
||||
if utils.FileExists(configPath) {
|
||||
backupPath := fmt.Sprintf("%s.backup.%d", configPath, time.Now().Unix())
|
||||
if err := m.backupConfig(configPath, backupPath); err != nil {
|
||||
m.logger.Warn("Failed to backup existing configuration", "error", err)
|
||||
} else {
|
||||
m.logger.Info("Backed up existing configuration", "backup", backupPath)
|
||||
}
|
||||
}
|
||||
|
||||
if err := m.writeConfig(configPath, config); err != nil {
|
||||
return fmt.Errorf("failed to write configuration: %w", err)
|
||||
}
|
||||
|
||||
m.logger.Info("Reloading TLP configuration")
|
||||
if _, err := utils.RunCommand("tlp", "start"); err != nil {
|
||||
m.logger.Warn("Failed to reload TLP configuration", "error", err)
|
||||
}
|
||||
|
||||
m.logger.Info("TLP configuration applied successfully")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Manager) ValidateConfig(config *types.TLPConfiguration) error {
|
||||
if config == nil {
|
||||
return fmt.Errorf("configuration is nil")
|
||||
}
|
||||
|
||||
if len(config.Settings) == 0 {
|
||||
return fmt.Errorf("configuration has no settings")
|
||||
}
|
||||
|
||||
for key, value := range config.Settings {
|
||||
if key == "" {
|
||||
return fmt.Errorf("empty setting key found")
|
||||
}
|
||||
|
||||
switch key {
|
||||
case "TLP_ENABLE":
|
||||
if value != "1" && value != "0" {
|
||||
return fmt.Errorf("TLP_ENABLE must be 0 or 1, got: %s", value)
|
||||
}
|
||||
case "CPU_SCALING_GOVERNOR_ON_AC", "CPU_SCALING_GOVERNOR_ON_BAT":
|
||||
validGovernors := []string{"performance", "powersave", "ondemand", "conservative", "schedutil"}
|
||||
if !utils.Contains(validGovernors, value) {
|
||||
return fmt.Errorf("invalid CPU governor: %s", value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Manager) readConfig(configPath string) (map[string]string, error) {
|
||||
config := make(map[string]string)
|
||||
|
||||
lines, err := utils.ReadFileLines(configPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, line := range lines {
|
||||
line = strings.TrimSpace(line)
|
||||
|
||||
if line == "" || strings.HasPrefix(line, "#") {
|
||||
continue
|
||||
}
|
||||
|
||||
if key, value, ok := utils.ParseKeyValue(line); ok {
|
||||
config[key] = value
|
||||
}
|
||||
}
|
||||
|
||||
return config, nil
|
||||
}
|
||||
|
||||
func (m *Manager) writeConfig(configPath string, config *types.TLPConfiguration) error {
|
||||
if err := os.MkdirAll(filepath.Dir(configPath), 0755); err != nil {
|
||||
return fmt.Errorf("failed to create config directory: %w", err)
|
||||
}
|
||||
|
||||
file, err := os.OpenFile(configPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to open config file: %w", err)
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
fmt.Fprintf(file, "# TLP Configuration File\n")
|
||||
fmt.Fprintf(file, "# Generated by WiseTLP on %s\n", time.Now().Format("2006-01-02 15:04:05"))
|
||||
fmt.Fprintf(file, "# %s\n\n", config.Description)
|
||||
|
||||
for key, value := range config.Settings {
|
||||
if rationale, exists := config.Rationale[key]; exists {
|
||||
fmt.Fprintf(file, "# %s\n", rationale)
|
||||
}
|
||||
fmt.Fprintf(file, "%s=%s\n\n", key, value)
|
||||
}
|
||||
|
||||
if len(config.Warnings) > 0 {
|
||||
fmt.Fprintf(file, "# WARNINGS:\n")
|
||||
for _, warning := range config.Warnings {
|
||||
fmt.Fprintf(file, "# - %s\n", warning)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Manager) backupConfig(configPath, backupPath string) error {
|
||||
input, err := os.ReadFile(configPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return os.WriteFile(backupPath, input, 0644)
|
||||
}
|
||||
|
||||
type Configuration struct {
|
||||
*types.TLPConfiguration
|
||||
}
|
||||
|
||||
func (c *Configuration) Present() error {
|
||||
fmt.Println("\n" + strings.Repeat("=", 60))
|
||||
fmt.Println("GENERATED TLP CONFIGURATION")
|
||||
fmt.Println(strings.Repeat("=", 60))
|
||||
|
||||
fmt.Printf("\nDescription: %s\n", c.Description)
|
||||
|
||||
if len(c.Warnings) > 0 {
|
||||
fmt.Println("\n⚠️ WARNINGS:")
|
||||
for _, warning := range c.Warnings {
|
||||
fmt.Printf(" - %s\n", warning)
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Println("\nConfiguration Settings:")
|
||||
fmt.Println(strings.Repeat("-", 40))
|
||||
|
||||
// Group settings by category for better presentation
|
||||
categories := map[string][]string{
|
||||
"General": {"TLP_ENABLE", "TLP_WARN_LEVEL", "TLP_DEBUG"},
|
||||
"CPU": {"CPU_SCALING_GOVERNOR_ON_AC", "CPU_SCALING_GOVERNOR_ON_BAT",
|
||||
"CPU_SCALING_MIN_FREQ_ON_AC", "CPU_SCALING_MAX_FREQ_ON_AC",
|
||||
"CPU_SCALING_MIN_FREQ_ON_BAT", "CPU_SCALING_MAX_FREQ_ON_BAT"},
|
||||
"Platform": {"PLATFORM_PROFILE_ON_AC", "PLATFORM_PROFILE_ON_BAT"},
|
||||
"Disk": {"DISK_APM_LEVEL_ON_AC", "DISK_APM_LEVEL_ON_BAT",
|
||||
"DISK_SPINDOWN_TIMEOUT_ON_AC", "DISK_SPINDOWN_TIMEOUT_ON_BAT"},
|
||||
"Graphics": {"RADEON_DPM_STATE_ON_AC", "RADEON_DPM_STATE_ON_BAT"},
|
||||
"Network": {"WIFI_PWR_ON_AC", "WIFI_PWR_ON_BAT"},
|
||||
"USB": {"USB_AUTOSUSPEND", "USB_BLACKLIST"},
|
||||
}
|
||||
|
||||
for category, keys := range categories {
|
||||
hasSettings := false
|
||||
var categorySettings []string
|
||||
|
||||
for _, key := range keys {
|
||||
if value, exists := c.Settings[key]; exists {
|
||||
if !hasSettings {
|
||||
categorySettings = append(categorySettings, fmt.Sprintf("\n%s:", category))
|
||||
hasSettings = true
|
||||
}
|
||||
|
||||
rationale := ""
|
||||
if r, exists := c.Rationale[key]; exists {
|
||||
rationale = fmt.Sprintf(" (%s)", r)
|
||||
}
|
||||
|
||||
categorySettings = append(categorySettings, fmt.Sprintf(" %s = %s%s", key, value, rationale))
|
||||
}
|
||||
}
|
||||
|
||||
if hasSettings {
|
||||
for _, setting := range categorySettings {
|
||||
fmt.Println(setting)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Show any remaining settings not in categories
|
||||
fmt.Println("\nOther Settings:")
|
||||
for key, value := range c.Settings {
|
||||
found := false
|
||||
for _, keys := range categories {
|
||||
if utils.Contains(keys, key) {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
rationale := ""
|
||||
if r, exists := c.Rationale[key]; exists {
|
||||
rationale = fmt.Sprintf(" (%s)", r)
|
||||
}
|
||||
fmt.Printf(" %s = %s%s\n", key, value, rationale)
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Println(strings.Repeat("=", 60))
|
||||
|
||||
// Get user approval
|
||||
fmt.Print("\nDo you want to apply this configuration? (y/N): ")
|
||||
var response string
|
||||
fmt.Scanln(&response)
|
||||
|
||||
response = strings.ToLower(strings.TrimSpace(response))
|
||||
if response != "y" && response != "yes" {
|
||||
return fmt.Errorf("configuration rejected by user")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
Reference in New Issue
Block a user