diff --git a/.golangci.yml b/.golangci.yml
index 6f5d185..62aa991 100644
--- a/.golangci.yml
+++ b/.golangci.yml
@@ -1,5 +1,7 @@
run:
- deadline: 1m
+ timeout: 5m
+ skip-files:
+ - '(.+)_test\.go'
linters:
disable-all: false
diff --git a/README.md b/README.md
index 3681cfc..4b9da5d 100644
--- a/README.md
+++ b/README.md
@@ -1,10 +1,34 @@
# check_cloud_aws
-Icinga check plugin to check Amazon AWS resources. At the moment the check supports EC2 instances, Cloudfront and S3
-Buckets.
+An Icinga check plugin to check Amazon AWS resources.
## Usage
+### Health Status
+
+A general status based on the RSS feed on the AWS Health Page
+
+```
+Usage:
+ check_cloud_aws status [flags]
+
+Flags:
+ -u, --url string The AWS Status Page URL (default "https://status.aws.amazon.com")
+ -s, --service string The AWS Service to check (default "ec2")
+ -h, --help help for status
+```
+
+```
+check_cloud_aws status --service cloudfront
+OK - Service cloudfront is operating normally
+
+check_cloud_aws --region us-west-1 status --service cloudwatch
+WARNING - Information available for cloudwatch in us-west-1
+
+check_cloud_aws --region eu-west-1 status
+CRITICAL - Service disruption for ec2 in eu-west-1
+```
+
### EC2 - Instances
When one of the states is non-ok, or a machine is stopped, the check will alert.
@@ -152,7 +176,7 @@ Global Flags:
````
````
-$ check_cloud_aws cloudfront
+$ check_cloud_aws cloudfront
CRITICAL - Found 2 Distributions - critical 1 - warning 1
[WARNING] E32127W2BLH4SR status=InProgress enabled=true
diff --git a/cmd/root.go b/cmd/root.go
index e92d101..6be3d0d 100644
--- a/cmd/root.go
+++ b/cmd/root.go
@@ -15,7 +15,7 @@ var (
var rootCmd = &cobra.Command{
Use: "check_cloud_aws",
- Short: "Icinga check plugin to check Amazon EC2 instances",
+ Short: "An Icinga check plugin to check Amazon Web Services",
PersistentPreRun: func(cmd *cobra.Command, args []string) {
go check.HandleTimeout(Timeout)
},
diff --git a/cmd/status.go b/cmd/status.go
new file mode 100644
index 0000000..41ee3a6
--- /dev/null
+++ b/cmd/status.go
@@ -0,0 +1,183 @@
+package cmd
+
+import (
+ "encoding/xml"
+ "fmt"
+ "github.com/NETWAYS/check_cloud_aws/internal/status"
+ "github.com/NETWAYS/go-check"
+ "github.com/spf13/cobra"
+ "net/http"
+ "net/url"
+ "strings"
+)
+
+// To store the CLI parameters
+type StatusConfig struct {
+ Url string
+ Service string
+}
+
+var cliStatusConfig StatusConfig
+
+func contains(s string, list []string) bool {
+ // Tiny helper to see if a string is in a list of strings
+ for _, elem := range list {
+ if s == elem {
+ return true
+ }
+ }
+
+ return false
+}
+
+var statusCmd = &cobra.Command{
+ Use: "status",
+ Short: "Checks the status of AWS services",
+ Example: `
+ check_cloud_aws status --service cloudfront
+ OK - Service cloudfront is operating normally
+
+ check_cloud_aws --region us-west-1 status --service cloudwatch
+ WARNING - Information available for cloudwatch in us-west-1
+
+ check_cloud_aws --region eu-west-1 status
+ CRITICAL - Service disruption for ec2 in eu-west-1
+
+ check_cloud_aws --region "" status iam
+ CRITICAL - WARNING - Information available for iam (Global)`,
+ Run: func(cmd *cobra.Command, args []string) {
+ var (
+ feed status.Rss
+ rc int
+ output string
+ feedUrl string
+ )
+
+ // These services don't require a region
+ // Hint: This list might not be extensive
+ var globalServices = []string{"route53",
+ "route53domainregistration",
+ "route53apprecoverycontroller",
+ "chime",
+ "health",
+ "import-export",
+ "iam",
+ "awsiotdevicemanagement",
+ "marketplace",
+ "apipricing",
+ "awswaf",
+ "trustedadvisor",
+ "supportcenter",
+ "resourcegroups",
+ "organizations",
+ "management-console",
+ "awsiotdevicemanagement",
+ "account",
+ "interregionvpcpeering",
+ "cloudfront",
+ "billingconsole",
+ "chatbot"}
+
+ // Creating an client and connecting to the RSS Feed
+ // Access the AWS Health Dashboard at health.aws.amazon.com directly,
+ // since the AWS Go SDK just supports Personal Dashboards
+ c := &http.Client{}
+
+ if Region == "" && !contains(cliStatusConfig.Service, globalServices) {
+ check.ExitError(fmt.Errorf("Region required for regional services"))
+ }
+
+ // Using + concatenation since the JoinPath will add / inbetween
+ if Region == "" && contains(cliStatusConfig.Service, globalServices) {
+ feedUrl, _ = url.JoinPath(cliStatusConfig.Url, "/rss/", cliStatusConfig.Service+".rss")
+ // Just for the output later
+ Region = "Global"
+ } else {
+ feedUrl, _ = url.JoinPath(cliStatusConfig.Url, "/rss/", cliStatusConfig.Service+"-"+Region+".rss")
+ }
+
+ resp, err := c.Get(feedUrl)
+
+ if err != nil {
+ check.ExitError(err)
+ }
+
+ if resp.StatusCode != http.StatusOK {
+ check.ExitError(fmt.Errorf("Could not get %s - Error: %d", feedUrl, resp.StatusCode))
+ }
+
+ defer resp.Body.Close()
+ err = xml.NewDecoder(resp.Body).Decode(&feed)
+
+ if err != nil {
+ check.ExitError(err)
+ }
+
+ // Exit if there are no events
+ if len(feed.Channel.Items) == 0 {
+ rc = check.OK
+ output = fmt.Sprintf("No events for %s (%s)", cliStatusConfig.Service, Region)
+ check.ExitRaw(rc, output)
+ }
+
+ rc = check.Unknown
+ output = "Status unknown"
+
+ // Get the latest event
+ item := strings.Split(feed.Channel.Items[0].Title, ":")
+
+ // If we couldn't split the title
+ if len(item) < 2 {
+ output = "Could not determine status."
+ check.ExitRaw(rc, output, feed.Channel.Items[0].Title)
+ }
+
+ event := item[0]
+ details := item[1]
+
+ if strings.Contains(event, "Service disruption") {
+ // Service disruptions are Critical
+ rc = check.Critical
+ output = fmt.Sprintf("Service disruption for %s (%s)", cliStatusConfig.Service, Region)
+ }
+
+ if strings.Contains(event, "Performance issue") {
+ // Performance issues are Warnings
+ rc = check.Warning
+ output = fmt.Sprintf("Performance issues for %s (%s)", cliStatusConfig.Service, Region)
+ }
+
+ if strings.Contains(event, "Informational message") {
+ // There will be no new item if an information is resolved,
+ // we need to check if the info is resolved.
+ if strings.Contains(details, "[RESOLVED]") {
+ rc = check.OK
+ output = fmt.Sprintf("Event resolved for %s (%s)", cliStatusConfig.Service, Region)
+ } else {
+ // An information that should be checked by someone
+ rc = check.Warning
+ output = fmt.Sprintf("Information available for %s (%s)", cliStatusConfig.Service, Region)
+ }
+ }
+
+ if strings.Contains(event, "Service is operating normally") {
+ rc = check.OK
+ output = fmt.Sprintf("Service %s is operating normally (%s)", cliStatusConfig.Service, Region)
+ }
+
+ check.ExitRaw(rc, output)
+ },
+}
+
+func init() {
+ rootCmd.AddCommand(statusCmd)
+
+ fs := statusCmd.Flags()
+
+ fs.StringVarP(&cliStatusConfig.Url, "url", "u", "https://status.aws.amazon.com",
+ "The AWS Status Page URL")
+ fs.StringVarP(&cliStatusConfig.Service, "service", "s", "ec2",
+ "The AWS Service to check")
+
+ fs.SortFlags = false
+}
diff --git a/cmd/status_test.go b/cmd/status_test.go
new file mode 100644
index 0000000..944fd53
--- /dev/null
+++ b/cmd/status_test.go
@@ -0,0 +1,255 @@
+package cmd
+
+import (
+ "net/http"
+ "net/http/httptest"
+ "os/exec"
+ "strings"
+ "testing"
+)
+
+func TestStatus_ConnectionRefused(t *testing.T) {
+
+ cmd := exec.Command("go", "run", "../main.go", "status", "--region", "foobar", "--url", "https://localhost")
+ out, _ := cmd.CombinedOutput()
+
+ actual := string(out)
+ expected := "UNKNOWN - Get \"https://localhost"
+
+ if !strings.Contains(actual, expected) {
+ t.Error("\nActual: ", actual, "\nExpected: ", expected)
+ }
+}
+
+type StatusTest struct {
+ name string
+ server *httptest.Server
+ args []string
+ expected string
+}
+
+func TestStatusCmd(t *testing.T) {
+ tests := []StatusTest{
+ {
+ name: "status-ok",
+ server: httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ w.WriteHeader(http.StatusOK)
+ w.Write([]byte(`
+
+
+
+ http://httptest.localhost/
+ en-us
+ Mon, 02 Jan 2023 00:05:49 PST
+ AWS Service Health Dashboard RSS Generator
+
+ 5
+ -
+
+
+
+`))
+ })),
+ args: []string{"run", "../main.go", "status", "--region", "eu-foobar-1"},
+ expected: "UNKNOWN - Could not determine status. Nothing so split into Slice",
+ },
+ {
+ name: "status-ok",
+ server: httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ w.WriteHeader(http.StatusOK)
+ w.Write([]byte(`
+
+
+
+ http://httptest.localhost/
+ en-us
+ Mon, 02 Jan 2023 00:05:49 PST
+ AWS Service Health Dashboard RSS Generator
+
+ 5
+ -
+
+ http://httptest.localhost/
+ Fri, 24 Jul 2015 11:54:38 PDT
+ http://httptest.localhost/1234
+
+ -
+
+ http://httptest.localhost/
+ Fri, 20 Jul 2015 11:54:38 PDT
+ http://httptest.localhost/1234
+
+
+`))
+ })),
+ args: []string{"run", "../main.go", "status", "--region", "eu-foobar-1"},
+ expected: "OK - Event resolved for ec2 (eu-foobar-1)",
+ },
+ {
+ name: "status-ok-no-items",
+ server: httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ w.WriteHeader(http.StatusOK)
+ w.Write([]byte(`
+
+
+
+ http://httptest.localhost/
+ en-us
+ Mon, 02 Jan 2023 00:05:49 PST
+ AWS Service Health Dashboard RSS Generator
+
+ 5
+
+`))
+ })),
+ args: []string{"run", "../main.go", "status", "--region", "eu-foobar-1"},
+ expected: "OK - No events for ec2 (eu-foobar-1)",
+ },
+ {
+ name: "status-ok-normal",
+ server: httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ w.WriteHeader(http.StatusOK)
+ w.Write([]byte(`
+
+
+
+ http://httptest.localhost/
+ en-us
+ Mon, 02 Jan 2023 00:05:49 PST
+ AWS Service Health Dashboard RSS Generator
+
+ 5
+ -
+
+ http://httptest.localhost/
+ Fri, 24 Jul 2015 11:54:38 PDT
+ http://httptest.localhost/1234
+
+
+`))
+ })),
+ args: []string{"run", "../main.go", "status", "--region", "eu-foobar-1"},
+ expected: "OK - Service ec2 is operating normally (eu-foobar-1)",
+ },
+ {
+ name: "status-warning",
+ server: httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ w.WriteHeader(http.StatusOK)
+ w.Write([]byte(`
+
+
+
+ http://httptest.localhost/
+ en-us
+ Mon, 02 Jan 2023 00:05:49 PST
+ AWS Service Health Dashboard RSS Generator
+
+ 5
+ -
+
+ http://httptest.localhost/
+ Fri, 24 Jul 2015 11:54:38 PDT
+ http://httptest.localhost/1234
+
+
+`))
+ })),
+ args: []string{"run", "../main.go", "status", "--region", "eu-foobar-1"},
+ expected: "WARNING - Performance issues for ec2 (eu-foobar-1)",
+ },
+ {
+ name: "status-critical",
+ server: httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ w.WriteHeader(http.StatusOK)
+ w.Write([]byte(`
+
+
+
+ http://httptest.localhost/
+ en-us
+ Mon, 02 Jan 2023 00:05:49 PST
+ AWS Service Health Dashboard RSS Generator
+
+ 5
+ -
+
+ http://httptest.localhost/
+ Fri, 24 Jul 2015 11:54:38 PDT
+ http://httptest.localhost/1234
+
+
+`))
+ })),
+ args: []string{"run", "../main.go", "status", "--region", "eu-foobar-1"},
+ expected: "CRITICAL - Service disruption for ec2 (eu-foobar-1)",
+ },
+ {
+ name: "status-informational",
+ server: httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ w.WriteHeader(http.StatusOK)
+ w.Write([]byte(`
+
+
+
+ http://httptest.localhost/
+ en-us
+ Mon, 02 Jan 2023 00:05:49 PST
+ AWS Service Health Dashboard RSS Generator
+
+ 5
+ -
+
+ http://httptest.localhost/
+ Fri, 24 Jul 2015 11:54:38 PDT
+ http://httptest.localhost/1234
+
+
+`))
+ })),
+ args: []string{"run", "../main.go", "status", "--region", "eu-foobar-1"},
+ expected: "WARNING - Information available for ec2 (eu-foobar-1)",
+ },
+ {
+ name: "status-global-service",
+ server: httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ w.WriteHeader(http.StatusOK)
+ w.Write([]byte(`
+
+
+
+ http://httptest.localhost/
+ en-us
+ Mon, 02 Jan 2023 00:05:49 PST
+ AWS Service Health Dashboard RSS Generator
+
+ 5
+ -
+
+ http://httptest.localhost/
+ Fri, 24 Jul 2015 11:54:38 PDT
+ http://httptest.localhost/1234
+
+
+`))
+ })),
+ args: []string{"run", "../main.go", "status", "-s", "iam", "--region", ""},
+ expected: "WARNING - Information available for iam (Global)",
+ },
+ }
+
+ for _, test := range tests {
+ t.Run(test.name, func(t *testing.T) {
+ defer test.server.Close()
+
+ cmd := exec.Command("go", append(test.args, "--url", test.server.URL)...)
+ out, _ := cmd.CombinedOutput()
+
+ actual := string(out)
+
+ if !strings.Contains(actual, test.expected) {
+ t.Error("\nActual: ", actual, "\nExpected: ", test.expected)
+ }
+
+ })
+ }
+}
diff --git a/internal/status/rss.go b/internal/status/rss.go
new file mode 100644
index 0000000..3734e2d
--- /dev/null
+++ b/internal/status/rss.go
@@ -0,0 +1,14 @@
+package status
+
+type Rss struct {
+ Channel struct {
+ Title string `xml:"title"`
+ Link string `xml:"link"`
+ Desc string `xml:"description"`
+ Items []Item `xml:"item"`
+ } `xml:"channel"`
+}
+
+type Item struct {
+ Title string `xml:"title"`
+}