From d646be70e9a150d31803d86b030b7778874bfdfa Mon Sep 17 00:00:00 2001 From: Tobias Bauriedel Date: Fri, 15 Nov 2024 15:15:16 +0100 Subject: [PATCH] Refactor control (#135) * Refactor control handling Introduce answer-file Update wizard * Update README --- .github/workflows/golangci-lint.yml | 2 +- .golangci.yml | 74 ++++---- README.md | 123 ++++++++----- doc/answer-file.yml.example | 21 +++ go.mod | 2 +- internal/arguments/arguments.go | 165 ----------------- internal/collection/collection.go | 9 +- internal/config/answerfile.go | 49 +++++ internal/config/config.go | 81 ++++++++ internal/config/wizard.go | 276 ++++++++++++++++++++++++++++ internal/metrics/metrics.go | 8 +- internal/util/util.go | 9 + main.go | 275 ++++++++++++++------------- modules/ansible/collector.go | 2 +- modules/corosync/collector.go | 2 +- modules/elastic/collector.go | 2 +- modules/foreman/collector.go | 2 +- modules/grafana/collector.go | 2 +- modules/graphite/collector.go | 2 +- modules/graylog/collector.go | 2 +- modules/icinga2/collector.go | 36 +++- modules/icinga2/icingaapi/api.go | 85 +++++++++ modules/icingadb/collector.go | 2 +- modules/icingadirector/collector.go | 2 +- modules/icingaweb2/collector.go | 2 +- modules/influxdb/collector.go | 2 +- modules/keepalived/collector.go | 2 +- modules/mongodb/collector.go | 2 +- modules/mysql/collector.go | 2 +- modules/postgresql/collector.go | 2 +- modules/prometheus/collector.go | 2 +- modules/puppet/collector.go | 2 +- modules/redis/collector.go | 2 +- modules/webservers/collector.go | 2 +- 34 files changed, 827 insertions(+), 426 deletions(-) create mode 100644 doc/answer-file.yml.example delete mode 100644 internal/arguments/arguments.go create mode 100644 internal/config/answerfile.go create mode 100644 internal/config/config.go create mode 100644 internal/config/wizard.go create mode 100644 modules/icinga2/icingaapi/api.go diff --git a/.github/workflows/golangci-lint.yml b/.github/workflows/golangci-lint.yml index 69fd1b1..a2c245d 100644 --- a/.github/workflows/golangci-lint.yml +++ b/.github/workflows/golangci-lint.yml @@ -15,4 +15,4 @@ jobs: - name: golangci-lint uses: golangci/golangci-lint-action@v6 with: - version: v1.61 + version: v1.61.0 diff --git a/.golangci.yml b/.golangci.yml index f486843..beefb66 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -2,46 +2,38 @@ run: timeout: 5m tests: false linters: - enable-all: true - disable: - - goimports - - cyclop - - depguard - - exhaustivestruct - - exhaustruct - - forbidigo - - forcetypeassert - - gci - - gochecknoglobals - - gochecknoinits - - godox - - godot - - goerr113 - - gofumpt - - gomnd - - lll - - musttag - - nakedret - - nlreturn - - nolintlint - - nonamedreturns - - tagliatelle - - varnamelen - - wrapcheck - - golint # deprecated - - nosnakecase # deprecated - - scopelint # deprecated - - ifshort # deprecated - - interfacer # deprecated - - structcheck # deprecated - - maligned # deprecated - - varcheck # deprecated - - deadcode # deprecated - - goconst # not needed in our case - - perfsprint # not needed in our case + enable-all: true + disable: + - err113 + - goimports + - cyclop + - depguard + - exhaustruct + - forbidigo + - forcetypeassert + - gci + - gochecknoglobals + - gochecknoinits + - godox + - godot + - gofumpt + - gomnd + - lll + - musttag + - nakedret + - nlreturn + - nolintlint + - nonamedreturns + - tagalign + - tagliatelle + - varnamelen + - wrapcheck + - goconst # not needed in our case + - perfsprint # not needed in our case linters-settings: - funlen: + funlen: ignore-comments: true - lines: 80 - nestif: - min-complexity: 5 + lines: 120 + statements: -1 + nestif: + min-complexity: 5 diff --git a/README.md b/README.md index 4f5ceb4..51dcb99 100644 --- a/README.md +++ b/README.md @@ -5,63 +5,15 @@ The support collector allows to collect relevant information from servers. The resulting ZIP file can be given to second to get an insight into the system. -> **WARNING:** DO NOT transfer the generated file over insecure connections, it contains potential sensitive +> **WARNING:** Do not transfer the generated file over insecure connections, it contains potential sensitive > information! If you are a customer, you can contact us at [support@netways.de](mailto:support@netways.de) or [netways.de/en/contact/](https://www.netways.de/en/contact/). -Inspired by [NETWAYS/icinga2-diagnostics](https://github.com/Icinga/icinga2-diagnostics). +The initial idea and inspiration came from [NETWAYS/icinga2-diagnostics](https://github.com/Icinga/icinga2-diagnostics). -## Usage - -`$ support-collector` - -The CLI wizard will guide you through the possible arguments after calling the command. -If you prefer to pass the arguments in the command call, use `--non-interactive` and pass the arguments as described in the documentation. - -`--hide`, `--command-timeout` can only be used as CLI argument. - -You can also combine your CLI arguments and the wizard. All arguments you pass from the CLI will be given into the wizard. - ---- - -> **WARNING:** Some passwords or secrets are automatically removed, but this no guarantee, so be careful what you share! - -The `--hide` flag can be used multiple times to hide sensitive data. -As these obfuscators are based on regex, you must add a regex pattern that meets your requirements. - -`# support-collector --hide "Secret:\s*(.*)"` - -This will replace: -* Values like `Secret: DummyValue` - -In addition, files and folders that follow a specific pattern are not collected. This affects all files that correspond to the following filters: -`.*`, `*~`, `*.key`, `*.csr`, `*.crt`, and `*.pem` - -By default, we collect all we can find. You can control this by only enabling certain modules, or disabling some. - -If you want to see what is collected, add `--verbose` - -To collect advanced data for module `Icinga 2`, you can use the Icinga 2 API to collect data from all endpoints provided. -The API requests are performed with a global API user you have to create yourself. Just create that user in a global zone like 'director-global' to sync it to all endpoints - -| Short | Long | Description | -|:-----:|:------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| -o | --output | Output file for the zip content (default: current directory and named like '\$HOSTNAME'-netways-support-\$TIMESTAMP.zip) | -| | --non-interactive | Disable the interactive CLI wizard | -| | --no-details | Disable detailed collection including logs and more | -| | --enable | List of enabled modules (default: all) | -| | --disable | List of disabled modules (default: none) | -| | --hide | List of keywords to obfuscate. Can be used multiple times | -| | --command-timeout | Timeout for command execution in modules (default: 1m0s) | -| | --icinga2-api-user | Username of global Icinga 2 API user to collect data about Icinga 2 Infrastructure (Optional and only for module `icinga2`) | -| | --icinga2-api-pass | Password for global Icinga 2 API user to collect data about Icinga 2 Infrastructure (Optional and only for module `icinga2`) | -| | --icinga2-api-endpoints | List of Icinga 2 API Endpoints (including port) to collect data from. FQDN or IP address must be reachable. (Example: i2-master01.local:5665) (Optional and only for module `icinga2`) | -| -v | --verbose | Enable verbose logging | -| -V | --version | Print version and exit | - -## Modules +## Available Modules A brief overview about the modules, you can check the source code under [modules](modules) for what exactly is collected. @@ -91,6 +43,75 @@ Most modules check if the component is installed before trying to collect data. | redis | Configuration, logs, packages and service status | | webservers | Includes apache2, httpd and nginx. Collects configuration, logs, packages and service status | + +## Usage + +`$ support-collector` + +The CLI wizard will guide you through the possible arguments after calling the command. If you prefer to skip the wizard, you can use `--disable-wizard` and use the default control values. +A more detailed control is possible through the use of an answer-file. + +**Available arguments:** + +| Short | Long | Description | +|-------|------------------------|-----------------------------------------------------------| +| -f | --answer-file | Provide an answer-file to control the collection | +| | --disable-wizard | Disable interactive wizard and use default control values | +| | --generate-answer-file | Generate an example answer-file with default values | +| -V | --verbose | Enable verbose logging | +| -v | --version | Print version and exit | + +## Obfuscation + +> **WARNING:** Some passwords or secrets are automatically removed, but this no guarantee, so be careful what you share! + +With using an answer-file, you are able to add multiple custom obfuscators. +As these obfuscators are based on regex, you must add a valid regex pattern that meets your requirements. + +For example, `Secret:\s*(.*)` will find `Secret: DummyValue` and set it to `Secret: `. + +In addition, files and folders that follow a specific pattern are not collected. This affects all files that correspond to the following filters: +`.*`, `*~`, `*.key`, `*.csr`, `*.crt`, and `*.pem` + +## Answer File + +By providing an answer-file you can customize the data collection. +In addition to some general control values that customize the collection, further details for modules - that are not included by default - can be collected and configured. + +The answer-file has to be in YAML format. +To generate a default answer-file, you can use `--generate-answer-file`. + +To provide an answer-file, just use `--answer-file .yml` or `-f .yml`. With using this, the wizard will be skipped. + +You can find an example answer-file [here](doc/answer-file.yml.example). + +### General + +Inside the general section we can configure some general behavior for the support-collector. +````yaml +general: + outputFile: data.zip # Name of the resulting zip file + enabledModules: [] # List of enabled modules (Can also be 'all') + disabledModules: [] # List of disabled modules + extraObfuscators: [] # Custom obfuscators that should be applied + detailedCollection: true # Enable detailed collection + commandTimeout: [] # Command timeout for exec commands (Default 1m0s) +```` + +### Icinga 2 + +For the module `icinga2` it is possible do define some API endpoints to collect data from. +There is no limit of endpoints that can be defined. + +````yaml +icinga2: + endpoints: # List of Icinga 2 API endpoint to collect data from + - address: 127.0.0.1 # Address of endpoint + port: 5665 # Icinga 2 port + username: icinga # Icinga 2 API user + password: icinga # Icinga 2 API password +```` + ## Supported systems | Distribution | Tested on | Supported | diff --git a/doc/answer-file.yml.example b/doc/answer-file.yml.example new file mode 100644 index 0000000..634c0d7 --- /dev/null +++ b/doc/answer-file.yml.example @@ -0,0 +1,21 @@ +general: + outputFile: data.zip + enabledModules: + - all + disabledModules: + - mysql + extraObfuscators: + - Secret:\s*?(.*) + - Pass[:|=]\s*(.+) + detailedCollection: true + commandTimeout: 1m0s +icinga2: + endpoints: + - address: 10.20.140.10 + port: 5665 + username: icinga + password: superStrong1! + - address: 10.20.140.11 + port: 5665 + username: icinga + password: alsoSuperStrong2! diff --git a/go.mod b/go.mod index fcd703b..b6cb8f5 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/NETWAYS/support-collector -go 1.22 +go 1.23 require ( github.com/Showmax/go-fqdn v1.0.0 diff --git a/internal/arguments/arguments.go b/internal/arguments/arguments.go deleted file mode 100644 index 6da62f2..0000000 --- a/internal/arguments/arguments.go +++ /dev/null @@ -1,165 +0,0 @@ -package arguments - -import ( - "bufio" - "fmt" - flag "github.com/spf13/pflag" - "os" - "strings" -) - -var ( - NonInteractive bool - validBooleanInputs = map[string]bool{ - "y": true, - "yes": true, - "n": false, - "no": false, - "true": true, - "false": false, - } -) - -const interactiveHelpText = `Welcome to the support-collector argument wizard! -We will guide you through all required details.` - -type Argument struct { - Name string - InputFunction func() - Dependency func() bool -} - -type Handler struct { - scanner *bufio.Scanner - arguments []Argument -} - -// New creates a new Handler object -func New() Handler { - return Handler{ - scanner: bufio.NewScanner(os.Stdin), - } -} - -func (args *Handler) CollectArgsFromStdin(availableModules string) []error { - fmt.Printf("%s\n\nAvailable modules are: %s\n\n", interactiveHelpText, availableModules) - - errors := make([]error, 0, len(args.arguments)) - - for _, argument := range args.arguments { - if argument.Dependency == nil { - argument.InputFunction() - continue - } - - if ok := argument.Dependency(); ok { - argument.InputFunction() - continue - } - - errors = append(errors, fmt.Errorf("argument '%s' is not matching the needed depenency. Skipping... ", argument.Name)) - } - - fmt.Print("\nInteractive wizard finished. Starting...\n\n") - - return errors -} - -func (args *Handler) NewPromptStringVar(callback *string, name, defaultValue, usage string, required bool, dependency func() bool) { - flag.StringVar(callback, name, defaultValue, usage) - - args.arguments = append(args.arguments, Argument{ - Name: name, - InputFunction: func() { - if *callback != "" { - defaultValue = *callback - } - - args.newStringPrompt(callback, defaultValue, usage, required) - }, - Dependency: dependency, - }) -} - -func (args *Handler) NewPromptStringSliceVar(callback *[]string, name string, defaultValue []string, usage string, required bool, dependency func() bool) { - flag.StringSliceVar(callback, name, defaultValue, usage) - - args.arguments = append(args.arguments, Argument{ - Name: name, - InputFunction: func() { - if len(*callback) > 0 { - defaultValue = *callback - } - - var input string - - args.newStringPrompt(&input, strings.Join(defaultValue, ","), usage, required) - *callback = strings.Split(input, ",") - }, - Dependency: dependency, - }) -} - -func (args *Handler) NewPromptBoolVar(callback *bool, name string, defaultValue bool, usage string, dependency func() bool) { - flag.BoolVar(callback, name, defaultValue, usage) - - args.arguments = append(args.arguments, Argument{ - Name: name, - InputFunction: func() { - args.newBoolPrompt(callback, defaultValue, usage) - }, - Dependency: dependency, - }) -} - -func (args *Handler) newStringPrompt(callback *string, defaultValue, usage string, required bool) { - for { - fmt.Printf("%s - (Preselection: '%s'): ", usage, defaultValue) - - if args.scanner.Scan() { - input := args.scanner.Text() - - switch { - case input != "": - *callback = input - return - case input == "" && defaultValue != "": - *callback = defaultValue - return - case input == "" && !required: - return - } - } else { - if err := args.scanner.Err(); err != nil { - _, _ = fmt.Fprintln(os.Stderr, "reading standard input:", err) - return - } - } - } -} - -func (args *Handler) newBoolPrompt(callback *bool, defaultValue bool, usage string) { - for { - fmt.Printf("%s [y/n] - (Preselection: '%t'): ", usage, defaultValue) - - if args.scanner.Scan() { - input := strings.ToLower(args.scanner.Text()) - - if input != "" && isValidBoolString(input) { - *callback = validBooleanInputs[input] - break - } else if input == "" { - *callback = defaultValue - break - } - } - } -} - -func isValidBoolString(input string) bool { - if _, ok := validBooleanInputs[input]; !ok { - return false - } - - return true -} diff --git a/internal/collection/collection.go b/internal/collection/collection.go index 55d46b8..4149e22 100644 --- a/internal/collection/collection.go +++ b/internal/collection/collection.go @@ -4,6 +4,8 @@ import ( "archive/zip" "bytes" "fmt" + "github.com/NETWAYS/support-collector/internal/config" + "github.com/NETWAYS/support-collector/internal/metrics" "io" "strings" "time" @@ -21,17 +23,22 @@ type Collection struct { Obfuscators []*obfuscate.Obfuscator Detailed bool JournalLoggingInterval string + Config config.Config + Metric *metrics.Metrics } -// New initializes new collection +// New initializes new collection with defaults func New(w io.Writer) (c *Collection) { c = &Collection{ Output: zip.NewWriter(w), Log: logrus.New(), LogData: &bytes.Buffer{}, ExecTimeout: DefaultTimeout, + Obfuscators: nil, Detailed: true, JournalLoggingInterval: "7 days ago", + Config: config.GetControlDefaultObject(), + Metric: nil, } c.Log.Out = c.LogData diff --git a/internal/config/answerfile.go b/internal/config/answerfile.go new file mode 100644 index 0000000..d818ee7 --- /dev/null +++ b/internal/config/answerfile.go @@ -0,0 +1,49 @@ +package config + +import ( + "fmt" + "gopkg.in/yaml.v3" + "io" + "os" +) + +const defaultAnswerFileName = "answer-file.yml" + +// GenerateDefaultAnswerFile creates a new answer-file with default values in current dir +func GenerateDefaultAnswerFile() error { + file, err := os.Create(defaultAnswerFileName) + if err != nil { + return fmt.Errorf("could not create answer file: %w", err) + } + + defer file.Close() + + defaults := GetControlDefaultObject() + + yamlData, err := yaml.Marshal(defaults) + if err != nil { + return fmt.Errorf("could not marshal yamldata for answer-file: %w", err) + } + + _, err = io.Writer.Write(file, yamlData) + if err != nil { + return fmt.Errorf("could not write to answer file: %w", err) + } + + return nil +} + +// ReadAnswerFile reads given values from answerFile and returns new Config +func ReadAnswerFile(answerFile string, conf *Config) error { + data, err := os.ReadFile(answerFile) + if err != nil { + return fmt.Errorf("could not read answer file: %w", err) + } + + err = yaml.Unmarshal(data, &conf) + if err != nil { + return fmt.Errorf("could not unmarshal answer file: %w", err) + } + + return nil +} diff --git a/internal/config/config.go b/internal/config/config.go new file mode 100644 index 0000000..954720b --- /dev/null +++ b/internal/config/config.go @@ -0,0 +1,81 @@ +package config + +import ( + "fmt" + "github.com/NETWAYS/support-collector/internal/util" + "github.com/NETWAYS/support-collector/modules/icinga2/icingaapi" + "slices" + "time" +) + +var ( + ModulesOrder = []string{ + "ansible", + "base", + "corosync", + "elastic", + "foreman", + "grafana", + "graphite", + "graylog", + "icinga-director", + "icinga2", + "icingadb", + "icingaweb2", + "influxdb", + "keepalived", + "mongodb", + "mysql", + "postgresql", + "prometheus", + "puppet", + "redis", + "webservers", + } +) + +type Config struct { + General General `yaml:"general" json:"general"` + Icinga2 Icinga2 `yaml:"icinga2" json:"icinga2"` +} + +type General struct { + OutputFile string `yaml:"outputFile" json:"outputFile"` + AnswerFile string `yaml:"answerFile,omitempty" json:"answerFile,omitempty"` + EnabledModules []string `yaml:"enabledModules" json:"enabledModules"` + DisabledModules []string `yaml:"disabledModules" json:"disabledModules"` + ExtraObfuscators []string `yaml:"extraObfuscators" json:"extraObfuscators"` + DetailedCollection bool `yaml:"detailedCollection" json:"detailedCollection"` + CommandTimeout time.Duration `yaml:"commandTimeout" json:"commandTimeout"` +} + +type Icinga2 struct { + Endpoints []icingaapi.Endpoint `yaml:"endpoints" json:"endpoints"` +} + +// GetControlDefaultObject returns a new Config object with some pre-defined default values +func GetControlDefaultObject() Config { + return Config{ + General: General{ + OutputFile: util.BuildFileName(), + AnswerFile: "", + EnabledModules: []string{"all"}, + DisabledModules: nil, + ExtraObfuscators: nil, + DetailedCollection: true, + CommandTimeout: 60 * time.Second, //nolint:mnd + }, + Icinga2: Icinga2{}, + } +} + +// ValidateConfig validates the given config.Config for errors. Returns []error +func ValidateConfig(conf Config) (errors []error) { + for _, name := range conf.General.EnabledModules { + if !slices.Contains(ModulesOrder, name) { + errors = append(errors, fmt.Errorf("invalid module '%s' provided. Cant be enabled", name)) + } + } + + return errors +} diff --git a/internal/config/wizard.go b/internal/config/wizard.go new file mode 100644 index 0000000..8f4ba04 --- /dev/null +++ b/internal/config/wizard.go @@ -0,0 +1,276 @@ +package config + +import ( + "bufio" + "fmt" + "github.com/NETWAYS/support-collector/modules/icinga2/icingaapi" + "github.com/sirupsen/logrus" + "os" + "strconv" + "strings" +) + +const interactiveHelpText = `Welcome to the support-collector wizard! +We will guide you through all required details. + +If you do not want to use the wizard, you can also pass an answer file containing the configuration. +For more details have a look at the official repository. +https://github.com/NETWAYS/support-collector` + +var ( + validBooleanInputs = map[string]bool{ + "y": true, + "yes": true, + "true": true, + "n": false, + "no": false, + "false": false, + } +) + +type argument struct { + name string + inputFunction func() + dependency func() bool +} + +type Wizard struct { + Scanner *bufio.Scanner + Arguments []argument +} + +// NewWizard creates a new Wizard +func NewWizard() Wizard { + return Wizard{ + Scanner: bufio.NewScanner(os.Stdin), + } +} + +// Parse starts the interactive wizard for all Arguments defined in Wizard +func (w *Wizard) Parse(availableModules string) { + // Print "welcome" text for wizard + fmt.Printf("%s\n\nThe following modules are available:\n%s\n\n", interactiveHelpText, availableModules) + + for _, arg := range w.Arguments { + if arg.dependency == nil { + arg.inputFunction() + continue + } + + if ok := arg.dependency(); ok { + arg.inputFunction() + continue + } + } +} + +// AddStringVar adds argument for a string variable +// +// callback: Variable to save the input to +// name: Internal name +// defaultValue: Default +// usage: usage string +// required: bool +// dependency: Add dependency function to validate if that argument will be added or not +func (w *Wizard) AddStringVar(callback *string, name, defaultValue, usage string, required bool, dependency func() bool) { + w.Arguments = append(w.Arguments, argument{ + name: name, + inputFunction: func() { + if *callback != "" { + defaultValue = *callback + } + + w.newStringPromptWithDefault(callback, defaultValue, usage, required) + }, + dependency: dependency, + }) +} + +// AddStringSliceVar adds argument for a string slice variable +// +// callback: Variable to save the input to +// name: Internal name +// defaultValue: Default +// usage: usage string +// required: bool +// dependency: Add dependency function to validate if that argument will be added or not +func (w *Wizard) AddStringSliceVar(callback *[]string, name string, defaultValue []string, usage string, required bool, dependency func() bool) { + w.Arguments = append(w.Arguments, argument{ + name: name, + inputFunction: func() { + if len(*callback) > 0 { + defaultValue = *callback + } + + var input string + + w.newStringPromptWithDefault(&input, strings.Join(defaultValue, ","), usage, required) + *callback = strings.Split(input, ",") + }, + dependency: dependency, + }) +} + +// AddBoolVar adds argument for a boolean variable +// +// callback: Variable to save the input to +// name: Internal name +// defaultValue: Default +// usage: usage string +// dependency: Add dependency function to validate if that argument will be added or not +func (w *Wizard) AddBoolVar(callback *bool, name string, defaultValue bool, usage string, dependency func() bool) { + w.Arguments = append(w.Arguments, argument{ + name: name, + inputFunction: func() { + w.newBoolPrompt(callback, defaultValue, usage) + }, + dependency: dependency, + }) +} + +func (w *Wizard) AddIcingaEndpoints(callback *[]icingaapi.Endpoint, name, usage string, dependency func() bool) { + w.Arguments = append(w.Arguments, argument{ + name: name, + inputFunction: func() { + // Ask if endpoint should be added. If not, return + var collect bool + w.newBoolPrompt(&collect, true, usage) + + if !collect { + return + } + + var endpoints []icingaapi.Endpoint + + for collect { + e := w.newIcinga2EndpointPrompt() + endpoints = append(endpoints, e) + + w.newBoolPrompt(&collect, false, "Collect more Icinga 2 API endpoints?") + } + + *callback = endpoints + }, + dependency: dependency, + }) +} + +// newStringPromptWithDefault creates a new stdout / stdin prompt for a string +func (w *Wizard) newStringPrompt(callback *string, usage string, required bool) { + for { + fmt.Printf("%s: ", usage) + + if w.Scanner.Scan() { + input := w.Scanner.Text() + + switch { + case input != "": + *callback = input + return + case input == "" && !required: + return + } + } else { + if err := w.Scanner.Err(); err != nil { + _, _ = fmt.Fprintln(os.Stderr, "reading standard input:", err) + return + } + } + } +} + +// newStringPromptWithDefault creates a new stdout / stdin prompt for a string with default a value +func (w *Wizard) newStringPromptWithDefault(callback *string, defaultValue, usage string, required bool) { + for { + fmt.Printf("%s - (Default: '%s'): ", usage, defaultValue) + + if w.Scanner.Scan() { + input := w.Scanner.Text() + + switch { + case input != "": + *callback = input + return + case input == "" && defaultValue != "": + *callback = defaultValue + return + case input == "" && !required: + return + } + } else { + if err := w.Scanner.Err(); err != nil { + _, _ = fmt.Fprintln(os.Stderr, "reading standard input:", err) + return + } + } + } +} + +// newIntPromptWithDefault creates a new stdout / stdin prompt for an int +func (w *Wizard) newIntPromptWithDefault(callback *int, defaultValue int, usage string, required bool) { + for { + fmt.Printf("%s - (Default: '%d'): ", usage, defaultValue) + + if w.Scanner.Scan() { + input := w.Scanner.Text() + + switch { + case input != "": + converted, err := strconv.Atoi(input) + if err != nil { + logrus.Fatalf("could not convert '%s' to integer: %s", input, err) + } + + *callback = converted + + return + case input == "" && required: + *callback = defaultValue + return + } + } else { + if err := w.Scanner.Err(); err != nil { + _, _ = fmt.Fprintln(os.Stderr, "reading standard input:", err) + return + } + } + } +} + +// newBoolPrompt creates a new stdout / stdin prompt for a boolean +func (w *Wizard) newBoolPrompt(callback *bool, defaultValue bool, usage string) { + for { + fmt.Printf("%s [y/n] - (Default: '%t'): ", usage, defaultValue) + + if w.Scanner.Scan() { + input := strings.ToLower(w.Scanner.Text()) + + if input != "" && isValidBoolString(input) { + *callback = validBooleanInputs[input] + break + } else if input == "" { + *callback = defaultValue + break + } + } + } +} + +// isValidBoolString validate the inputs for boolean. Valid are everything from var validBoolString +func isValidBoolString(input string) bool { + if _, ok := validBooleanInputs[input]; !ok { + return false + } + + return true +} + +// newIcinga2EndpointPrompt creates all needed stdout / stdin prompts to build an Icinga API endpoint. Returns icingaapi.Endpoint +func (w *Wizard) newIcinga2EndpointPrompt() (e icingaapi.Endpoint) { + w.newStringPromptWithDefault(&e.Address, "127.0.0.1", "Host address / FQDN of the endpoint", true) + w.newIntPromptWithDefault(&e.Port, 5665, "Port number of the endpoint", true) //nolint:mnd + w.newStringPrompt(&e.Username, "Username for the api connection", true) + w.newStringPrompt(&e.Password, "Password for the api connection", true) + + return e +} diff --git a/internal/metrics/metrics.go b/internal/metrics/metrics.go index e6e9912..1f6938f 100644 --- a/internal/metrics/metrics.go +++ b/internal/metrics/metrics.go @@ -1,6 +1,7 @@ package metrics import ( + "github.com/NETWAYS/support-collector/internal/config" "github.com/NETWAYS/support-collector/internal/obfuscate" "os" "strings" @@ -8,9 +9,10 @@ import ( ) type Metrics struct { - Command string `json:"command"` - Version string `json:"version"` - Timings map[string]time.Duration `json:"timings"` + Command string `json:"command"` + Controls config.Config `json:"controls"` + Version string `json:"version"` + Timings map[string]time.Duration `json:"timings"` } // New creates new Metrics diff --git a/internal/util/util.go b/internal/util/util.go index 73136c4..d4cd146 100644 --- a/internal/util/util.go +++ b/internal/util/util.go @@ -5,10 +5,14 @@ import ( "os/exec" "os/user" "strings" + "time" "github.com/sirupsen/logrus" ) +// FilePrefix for the outfile file. +const FilePrefix = "support-collector" + // StringInSlice matches if a string is contained in a slice. func StringInSlice(a string, list []string) bool { for _, b := range list { @@ -67,3 +71,8 @@ func GetHostnameWithoutDomain() string { return result } + +// BuildFileName returns a filename to store the output of support collector. +func BuildFileName() string { + return FilePrefix + "_" + GetHostnameWithoutDomain() + "_" + time.Now().Format("20060102-1504") + ".zip" +} diff --git a/main.go b/main.go index b583bff..5e7cd9a 100644 --- a/main.go +++ b/main.go @@ -3,18 +3,9 @@ package main import ( "encoding/json" "fmt" - "github.com/NETWAYS/support-collector/internal/arguments" - "github.com/NETWAYS/support-collector/internal/metrics" - "github.com/NETWAYS/support-collector/modules/redis" - flag "github.com/spf13/pflag" - "os" - "path/filepath" - "strings" - "time" - "github.com/NETWAYS/support-collector/internal/collection" - "github.com/NETWAYS/support-collector/internal/obfuscate" - "github.com/NETWAYS/support-collector/internal/util" + "github.com/NETWAYS/support-collector/internal/config" + "github.com/NETWAYS/support-collector/internal/metrics" "github.com/NETWAYS/support-collector/modules/ansible" "github.com/NETWAYS/support-collector/modules/base" "github.com/NETWAYS/support-collector/modules/corosync" @@ -23,7 +14,6 @@ import ( "github.com/NETWAYS/support-collector/modules/grafana" "github.com/NETWAYS/support-collector/modules/graphite" "github.com/NETWAYS/support-collector/modules/graylog" - "github.com/NETWAYS/support-collector/modules/icinga2" "github.com/NETWAYS/support-collector/modules/icingadb" "github.com/NETWAYS/support-collector/modules/icingadirector" "github.com/NETWAYS/support-collector/modules/icingaweb2" @@ -34,17 +24,24 @@ import ( "github.com/NETWAYS/support-collector/modules/postgresql" "github.com/NETWAYS/support-collector/modules/prometheus" "github.com/NETWAYS/support-collector/modules/puppet" + "github.com/NETWAYS/support-collector/modules/redis" "github.com/NETWAYS/support-collector/modules/webservers" + flag "github.com/spf13/pflag" + "os" + "path/filepath" + "slices" + "strings" + "time" + "github.com/NETWAYS/support-collector/internal/obfuscate" + "github.com/NETWAYS/support-collector/internal/util" + "github.com/NETWAYS/support-collector/modules/icinga2" "github.com/mattn/go-colorable" "github.com/sirupsen/logrus" ) const Product = "NETWAYS support-collector" -// FilePrefix for the outfile file. -const FilePrefix = "support-collector" - const Readme = ` The support-collector allows our customers to collect relevant information from their servers. A resulting ZIP file can then be provided to our support team @@ -60,6 +57,13 @@ WARNING: DO NOT transfer the generated file over insecure connections or by email, it contains potential sensitive information! ` +var ( + disableWizard bool + answerFile string + verbose, printVersion, detailedCollection bool + startTime = time.Now() +) + var modules = map[string]func(*collection.Collection){ "ansible": ansible.Collect, "base": base.Collect, @@ -84,138 +88,60 @@ var modules = map[string]func(*collection.Collection){ "webservers": webservers.Collect, } -var ( - moduleOrder = []string{ - "ansible", - "base", - "corosync", - "elastic", - "foreman", - "grafana", - "graphite", - "graylog", - "icinga-director", - "icinga2", - "icingadb", - "icingaweb2", - "influxdb", - "keepalived", - "mongodb", - "mysql", - "postgresql", - "prometheus", - "puppet", - "redis", - "webservers", - } -) - -var ( - verbose, printVersion, noDetailedCollection bool - enabledModules, disabledModules, extraObfuscators []string - outputFile string - commandTimeout = 60 * time.Second - startTime = time.Now() - metric *metrics.Metrics - initErrors []error -) - func init() { // Set locale to C, to avoid translations in command output _ = os.Setenv("LANG", "C") +} - args := arguments.New() - - // General arguments without interactive prompt - flag.StringArrayVar(&extraObfuscators, "hide", []string{}, "List of additional strings to obfuscate. Can be used multiple times and supports regex.") //nolint:lll - flag.BoolVar(&arguments.NonInteractive, "non-interactive", false, "Enable non-interactive mode") - flag.BoolVar(&printVersion, "version", false, "Print version and exit") - flag.BoolVar(&verbose, "verbose", false, "Enable verbose logging") - flag.DurationVar(&commandTimeout, "command-timeout", commandTimeout, "Timeout for command execution in modules") - - // Run specific arguments - args.NewPromptStringVar(&outputFile, "output", buildFileName(), "Filename for resulting zip", true, nil) - args.NewPromptStringSliceVar(&enabledModules, "enable", moduleOrder, "Enabled modules for collection (comma separated)", false, nil) - args.NewPromptStringSliceVar(&disabledModules, "disable", []string{}, "Explicit disabled modules for collection (comma separated)", false, func() bool { - if len(enabledModules) == 0 || len(enabledModules) == len(moduleOrder) { - return true - } - - return false - }) - args.NewPromptBoolVar(&noDetailedCollection, "no-details", false, "Disable detailed collection including logs and more", nil) - - // Icinga 2 specific arguments - args.NewPromptStringVar(&icinga2.APICred.Username, "icinga2-api-user", "", "Icinga 2: Username of global API user to collect data about Icinga 2 Infrastructure", false, isIcingaEnabled) - args.NewPromptStringVar(&icinga2.APICred.Password, "icinga2-api-pass", "", "Icinga 2: Password for global API user to collect data about Icinga 2 Infrastructure", false, isIcingaEnabled) - args.NewPromptStringSliceVar(&icinga2.APIEndpoints, "icinga2-api-endpoints", []string{}, "Icinga 2: Comma separated list of API Endpoints (including port) to collect data from. FQDN or IP address must be reachable. (Example: i2-master01.local:5665)", false, isIcingaEnabled) - - flag.CommandLine.SortFlags = false - - // Output a proper help message with details - flag.Usage = func() { - _, _ = fmt.Fprintf(os.Stderr, "%s\n\n%s\n\n", Product, strings.Trim(Readme, "\n")) - - _, _ = fmt.Fprintf(os.Stderr, "Usage of %s:\n", os.Args[0]) +func main() { + // Create new config object with defaults + conf := config.GetControlDefaultObject() - flag.PrintDefaults() + // Add and parse flags + if err := parseFlags(); err != nil { + logrus.Fatal(err) } - // Parse flags from command-line - flag.Parse() + // Read input from answer-file if provided + // Needs to done after parsing flags to have the value for answerFile + if answerFile != "" { + if err := config.ReadAnswerFile(answerFile, &conf); err != nil { + logrus.Fatal(err) + } - if printVersion { - fmt.Println(Product, "version", getBuildInfo()) //nolint:forbidigo - os.Exit(0) + conf.General.AnswerFile = answerFile } - // Start interactive wizard if interactive is enabled - if !arguments.NonInteractive { - initErrors = args.CollectArgsFromStdin(strings.Join(moduleOrder, ",")) + // Start interactive config wizard if not disabled via flag and no answer-file is provided + if !disableWizard && answerFile == "" { + startConfigWizard(&conf) } - // Verify enabled modules - for _, name := range enabledModules { - if _, ok := modules[name]; !ok { - logrus.Fatal("Unknown module to enable: ", name) - } + // If "all" provided for enabled modules, enable all + if slices.Contains(conf.General.EnabledModules, "all") { + conf.General.EnabledModules = config.ModulesOrder } -} -// buildFileName returns a filename to store the output of support collector. -func buildFileName() string { - return FilePrefix + "_" + util.GetHostnameWithoutDomain() + "_" + time.Now().Format("20060102-1504") + ".zip" -} - -func isIcingaEnabled() bool { - for _, name := range enabledModules { - if name == "icinga2" { - return true + // Validate conf. If errors found, print them and exit + if validationErrors := config.ValidateConfig(conf); len(validationErrors) > 0 { + for _, e := range validationErrors { + logrus.Error(e) } - } - return false -} + os.Exit(1) + } -func main() { - // Initialize new collection - c, closeCollection := NewCollection(outputFile) + // Initialize new collection with default values + c, closeCollection := NewCollection(conf) // Close collection defer closeCollection() - // Check for errors in init() - if len(initErrors) > 0 { - for _, err := range initErrors { - c.Log.Info(err) - } - } - // Initialize new metrics and defer function to save it to json - metric = metrics.New(getVersion()) + c.Metric = metrics.New(getVersion()) defer func() { // Save metrics to file - body, err := json.Marshal(metric) + body, err := json.Marshal(c.Metric) if err != nil { c.Log.Warn("cant unmarshal metrics: %w", err) } @@ -223,10 +149,15 @@ func main() { c.AddFileJSONRaw("metrics.json", body) }() - if noDetailedCollection { + c.Metric.Controls = c.Config + + // Choose whether detailed collection will be enabled or not + if !conf.General.DetailedCollection { c.Detailed = false + c.Config.General.DetailedCollection = false c.Log.Warn("Detailed collection is disabled") } else { + c.Detailed = true c.Log.Info("Detailed collection is enabled") } @@ -235,15 +166,15 @@ func main() { } // Set command Timeout from argument - c.ExecTimeout = commandTimeout + c.ExecTimeout = c.Config.General.CommandTimeout - // Collect modules + // Parse modules collectModules(c) // Save overall timing - metric.Timings["total"] = time.Since(startTime) + c.Metric.Timings["total"] = time.Since(startTime) - c.Log.Infof("Collection complete, took us %.3f seconds", metric.Timings["total"].Seconds()) + c.Log.Infof("Collection complete, took us %.3f seconds", c.Metric.Timings["total"].Seconds()) // Collect obfuscation info var ( @@ -262,7 +193,7 @@ func main() { } // get absolute path of outputFile - path, err := filepath.Abs(outputFile) + path, err := filepath.Abs(c.Config.General.OutputFile) if err != nil { c.Log.Debug(err) } @@ -270,16 +201,17 @@ func main() { c.Log.Infof("Generated ZIP file located at %s", path) } -// NewCollection starts a new collection. outputFile will be created. +// NewCollection starts a new collection based on given controls.Config // // Collection and cleanup function to defer are returned -func NewCollection(outputFile string) (*collection.Collection, func()) { - file, err := os.Create(outputFile) +func NewCollection(control config.Config) (*collection.Collection, func()) { + file, err := os.Create(control.General.OutputFile) if err != nil { - logrus.Fatal(err) + logrus.Fatalf("cant create or open file for collection for given value '%s' - %s", control.General.OutputFile, err) } c := collection.New(file) + c.Config = control consoleLevel := logrus.InfoLevel if verbose { @@ -313,13 +245,77 @@ func NewCollection(outputFile string) (*collection.Collection, func()) { } } +// parseFlags adds the default control arguments and parses them +func parseFlags() (err error) { + var generateAnswerFile bool + // General arguments without interactive prompt + flag.BoolVar(&disableWizard, "disable-wizard", false, "Disable interactive wizard for input via stdin") + flag.BoolVarP(&printVersion, "version", "v", false, "Print version and exit") + flag.BoolVarP(&verbose, "verbose", "V", false, "Enable verbose logging") + flag.BoolVar(&generateAnswerFile, "generate-answer-file", false, "Generate an example answer-file with default values") + flag.StringVarP(&answerFile, "answer-file", "f", "", "Provide an answer-file to control the collection") + + // Output a proper help message with details + flag.Usage = func() { + _, _ = fmt.Fprintf(os.Stderr, "%s\n\n%s\n\n", Product, strings.Trim(Readme, "\n")) + + _, _ = fmt.Fprintf(os.Stderr, "Usage of %s:\n", os.Args[0]) + + flag.PrintDefaults() + } + + // Parse flags from command-line + flag.Parse() + + // Print version and exit + if printVersion { + fmt.Println(Product, "version", getBuildInfo()) //nolint:forbidigo + os.Exit(0) + } + + if generateAnswerFile { + err = config.GenerateDefaultAnswerFile() + if err != nil { + return err + } + + os.Exit(0) + } + + return nil +} + +// startConfigWizard will start the stdin config wizard to provide config +func startConfigWizard(conf *config.Config) { + wizard := config.NewWizard() + + // Define arguments for interactive input via stdin + wizard.AddStringVar(&conf.General.OutputFile, "output", util.BuildFileName(), "Filename for resulting zip", true, nil) + wizard.AddStringSliceVar(&conf.General.EnabledModules, "enable", []string{"all"}, "Which modules should be enabled? (Comma separated list of modules)", false, nil) + wizard.AddBoolVar(&detailedCollection, "detailed", true, "Enable detailed collection including logs and more (recommended)?", nil) + + // Collect Icinga 2 API endpoints if module 'icinga2' is enabled + // Because we only add this when module 'icinga2' or 'all' is enabled, this needs to be after saving the enabled modules + wizard.AddIcingaEndpoints(&conf.Icinga2.Endpoints, "icinga-endpoints", "\nModule 'icinga2'is enabled.\nDo you want to collect data from Icinga 2 API endpoints?", func() bool { + if ok := slices.Contains(conf.General.EnabledModules, "all") || slices.Contains(conf.General.EnabledModules, "icinga"); ok { + return true + } + + return false + }) + + wizard.Parse(strings.Join(config.ModulesOrder, ",")) + + fmt.Printf("\nArgument wizard finished. Starting...\n\n") +} + func collectModules(c *collection.Collection) { // Check if module is enabled / disabled and call it - for _, name := range moduleOrder { + for _, name := range config.ModulesOrder { switch { - case util.StringInSlice(name, disabledModules): + case util.StringInSlice(name, c.Config.General.DisabledModules): c.Log.Debugf("Module %s is disabled", name) - case !util.StringInSlice(name, enabledModules): + case !util.StringInSlice(name, c.Config.General.EnabledModules): c.Log.Debugf("Module %s is not enabled", name) default: // Save current time @@ -328,18 +324,19 @@ func collectModules(c *collection.Collection) { c.Log.Debugf("Start collecting data for module %s", name) // Register custom obfuscators - for _, o := range extraObfuscators { + for _, o := range c.Config.General.ExtraObfuscators { c.Log.Debugf("Adding custom obfuscator for '%s' to module %s", o, name) c.RegisterObfuscator(obfuscate.NewAny(o)) } // Call collection function for module + // TODO return errors? modules[name](c) // Save runtime of module - metric.Timings[name] = time.Since(moduleStart) + c.Metric.Timings[name] = time.Since(moduleStart) - c.Log.Debugf("Finished with module %s in %.3f seconds", name, metric.Timings[name].Seconds()) + c.Log.Debugf("Finished with module %s in %.3f seconds", name, c.Metric.Timings[name].Seconds()) } } } diff --git a/modules/ansible/collector.go b/modules/ansible/collector.go index c4936b5..3fe9bbe 100644 --- a/modules/ansible/collector.go +++ b/modules/ansible/collector.go @@ -24,7 +24,7 @@ var commands = map[string][]string{ func Collect(c *collection.Collection) { if !util.ModuleExists(relevantPaths) { - c.Log.Info("Could not find ansible") + c.Log.Info("Could not find ansible. Skipping") return } diff --git a/modules/corosync/collector.go b/modules/corosync/collector.go index 1c85e23..78304b5 100644 --- a/modules/corosync/collector.go +++ b/modules/corosync/collector.go @@ -40,7 +40,7 @@ var commands = map[string][]string{ func Collect(c *collection.Collection) { if !util.ModuleExists(relevantPaths) { - c.Log.Info("Could not find corosync") + c.Log.Info("Could not find corosync. Skipping") return } diff --git a/modules/elastic/collector.go b/modules/elastic/collector.go index 884a02a..efcbf82 100644 --- a/modules/elastic/collector.go +++ b/modules/elastic/collector.go @@ -44,7 +44,7 @@ var obfuscators = []*obfuscate.Obfuscator{ func Collect(c *collection.Collection) { if !util.ModuleExists(relevantPaths) { - c.Log.Info("Could not find Elastic Stack") + c.Log.Info("Could not find elastic. Skipping") return } diff --git a/modules/foreman/collector.go b/modules/foreman/collector.go index 5ee2719..0337025 100644 --- a/modules/foreman/collector.go +++ b/modules/foreman/collector.go @@ -33,7 +33,7 @@ var obfuscaters = []*obfuscate.Obfuscator{ func Collect(c *collection.Collection) { if !util.ModuleExists(relevantPaths) { - c.Log.Info("Could not find Foreman") + c.Log.Info("Could not find foreman. Skipping") return } diff --git a/modules/grafana/collector.go b/modules/grafana/collector.go index 1442b08..14c5f4f 100644 --- a/modules/grafana/collector.go +++ b/modules/grafana/collector.go @@ -37,7 +37,7 @@ var obfuscators = []*obfuscate.Obfuscator{ func Collect(c *collection.Collection) { if !util.ModuleExists(relevantPaths) { - c.Log.Info("Could not find grafana") + c.Log.Info("Could not find grafana. Skipping") return } diff --git a/modules/graphite/collector.go b/modules/graphite/collector.go index fc787aa..ecbf559 100644 --- a/modules/graphite/collector.go +++ b/modules/graphite/collector.go @@ -48,7 +48,7 @@ var processFilter = []string{ func Collect(c *collection.Collection) { if !util.ModuleExists(relevantPaths) { - c.Log.Info("Could not find graphite") + c.Log.Info("Could not find graphite. Skipping") return } diff --git a/modules/graylog/collector.go b/modules/graylog/collector.go index e46f1e4..defeac8 100644 --- a/modules/graylog/collector.go +++ b/modules/graylog/collector.go @@ -28,7 +28,7 @@ var obfuscators = []*obfuscate.Obfuscator{ func Collect(c *collection.Collection) { if !util.ModuleExists(relevantPaths) { - c.Log.Info("Could not find Graylog") + c.Log.Info("Could not find graylog. Skipping") return } diff --git a/modules/icinga2/collector.go b/modules/icinga2/collector.go index f5f8d90..bcfa322 100644 --- a/modules/icinga2/collector.go +++ b/modules/icinga2/collector.go @@ -1,11 +1,14 @@ package icinga2 import ( + "fmt" "github.com/NETWAYS/support-collector/internal/obfuscate" "github.com/NETWAYS/support-collector/internal/util" "os" "path/filepath" "regexp" + "strings" + "time" "github.com/NETWAYS/support-collector/internal/collection" ) @@ -79,7 +82,7 @@ func Collect(c *collection.Collection) { var icinga2version string if !util.ModuleExists(relevantPaths) { - c.Log.Info("Could not find icinga2") + c.Log.Info("Could not find icinga2. Skipping") return } @@ -151,9 +154,32 @@ func Collect(c *collection.Collection) { } } - // start the collection of remote api endpoints - err = InitAPICollection(c) - if err != nil { - c.Log.Warn(err) + // Collect from API endpoints if given + if len(c.Config.Icinga2.Endpoints) > 0 { + c.Log.Debug("Start to collect data from Icinga API endpoints") + + for _, e := range c.Config.Icinga2.Endpoints { + c.Log.Debugf("New API endpoint found: '%s'. Trying...", e.Address) + + // Check if endpoint is reachable + if err := e.IsReachable(5 * time.Second); err != nil { //nolint:mnd + c.Log.Warn(err) + continue + } + + c.Log.Debug("Collect from resource 'v1/status'") + + // Request stats and health from endpoint + res, err := e.Request("v1/status", 10*time.Second) //nolint:mnd + if err != nil { + c.Log.Warn(err) + continue + } + + // Save output to file. Replace "." in address with "_" and use as filename. + c.AddFileJSON(filepath.Join(ModuleName, "api", "v1", "status", fmt.Sprintf("%s.json", strings.ReplaceAll(e.Address, ".", "_"))), res) + + c.Log.Debugf("Successfully finished endpoint '%s'", e.Address) + } } } diff --git a/modules/icinga2/icingaapi/api.go b/modules/icinga2/icingaapi/api.go new file mode 100644 index 0000000..e33e98e --- /dev/null +++ b/modules/icinga2/icingaapi/api.go @@ -0,0 +1,85 @@ +package icingaapi + +import ( + "context" + "crypto/tls" + "fmt" + "io" + "net" + "net/http" + "time" +) + +type Endpoint struct { + Address string `yaml:"address" json:"address"` + Port int `yaml:"port" json:"port"` + Username string `yaml:"username" json:"-"` + Password string `yaml:"password" json:"-"` +} + +// Returns new *http.Client with insecure TLS and Proxy from ENV +func newClient() *http.Client { + tr := &http.Transport{ + TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, //nolint:gosec + Proxy: http.ProxyFromEnvironment, + } + client := &http.Client{Transport: tr} + + return client +} + +// IsReachable checks if the endpoint is reachable within 5 sec +func (endpoint *Endpoint) IsReachable(timeout time.Duration) error { + // try to dial tcp connection within 5 seconds + conn, err := net.DialTimeout("tcp", fmt.Sprintf("%s:%d", endpoint.Address, endpoint.Port), timeout) + if err != nil { + return fmt.Errorf("cant connect to endpoint '%s' within 5 seconds: %w", endpoint.Address, err) + } + defer conn.Close() + + return nil +} + +// Request prepares a new request for the given resourcePath and executes it. +// Url for the request is build by the given resourcePath, and the Endpoint details (url => 'https://:/') +// +// A context with 10sec timeout for the request is build. BasicAuth with username and password set. +// Returns err if something went wrong. Result is given as []byte. +func (endpoint *Endpoint) Request(resourcePath string, timeout time.Duration) ([]byte, error) { + // Return err if no username or password provided + if endpoint.Username == "" || endpoint.Password == "" { + return nil, fmt.Errorf("invalid or no username or password provided for api endpoint '%s'", endpoint.Address) + } + + // Build context for the request + ctx, cancel := context.WithTimeout(context.Background(), timeout) + defer cancel() + + // Build with context and url + request, err := http.NewRequestWithContext(ctx, http.MethodGet, fmt.Sprintf("https://%s/%s", net.JoinHostPort(endpoint.Address, fmt.Sprintf("%d", endpoint.Port)), resourcePath), nil) + if err != nil { + return nil, fmt.Errorf("cant build new request for '%s': %w", endpoint.Address, err) + } + + // Set basic auth for request + request.SetBasicAuth(endpoint.Username, endpoint.Password) + + response, err := newClient().Do(request) + if err != nil { + return nil, fmt.Errorf("cant make request for '%s' to '%s': %w", endpoint.Address, resourcePath, err) + } + + defer response.Body.Close() + + body, err := io.ReadAll(response.Body) + if err != nil { + return nil, fmt.Errorf("cant read response for '%s' to '%s': %w", endpoint.Address, resourcePath, err) + } + + // if response code is not '200 OK' throw error and return + if response.Status != "200 OK" { + return nil, fmt.Errorf("request failed with status code %s: %s", response.Status, string(body)) + } + + return body, nil +} diff --git a/modules/icingadb/collector.go b/modules/icingadb/collector.go index 00489a1..e33877e 100644 --- a/modules/icingadb/collector.go +++ b/modules/icingadb/collector.go @@ -51,7 +51,7 @@ var obfuscators = []*obfuscate.Obfuscator{ func Collect(c *collection.Collection) { if !util.ModuleExists(relevantPaths) { - c.Log.Info("Could not find IcingaDB") + c.Log.Info("Could not find icingadb. Skipping") return } diff --git a/modules/icingadirector/collector.go b/modules/icingadirector/collector.go index a8aba24..7cc5db8 100644 --- a/modules/icingadirector/collector.go +++ b/modules/icingadirector/collector.go @@ -34,7 +34,7 @@ var journalctlLogs = map[string]collection.JournalElement{ // Collect data for Icinga Director. func Collect(c *collection.Collection) { if !util.ModuleExists([]string{InstallationPath}) { - c.Log.Info("Could not find Icinga Director") + c.Log.Info("Could not find icinga-director. Skipping") return } diff --git a/modules/icingaweb2/collector.go b/modules/icingaweb2/collector.go index 3e85231..72c35a8 100644 --- a/modules/icingaweb2/collector.go +++ b/modules/icingaweb2/collector.go @@ -65,7 +65,7 @@ var obfuscators = []*obfuscate.Obfuscator{ // Collect data for icingaweb2. func Collect(c *collection.Collection) { if !util.ModuleExists(relevantPaths) { - c.Log.Info("Could not find icingaweb2") + c.Log.Info("Could not find icingaweb2. Skipping") return } diff --git a/modules/influxdb/collector.go b/modules/influxdb/collector.go index 2840ccd..5c5bb0b 100644 --- a/modules/influxdb/collector.go +++ b/modules/influxdb/collector.go @@ -24,7 +24,7 @@ var detailedFiles = []string{ func Collect(c *collection.Collection) { if !util.ModuleExists(relevantPaths) { - c.Log.Info("Could not find InfluxDB") + c.Log.Info("Could not find influxdb. Skipping") return } diff --git a/modules/keepalived/collector.go b/modules/keepalived/collector.go index 32cf50b..e17d30a 100644 --- a/modules/keepalived/collector.go +++ b/modules/keepalived/collector.go @@ -37,7 +37,7 @@ var obfuscators = []*obfuscate.Obfuscator{ func Collect(c *collection.Collection) { if !util.ModuleExists(relevantPaths) { - c.Log.Info("Could not find keepalived") + c.Log.Info("Could not find keepalived. Skipping") return } diff --git a/modules/mongodb/collector.go b/modules/mongodb/collector.go index a04c269..e1d1f3c 100644 --- a/modules/mongodb/collector.go +++ b/modules/mongodb/collector.go @@ -42,7 +42,7 @@ var obfuscators = []*obfuscate.Obfuscator{ func Collect(c *collection.Collection) { if !util.ModuleExists(relevantPaths) { - c.Log.Info("Could not find mongodb") + c.Log.Info("Could not find mongodb. Skipping") return } diff --git a/modules/mysql/collector.go b/modules/mysql/collector.go index 77f075b..6a1170c 100644 --- a/modules/mysql/collector.go +++ b/modules/mysql/collector.go @@ -59,7 +59,7 @@ func getService() string { func Collect(c *collection.Collection) { service := getService() if service == "" { - c.Log.Info("Could not a running MySQL or MariaDB service") + c.Log.Info("Could not find mysql. Skipping") return } diff --git a/modules/postgresql/collector.go b/modules/postgresql/collector.go index d267bd0..0126e7f 100644 --- a/modules/postgresql/collector.go +++ b/modules/postgresql/collector.go @@ -34,7 +34,7 @@ var possibleServices = []string{ func Collect(c *collection.Collection) { if !util.ModuleExists(relevantPaths) { - c.Log.Info("Could not find PostgreSQL") + c.Log.Info("Could not find postgresql. Skipping") return } diff --git a/modules/prometheus/collector.go b/modules/prometheus/collector.go index dd0e6e6..8e03773 100644 --- a/modules/prometheus/collector.go +++ b/modules/prometheus/collector.go @@ -46,7 +46,7 @@ var obfuscators = []*obfuscate.Obfuscator{ func Collect(c *collection.Collection) { if !util.ModuleExists(relevantPaths) { - c.Log.Info("Could not find Prometheus") + c.Log.Info("Could not find prometheus. Skipping") return } diff --git a/modules/puppet/collector.go b/modules/puppet/collector.go index be15830..d8cb850 100644 --- a/modules/puppet/collector.go +++ b/modules/puppet/collector.go @@ -35,7 +35,7 @@ var commands = map[string][]string{ func Collect(c *collection.Collection) { if !util.ModuleExists(relevantPaths) { - c.Log.Info("Could not find puppet") + c.Log.Info("Could not find puppet. Skipping") return } diff --git a/modules/redis/collector.go b/modules/redis/collector.go index c01da0f..4e83982 100644 --- a/modules/redis/collector.go +++ b/modules/redis/collector.go @@ -44,7 +44,7 @@ var obfuscators = []*obfuscate.Obfuscator{ func Collect(c *collection.Collection) { if !util.ModuleExists(relevantPaths) { - c.Log.Info("Could not find redis") + c.Log.Info("Could not find redis. Skipping") return } diff --git a/modules/webservers/collector.go b/modules/webservers/collector.go index 1c6cc3f..b238643 100644 --- a/modules/webservers/collector.go +++ b/modules/webservers/collector.go @@ -46,7 +46,7 @@ var possibleDaemons = []string{ func Collect(c *collection.Collection) { if !util.ModuleExists(relevantPaths) { - c.Log.Info("Could not find webservers") + c.Log.Info("Could not find webservers. Skipping") return }