From 265eea3f609ea72aea99633e51b19673922f6ca9 Mon Sep 17 00:00:00 2001 From: Markus Ressel Date: Sun, 6 Feb 2022 17:48:49 +0100 Subject: [PATCH] added "config validate" cli command added test to check for undefined curve IDs in functional curves added "Success" method to ui refactoring updated README.md --- README.md | 64 ++++++++++++++++------- cmd/config/config.go | 14 +++++ cmd/config/validate.go | 33 ++++++++++++ cmd/curve.go | 10 +++- cmd/fan/fan.go | 9 +++- cmd/root.go | 12 ++++- cmd/sensor/sensor.go | 9 +++- internal/configuration/config.go | 17 +++--- internal/configuration/validation.go | 6 ++- internal/configuration/validation_test.go | 43 +++++++++++---- internal/ui/logging.go | 4 ++ 11 files changed, 175 insertions(+), 46 deletions(-) create mode 100644 cmd/config/config.go create mode 100644 cmd/config/validate.go diff --git a/README.md b/README.md index a6059d4..57781e1 100644 --- a/README.md +++ b/README.md @@ -8,11 +8,11 @@

A daemon to control the fans of a computer.

- - [![Programming Language](https://img.shields.io/badge/Go-00ADD8?logo=go&logoColor=white)]() - [![Latest Release](https://img.shields.io/github/release/markusressel/fan2go.svg)](https://github.com/markusressel/fan2go/releases) - [![License](https://img.shields.io/badge/license-AGPLv3-blue.svg)](/LICENSE) - + +[![Programming Language](https://img.shields.io/badge/Go-00ADD8?logo=go&logoColor=white)]() +[![Latest Release](https://img.shields.io/github/release/markusressel/fan2go.svg)](https://github.com/markusressel/fan2go/releases) +[![License](https://img.shields.io/badge/license-AGPLv3-blue.svg)](/LICENSE) +

Screenshot of Pyrra

@@ -22,8 +22,8 @@ * [x] Fan speed control using user-defined speed curves * [x] Fully customizable and composable curve definitions * [x] Massive range of supported devices - * [x] Direct integration with lm-sensors - * [x] File Fan/Sensor for control/measurement of custom devices + * [x] Direct integration with lm-sensors + * [x] File Fan/Sensor for control/measurement of custom devices * [x] Works after resume from suspend * [x] **Stable** device paths after reboot * [x] Automatic analysis of fan properties, like: @@ -137,7 +137,7 @@ fans: path: /tmp/file_fan ``` -```bash +```shell > cat /tmp/file_fan 255 ``` @@ -239,17 +239,38 @@ curves: An example configuration file including more detailed documentation can be found in [fan2go.yaml](/fan2go.yaml). +### Verify your Configuration + +To check whether your configuration is correct before actually running fan2go you can use: + +```shell +> fan2go config validate + INFO Using configuration file at: ./fan2go.yaml + SUCCESS Config looks good! :) +``` + +or to validate a specific config file: + +```shell +> fan2go -c "./my_config.yaml" config validate + INFO Using configuration file at: ./my_config.yaml + WARNING Unused curve configuration: m2_first_ssd_curve + ERROR Validation failed: Curve m2_ssd_curve: no curve definition with id 'm2_first_ssd_curve123' found +``` + ## Run -Assuming you put your configuration file in `/etc/fan2go/fan2go.yaml`: +After successfully verifying your configuration you can launch fan2go from the CLI and make sure the initial setup is +working as expected. Assuming you put your configuration file in `/etc/fan2go/fan2go.yaml` run: ```shell -sudo fan2go +> sudo fan2go ``` Alternatively you can specify the path to your configuration file like this: + ```shell -fan2go -c /home/markus/my_fan2go_config.yaml +> fan2go -c /home/markus/my_fan2go_config.yaml ``` ## As a Service @@ -267,8 +288,8 @@ journalctl -u fan2go -f ## CLI Commands -Although fan2go is a fan controller daemon at heart, it also provides some handy cli commands -to interact with the devices that you have specified within your config. +Although fan2go is a fan controller daemon at heart, it also provides some handy cli commands to interact with the +devices that you have specified within your config. ### Fans interaction @@ -341,8 +362,8 @@ statistics: port: 9000 ``` -You can then see the metics on [http://localhost:9000/metrics](http://localhost:9000/metrics) while the fan2go daemon -is running. +You can then see the metics on [http://localhost:9000/metrics](http://localhost:9000/metrics) while the fan2go daemon is +running. # How it works @@ -361,19 +382,22 @@ To properly control a fan which fan2go has not seen before, its speed curve is a measurements. Measurements taken during this process will then be used to determine the lowest PWM value at which the fan is still running, as well as the highest PWM value that still yields a change in RPM. -All of this is saved to a local database (path given by the `dbPath` config option), so it is only needed once per fan configuration. +All of this is saved to a local database (path given by the `dbPath` config option), so it is only needed once per fan +configuration. -To reduce the risk of runnin the whole system on low fan speeds for such a long period of time, you can force fan2go to initialize only -one fan at a time, using the `runFanInitializationInParallel: false` config option. +To reduce the risk of runnin the whole system on low fan speeds for such a long period of time, you can force fan2go to +initialize only one fan at a time, using the `runFanInitializationInParallel: false` config option. ## Monitoring Temperature and RPM sensors are polled continuously at the rate specified by the `tempSensorPollingRate` config option. -`tempRollingWindowSize`/`rpmRollingWindowSize` amount of measurements are always averaged and stored as the average sensor value. +`tempRollingWindowSize`/`rpmRollingWindowSize` amount of measurements are always averaged and stored as the average +sensor value. ## Fan Controllers -Fan speeds are continuously adjusted at the rate specified by the `controllerAdjustmentTickRate` config option based on the value of their associated curve. +Fan speeds are continuously adjusted at the rate specified by the `controllerAdjustmentTickRate` config option based on +the value of their associated curve. # Dependencies diff --git a/cmd/config/config.go b/cmd/config/config.go new file mode 100644 index 0000000..cca03ef --- /dev/null +++ b/cmd/config/config.go @@ -0,0 +1,14 @@ +package config + +import "github.com/spf13/cobra" + +var Command = &cobra.Command{ + Use: "config", + Short: "Configuration related commands", + Long: ``, + TraverseChildren: true, +} + +func init() { + _ = Command.MarkPersistentFlagRequired("config") +} diff --git a/cmd/config/validate.go b/cmd/config/validate.go new file mode 100644 index 0000000..597ae71 --- /dev/null +++ b/cmd/config/validate.go @@ -0,0 +1,33 @@ +package config + +import ( + "github.com/markusressel/fan2go/internal/configuration" + "github.com/markusressel/fan2go/internal/ui" + "github.com/spf13/cobra" + "os" +) + +var validateCmd = &cobra.Command{ + Use: "validate", + Short: "Validates the current configuration", + Long: ``, + Args: cobra.NoArgs, + RunE: func(cmd *cobra.Command, args []string) error { + // note: config file path parameter comes from the root command (-c) + configPath := configuration.DetectConfigFile() + ui.Info("Using configuration file at: %s", configPath) + configuration.LoadConfig() + + if err := configuration.Validate(); err != nil { + ui.Error("Validation failed: %v", err) + os.Exit(1) + } + + ui.Success("Config looks good! :)") + return nil + }, +} + +func init() { + Command.AddCommand(validateCmd) +} diff --git a/cmd/curve.go b/cmd/curve.go index de549f1..dbde79f 100644 --- a/cmd/curve.go +++ b/cmd/curve.go @@ -19,9 +19,15 @@ import ( var curveCmd = &cobra.Command{ Use: "curve", Short: "Print the measured fan curve(s) to console", - //Long: `All software has versions. This is fan2go's`, Run: func(cmd *cobra.Command, args []string) { - configuration.ReadConfigFile() + configPath := configuration.DetectConfigFile() + ui.Info("Using configuration file at: %s", configPath) + configuration.LoadConfig() + err := configuration.Validate() + if err != nil { + ui.Fatal(err.Error()) + } + persistence := persistence.NewPersistence(configuration.CurrentConfig.DbPath) controllers := hwmon.GetChips() diff --git a/cmd/fan/fan.go b/cmd/fan/fan.go index 1f53ba1..e68157d 100644 --- a/cmd/fan/fan.go +++ b/cmd/fan/fan.go @@ -6,6 +6,7 @@ import ( "github.com/markusressel/fan2go/internal/configuration" "github.com/markusressel/fan2go/internal/fans" "github.com/markusressel/fan2go/internal/hwmon" + "github.com/markusressel/fan2go/internal/ui" "github.com/spf13/cobra" "regexp" ) @@ -30,7 +31,13 @@ func init() { } func getFan(id string) (fans.Fan, error) { - configuration.ReadConfigFile() + configPath := configuration.DetectConfigFile() + ui.Info("Using configuration file at: %s", configPath) + configuration.LoadConfig() + err := configuration.Validate() + if err != nil { + ui.Fatal(err.Error()) + } controllers := hwmon.GetChips() diff --git a/cmd/root.go b/cmd/root.go index a7a1e97..ae3ea15 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -2,6 +2,7 @@ package cmd import ( "fmt" + "github.com/markusressel/fan2go/cmd/config" "github.com/markusressel/fan2go/cmd/fan" "github.com/markusressel/fan2go/cmd/sensor" "github.com/markusressel/fan2go/internal" @@ -30,7 +31,14 @@ on your computer based on temperature sensors.`, setupUi() printHeader() - configuration.ReadConfigFile() + configPath := configuration.DetectConfigFile() + ui.Info("Using configuration file at: %s", configPath) + configuration.LoadConfig() + err := configuration.Validate() + if err != nil { + ui.Fatal(err.Error()) + } + internal.RunDaemon() }, } @@ -41,6 +49,8 @@ func init() { rootCmd.PersistentFlags().BoolVarP(&noStyle, "no-style", "", false, "Disable all terminal output styling") rootCmd.PersistentFlags().BoolVarP(&verbose, "verbose", "v", false, "More verbose output") + rootCmd.AddCommand(config.Command) + rootCmd.AddCommand(fan.Command) rootCmd.AddCommand(sensor.Command) } diff --git a/cmd/sensor/sensor.go b/cmd/sensor/sensor.go index 089366f..25290b9 100644 --- a/cmd/sensor/sensor.go +++ b/cmd/sensor/sensor.go @@ -6,6 +6,7 @@ import ( "github.com/markusressel/fan2go/internal/configuration" "github.com/markusressel/fan2go/internal/hwmon" "github.com/markusressel/fan2go/internal/sensors" + "github.com/markusressel/fan2go/internal/ui" "github.com/pterm/pterm" "github.com/spf13/cobra" "regexp" @@ -50,7 +51,13 @@ func init() { } func getSensor(id string) (sensors.Sensor, error) { - configuration.ReadConfigFile() + configPath := configuration.DetectConfigFile() + ui.Info("Using configuration file at: %s", configPath) + configuration.LoadConfig() + err := configuration.Validate() + if err != nil { + ui.Fatal(err.Error()) + } controllers := hwmon.GetChips() diff --git a/internal/configuration/config.go b/internal/configuration/config.go index 5068271..318cc04 100644 --- a/internal/configuration/config.go +++ b/internal/configuration/config.go @@ -71,21 +71,18 @@ func setDefaultValues() { viper.SetDefault("fans", []FanConfig{}) } -func ReadConfigFile() { +// DetectConfigFile detects the path of the first existing config file +func DetectConfigFile() string { if err := viper.ReadInConfig(); err != nil { // config file is required, so we fail here ui.Fatal("Error reading config file, %s", err) } - // this is only populated _after_ ReadInConfig() - ui.Info("Using configuration file at: %s", viper.ConfigFileUsed()) - - LoadConfig() + return GetFilePath() +} - err := validateConfig(&CurrentConfig) - if err != nil { - ui.Fatal(err.Error()) - } - ui.Fatal("Config validation failed") +// GetFilePath this is only populated _after_ DetectConfigFile() +func GetFilePath() string { + return viper.ConfigFileUsed() } func LoadConfig() { diff --git a/internal/configuration/validation.go b/internal/configuration/validation.go index 9f628b9..cb13393 100644 --- a/internal/configuration/validation.go +++ b/internal/configuration/validation.go @@ -8,7 +8,11 @@ import ( "github.com/markusressel/fan2go/internal/util" ) -func validateConfig(config *Configuration) error { +func Validate() error { + return ValidateConfig(&CurrentConfig) +} + +func ValidateConfig(config *Configuration) error { err := validateSensors(config) if err != nil { return err diff --git a/internal/configuration/validation_test.go b/internal/configuration/validation_test.go index 47224be..8f0a507 100644 --- a/internal/configuration/validation_test.go +++ b/internal/configuration/validation_test.go @@ -19,7 +19,7 @@ func TestValidateFanSubConfigIsMissing(t *testing.T) { } // WHEN - err := validateConfig(&config) + err := ValidateConfig(&config) // THEN assert.EqualError(t, err, "Fans fan: sub-configuration for fan is missing, use one of: hwmon | file") @@ -41,7 +41,7 @@ func TestValidateFanCurveWithIdIsNotDefined(t *testing.T) { } // WHEN - err := validateConfig(&config) + err := ValidateConfig(&config) // THEN assert.EqualError(t, err, "Fan fan: no curve definition with id 'curve' found") @@ -60,7 +60,7 @@ func TestValidateCurveSubConfigSensorIdIsMissing(t *testing.T) { } // WHEN - err := validateConfig(&config) + err := ValidateConfig(&config) // THEN assert.EqualError(t, err, "Curve curve: sub-configuration for curve is missing, use one of: linear | function") @@ -82,7 +82,7 @@ func TestValidateCurveSensorIdIsMissing(t *testing.T) { } // WHEN - err := validateConfig(&config) + err := ValidateConfig(&config) // THEN assert.EqualError(t, err, "Curve curve: Missing sensorId") @@ -104,7 +104,7 @@ func TestValidateCurveSensorWithIdIsNotDefined(t *testing.T) { } // WHEN - err := validateConfig(&config) + err := ValidateConfig(&config) // THEN assert.EqualError(t, err, "Curve curve: no sensor definition with id 'sensor' found") @@ -127,7 +127,7 @@ func TestValidateCurveDependencyToSelf(t *testing.T) { } // WHEN - err := validateConfig(&config) + err := ValidateConfig(&config) // THEN assert.EqualError(t, err, "Curve curve: a curve cannot reference itself") @@ -176,7 +176,7 @@ func TestValidateCurveDependencyCycle(t *testing.T) { } // WHEN - err := validateConfig(&config) + err := ValidateConfig(&config) // THEN assert.Contains(t, err.Error(), "You have created a curve dependency cycle") @@ -186,6 +186,29 @@ func TestValidateCurveDependencyCycle(t *testing.T) { assert.Contains(t, err.Error(), "curve2") } +func TestValidateCurveDependencyWithIdIsNotDefined(t *testing.T) { + // GIVEN + config := Configuration{ + Curves: []CurveConfig{ + { + ID: "curve1", + Function: &FunctionCurveConfig{ + Type: FunctionAverage, + Curves: []string{ + "curve2", + }, + }, + }, + }, + } + + // WHEN + err := ValidateConfig(&config) + + // THEN + assert.EqualError(t, err, "Curve curve1: no curve definition with id 'curve2' found") +} + func TestValidateCurve(t *testing.T) { // GIVEN config := Configuration{ @@ -211,7 +234,7 @@ func TestValidateCurve(t *testing.T) { } // WHEN - err := validateConfig(&config) + err := ValidateConfig(&config) // THEN assert.NoError(t, err) @@ -228,7 +251,7 @@ func TestValidateSensorSubConfigSensorIdIsMissing(t *testing.T) { } // WHEN - err := validateConfig(&config) + err := ValidateConfig(&config) // THEN assert.EqualError(t, err, "Sensor sensor: sub-configuration for sensor is missing, use one of: hwmon | file") @@ -248,7 +271,7 @@ func TestValidateSensor(t *testing.T) { } // WHEN - err := validateConfig(&config) + err := ValidateConfig(&config) // THEN assert.NoError(t, err) diff --git a/internal/ui/logging.go b/internal/ui/logging.go index 1849583..a65bd96 100644 --- a/internal/ui/logging.go +++ b/internal/ui/logging.go @@ -20,6 +20,10 @@ func Debug(format string, a ...interface{}) { pterm.Debug.Printfln(format, a...) } +func Success(format string, a ...interface{}) { + pterm.Success.Printfln(format, a...) +} + func Info(format string, a ...interface{}) { pterm.Info.Printfln(format, a...) }