Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Collect data from the Icinga 2 API #109

Merged
merged 4 commits into from
Dec 23, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .golangci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ run:
linters:
enable-all: true
disable:
- goimports
- cyclop
- depguard
- exhaustivestruct
Expand Down
71 changes: 39 additions & 32 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,16 +27,23 @@ By default, we collect all we can find. You can control this by only enabling ce

If you want to see what is collected, add `--verbose`

| Short | Long | Description |
|:-----:|:------------------|------------------------------------------------------------------------------------------------------------------------|
| -o | --output | Output file for the zip content (default: current directory and named like '$HOSTNAME'-netways-support-$TIMESTAMP.zip) |
| | --nodetails | 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) |
| -v | --verbose | Enable verbose logging |
| -V | --version | Print version and exit |
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'


| Short | Long | Description |
|:-----:|:------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------|
| -o | --output | Output file for the zip content (default: current directory and named like '$HOSTNAME'-netways-support-$TIMESTAMP.zip) |
| | --nodetails | 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 |
tbauriedel marked this conversation as resolved.
Show resolved Hide resolved
| | --icinga2-api-pass | Password for global Icinga 2 API user to collect data about Icinga 2 Infrastructure |
| | --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) |
| -v | --verbose | Enable verbose logging |
| -V | --version | Print version and exit |

## Modules

Expand All @@ -46,28 +53,28 @@ collected.
Most modules check if the component is installed before trying to collect data. If the module is not detected, it will
not be collected.

| Module name | Description |
|----------------|------------------------------------------------------------------------------------------------------------------------|
| ansible | Configuration and packages |
| base | Basic information about the system (operating system, kernel, memory, cpu, processes, repositories, firewalls, etc.) |
| corosync | Includes corosync and pacemaker. Collects configuration, logs, packages and service status |
| elastic | Includes elasticsearch, logstash and kibana. Collects configuration, packages and service status |
| grafana | Configuration, logs, plugins, packages and service status |
| graphite | Includes graphite and carbon. Collects configuration, logs, python / pip version and list, packages and service status |
| graylog | Configuration, packages and service status |
| icinga2 | Configuration, packages, service status, logs, Icinga objects, Icinga variables, plugins and icinga-installer |
| icingadb | Includes IcingaDB and IcingaDB redis. Collects configuration, logs, packages and service status |
| icingadirector | Packages or git information, logs, Director health status and service status |
| icingaweb2 | Configuration, logs, packages, modules, PHP, modules and service status |
| influxdb | Configuration, logs, packages and service status |
| keepalived | Configuration, packages and service status |
| mongodb | Configuration, logs, packages and service status |
| mysql | Configuration, logs, packages and service status |
| postgresql | Configuration, logs, packages and service status |
| prometheus | Configuration, packages and service status |
| puppet | Configuration, logs, module list, packages and service status |
| webservers | Includes apache2, httpd and nginx. Collects configuration, logs, packages and service status |
| foreman | Configuration, logs, packages and service status |
| Module name | Description |
|----------------|----------------------------------------------------------------------------------------------------------------------------------------------------------|
| ansible | Configuration and packages |
| base | Basic information about the system (operating system, kernel, memory, cpu, processes, repositories, firewalls, etc.) |
| corosync | Includes corosync and pacemaker. Collects configuration, logs, packages and service status |
| elastic | Includes elasticsearch, logstash and kibana. Collects configuration, packages and service status |
| grafana | Configuration, logs, plugins, packages and service status |
| graphite | Includes graphite and carbon. Collects configuration, logs, python / pip version and list, packages and service status |
| graylog | Configuration, packages and service status |
| icinga2 | Configuration, packages, service status, logs, Icinga 2 objects, Icinga 2 variables, plugins, icinga-installer and data from API endpoints (if provided) |
| icingadb | Includes IcingaDB and IcingaDB redis. Collects configuration, logs, packages and service status |
| icingadirector | Packages or git information, logs, Director health status and service status |
| icingaweb2 | Configuration, logs, packages, modules, PHP, modules and service status |
| influxdb | Configuration, logs, packages and service status |
| keepalived | Configuration, packages and service status |
| mongodb | Configuration, logs, packages and service status |
| mysql | Configuration, logs, packages and service status |
| postgresql | Configuration, logs, packages and service status |
| prometheus | Configuration, packages and service status |
| puppet | Configuration, logs, module list, packages and service status |
| webservers | Includes apache2, httpd and nginx. Collects configuration, logs, packages and service status |
| foreman | Configuration, logs, packages and service status |

## Supported systems

Expand Down
17 changes: 16 additions & 1 deletion internal/collection/collection.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package collection
import (
"archive/zip"
"bytes"
"encoding/json"
"fmt"
"io"
"strings"
Expand Down Expand Up @@ -140,6 +141,20 @@ func (c *Collection) AddFileYAML(fileName string, data interface{}) {
_ = c.AddFileToOutput(file)
}

func (c *Collection) AddFileJSON(fileName string, data interface{}) {
var buf bytes.Buffer

err := json.NewEncoder(&buf).Encode(&data)
if err != nil {
c.Log.Debugf("could not encode JSON data for '%s': %s", fileName, err)
}

file := NewFile(fileName)
file.Data = buf.Bytes()

_ = c.AddFileToOutput(file)
}

func (c *Collection) AddFiles(prefix, source string) {
c.Log.Debug("Collecting files from ", source)

Expand Down Expand Up @@ -178,7 +193,7 @@ func (c *Collection) AddFilesIfFound(prefix string, sources ...string) {

func (c *Collection) AddCommandOutputWithTimeout(file string,
timeout time.Duration, command string, arguments ...string) {
c.Log.Debugf("Collecting command output: %s %s", command, strings.Join(arguments, " "))
c.Log.Debugf("Collecting command output: '%s %s'", command, strings.Join(arguments, " "))

output, err := LoadCommandOutputWithTimeout(timeout, command, arguments...)
if err != nil {
Expand Down
14 changes: 10 additions & 4 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -193,17 +193,23 @@ func main() {
}

func handleArguments() {
flag.StringVarP(&outputFile, "output", "o", buildFileName(), "Output file for the ZIP content")
// arguments for collection handling
flag.StringSliceVar(&enabledModules, "enable", moduleOrder, "List of enabled module")
flag.StringSliceVar(&disabledModules, "disable", []string{}, "List of disabled module")
flag.StringVarP(&outputFile, "output", "o", buildFileName(), "Output file for the ZIP content")
flag.BoolVar(&noDetailedCollection, "nodetails", false, "Disable detailed collection including logs and more")
flag.StringArrayVar(&extraObfuscators, "hide", []string{}, "List of additional strings to obfuscate. Can be used multiple times and supports regex.") //nolint:lll
flag.DurationVar(&commandTimeout, "command-timeout", commandTimeout, "Timeout for command execution in modules")
flag.BoolVarP(&verbose, "verbose", "v", false, "Enable verbose logging")
flag.BoolVarP(&verbose, "debug", "d", false, "Enable debug logging (use verbose)")

// api credentials for icinga 2 modules
flag.StringVar(&icinga2.APICred.Username, "icinga2-api-user", "", "Username of global Icinga 2 API user to collect data about Icinga 2 Infrastructure") //nolint:lll
flag.StringVar(&icinga2.APICred.Password, "icinga2-api-pass", "", "Password for global Icinga 2 API user to collect data about Icinga 2 Infrastructure") //nolint:lll
flag.StringSliceVar(&icinga2.APIEndpoints, "icinga2-api-endpoints", []string{}, "List of Icinga 2 API Endpoints (including port) to collect data from. FQDN or IP address must be reachable. (Example: i2-master01.local:5665)") //nolint:lll

// basic arguments
flag.BoolVarP(&printVersion, "version", "V", false, "Print version and exit")
flag.BoolVarP(&verbose, "verbose", "v", false, "Enable verbose logging")

_ = flag.CommandLine.MarkHidden("debug")
flag.CommandLine.SortFlags = false

// Output a proper help message with details
Expand Down
128 changes: 128 additions & 0 deletions modules/icinga2/api.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
package icinga2

import (
"context"
"crypto/tls"
"fmt"
"github.com/NETWAYS/support-collector/internal/collection"
"io"
"net"
"net/http"
"path/filepath"
"strings"
"time"
)

type UserAuth struct {
Username string
Password string
}

// APICred saves the user and password. Provided as arguments
var APICred UserAuth

// APIEndpoints saves the FQDN or ip address for the endpoints, that will be collected. Provided as arguments.
var APIEndpoints []string

// InitAPICollection starts to collect data from the Icinga 2 API for given endpoints
func InitAPICollection(c *collection.Collection) error {
// return if no endpoints are provided
if len(APIEndpoints) == 0 {
return fmt.Errorf("0 API endpoints provided. No data will be collected from remote targets")
}

c.Log.Info("Start collection of Icinga 2 API endpoints")

// return if username or password is not provided
if APICred.Username == "" || APICred.Password == "" {
return fmt.Errorf("API Endpoints provided but username and/or password are missing")
}

for _, endpoint := range APIEndpoints {
// check if endpoint is reachable
err := endpointIsReachable(endpoint)
if err != nil {
c.Log.Warn(err)
continue
}

c.Log.Debugf("Endpoint '%s' is reachable", endpoint)

// collect /v1/status from endpoint
err = collectStatus(endpoint, c)
if err != nil {
c.Log.Warn(err)
}
}

return nil
}

// endpointIsReachable checks if the given endpoint is reachable within 5 sec
func endpointIsReachable(endpoint string) error {
timeout := 5 * time.Second

// try to dial tcp connection within 5 seconds
conn, err := net.DialTimeout("tcp", endpoint, timeout)
if err != nil {
return fmt.Errorf("cant connect to endpoint '%s' within 5 seconds: %w", endpoint, err)
}
defer conn.Close()

return nil
}

// collectStatus requests $endpoint$/v1/status with APICred and saves the json result to file
func collectStatus(endpoint string, c *collection.Collection) error {
c.Log.Debugf("request data from endpoint '%s/v1/status'", endpoint)

// allow insecure connections because of Icinga 2 certificates and add proxy if one is defined in the environments
tr := &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, //nolint:gosec
Proxy: http.ProxyFromEnvironment,
}
client := &http.Client{Transport: tr}

// build context for request
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()

// build request
req, err := http.NewRequestWithContext(ctx, http.MethodGet, fmt.Sprintf("https://%s/v1/status", endpoint), nil)
if err != nil {
return fmt.Errorf("cant build new request for '%s': %w", endpoint, err)
}

// set authentication for request
req.SetBasicAuth(APICred.Username, APICred.Password)

// make request
resp, err := client.Do(req)
if err != nil {
return fmt.Errorf("cant requests status from '%s': %w", endpoint, err)
}

defer resp.Body.Close()

body, err := io.ReadAll(resp.Body)
if err != nil {
return fmt.Errorf("cant read from response: %w", err)
}

// if response code is not '200 OK' throw error and return
if resp.Status != "200 OK" {
return fmt.Errorf("request failed with status code %s: %s", resp.Status, string(body))
}

// add body to file
c.AddFileJSON(filepath.Join(ModuleName, fmt.Sprintf("api-v1_status_%s.json", extractHostname(endpoint))), string(body))

return nil
}

// extractsHostname takes the endpoint and extract the hostname of it
func extractHostname(endpoint string) string {
splits := strings.Split(endpoint, ":")

return splits[0]
}
6 changes: 6 additions & 0 deletions modules/icinga2/collector.go
Original file line number Diff line number Diff line change
Expand Up @@ -143,4 +143,10 @@ func Collect(c *collection.Collection) {
c.AddCommandOutput(filepath.Join(ModuleName, name), cmd[0], cmd[1:]...)
}
}

// start the collection of remote api endpoints
err = InitAPICollection(c)
if err != nil {
c.Log.Warn(err)
}
}
Loading