diff --git a/CHANGELOG.md b/CHANGELOG.md index c7f676c..64d5885 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed - More detailed errors for Redmine import +### Added +- XMPP output support + ## [0.0.1] - 2019-05-13 ### Added - Import user mails for project members from Redmine diff --git a/Gopkg.lock b/Gopkg.lock index f78f1a0..7b7707e 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -137,6 +137,14 @@ pruneopts = "T" revision = "2267b9239bac0bb8a7123648f080f264fc5b3973" +[[projects]] + branch = "master" + digest = "1:aa1a2b0a8403f464aef17453dfebd0fa9edd2e99047c2ff27e29bdb2b58e97f7" + name = "github.com/mattn/go-xmpp" + packages = ["."] + pruneopts = "T" + revision = "6093f50721ed2204a87a81109ca5a466a5bec6c1" + [[projects]] digest = "1:53bc4cd4914cd7cd52139990d5170d6dc99067ae31c56530621b18b35fc30318" name = "github.com/mitchellh/mapstructure" @@ -364,6 +372,7 @@ "github.com/bluele/slack", "github.com/go-redis/redis", "github.com/mattn/go-redmine", + "github.com/mattn/go-xmpp", "github.com/sensu/sensu-go/types", "github.com/spf13/cobra", "github.com/spf13/viper", diff --git a/Gopkg.toml b/Gopkg.toml index 05bc127..8880586 100644 --- a/Gopkg.toml +++ b/Gopkg.toml @@ -24,3 +24,7 @@ [[constraint]] branch = "master" name = "github.com/bluele/slack" + +[[constraint]] + branch = "master" + name = "github.com/mattn/go-xmpp" diff --git a/cmd/event.go b/cmd/event.go index 0ebc35c..d226a51 100644 --- a/cmd/event.go +++ b/cmd/event.go @@ -81,6 +81,21 @@ func init() { "http://s3-us-west-2.amazonaws.com/sensuapp.org/sensu.png", "A URL to an image to use as the user avatar") + eventCmd.PersistentFlags().String( + "xmpp-server", + "", + "The XMPP server to send messages to") + + eventCmd.PersistentFlags().String( + "xmpp-username", + "", + "The XMPP username used to send messages") + + eventCmd.PersistentFlags().String( + "xmpp-password", + "", + "The XMPP password used for authentication") + _ = viper.BindPFlag("outputs", eventCmd.PersistentFlags().Lookup("outputs")) _ = viper.BindPFlag("annotation-prefix", eventCmd.PersistentFlags().Lookup("annotation-prefix")) _ = viper.BindPFlag("smtp-address", eventCmd.PersistentFlags().Lookup("smtp-address")) @@ -88,6 +103,9 @@ func init() { _ = viper.BindPFlag("slack-webhook-url", eventCmd.PersistentFlags().Lookup("slack-webhook-url")) _ = viper.BindPFlag("slack-username", eventCmd.PersistentFlags().Lookup("slack-username")) _ = viper.BindPFlag("slack-icon-url", eventCmd.PersistentFlags().Lookup("slack-icon-url")) + _ = viper.BindPFlag("xmpp-server", eventCmd.PersistentFlags().Lookup("xmpp-server")) + _ = viper.BindPFlag("xmpp-username", eventCmd.PersistentFlags().Lookup("xmpp-username")) + _ = viper.BindPFlag("xmpp-password", eventCmd.PersistentFlags().Lookup("xmpp-password")) } func loadEvent() (*types.Event, error) { @@ -142,6 +160,9 @@ func handleEvent(event *types.Event) error { SlackWebhookURL: viper.GetString("slack-webhook-url"), SlackUsername: viper.GetString("slack-username"), SlackIconURL: viper.GetString("slack-icon-url"), + XMPPServer: viper.GetString("xmpp-server"), + XMPPUsername: viper.GetString("xmpp-username"), + XMPPPassword: viper.GetString("xmpp-password"), } recipients := recipient.Parse(redisClient, val) diff --git a/output/helpers.go b/output/helpers.go index 3d7a246..8014ad0 100644 --- a/output/helpers.go +++ b/output/helpers.go @@ -11,7 +11,19 @@ import ( sensu "github.com/sensu/sensu-go/types" ) -func resolveTemplate(templateValue string, event *sensu.Event) (string, error) { +func extendedEventFromEvent(event *sensu.Event) *ExtendedEvent { + return &ExtendedEvent{ + Event: event, + Status: messageEventStatus(event), + EventAction: formattedEventAction(event), + EventKey: eventKey(event), + Output: formattedEventOutput(event, 100), + FullOutput: event.Check.Output, + FormattedMessage: formattedMessage(event), + } +} + +func resolveTemplate(templateValue string, event *ExtendedEvent) (string, error) { var resolved bytes.Buffer tmpl, err := template.New("tmpl").Parse(templateValue) @@ -27,11 +39,7 @@ func resolveTemplate(templateValue string, event *sensu.Event) (string, error) { return resolved.String(), nil } -func chomp(s string) string { - return strings.Trim(strings.Trim(strings.Trim(s, "\n"), "\r"), "\r\n") -} - -func messageStatus(event *sensu.Event) string { +func messageEventStatus(event *sensu.Event) string { switch event.Check.Status { case 0: return "Resolved" @@ -51,20 +59,20 @@ func formattedEventAction(event *sensu.Event) string { } } -func eventKey(event *sensu.Event) string { - return fmt.Sprintf("%s/%s", event.Entity.Name, event.Check.Name) -} - -func eventSummary(event *sensu.Event, maxLength int) string { - output := chomp(event.Check.Output) +func formattedEventOutput(event *sensu.Event, maxLength int) string { + output := strings.Trim(strings.Trim(strings.Trim(event.Check.Output, "\n"), "\r"), "\r\n") if len(event.Check.Output) > maxLength { output = output[0:maxLength] + "..." } - return fmt.Sprintf("%s:%s", eventKey(event), output) + return output +} + +func eventKey(event *sensu.Event) string { + return fmt.Sprintf("%s/%s", event.Entity.Name, event.Check.Name) } func formattedMessage(event *sensu.Event) string { - return fmt.Sprintf("%s - %s", formattedEventAction(event), eventSummary(event, 100)) + return fmt.Sprintf("[%s] %s - %s", formattedEventAction(event), eventKey(event), formattedEventOutput(event, 100)) } diff --git a/output/mail.go b/output/mail.go index e891989..71d7c23 100644 --- a/output/mail.go +++ b/output/mail.go @@ -9,18 +9,16 @@ import ( "net/mail" "net/smtp" - sensu "github.com/sensu/sensu-go/types" - "sensu-sic-handler/recipient" ) var ( - mailSubjectTemplate = "[Sensu] {{.Entity.Name}}/{{.Check.Name}}: {{.Check.State}}" - mailBodyTemplate = "{{.Check.Output}}" + mailSubjectTemplate = "[Sensu] [{{ .EventAction }}] /{{ .EventKey }}: {{ .Status }}" + mailBodyTemplate = "{{ .FullOutput }}" ) // Mail handles mail recipients (recipient.HandlerTypeMail) -func Mail(recipient *recipient.Recipient, event *sensu.Event, config *Config) (rerr error) { +func Mail(recipient *recipient.Recipient, event *ExtendedEvent, config *Config) (rerr error) { if len(config.MailFrom) == 0 { return errors.New("from email is empty") } diff --git a/output/output.go b/output/output.go index 94fd1e4..a5b0bf4 100644 --- a/output/output.go +++ b/output/output.go @@ -15,6 +15,8 @@ import ( func Notify(recipients []*recipient.Recipient, event *sensu.Event, config *Config) error { recipientMap := make(map[string]bool) + extendedEvent := extendedEventFromEvent(event) + for _, rcpt := range recipients { if _, ok := recipientMap[rcpt.ID]; !ok { var err error @@ -22,11 +24,11 @@ func Notify(recipients []*recipient.Recipient, event *sensu.Event, config *Confi switch rcpt.Type { case recipient.OutputTypeNone: case recipient.OutputTypeMail: - err = Mail(rcpt, event, config) + err = Mail(rcpt, extendedEvent, config) case recipient.OutputTypeXMPP: - err = XMPP(rcpt, event, config) + err = XMPP(rcpt, extendedEvent, config) case recipient.OutputTypeSlack: - err = Slack(rcpt, event, config) + err = Slack(rcpt, extendedEvent, config) default: fmt.Fprintln(os.Stderr, fmt.Sprintf("unsupported handler: %q", rcpt)) } diff --git a/output/slack.go b/output/slack.go index 1d4745f..840ebb1 100644 --- a/output/slack.go +++ b/output/slack.go @@ -7,13 +7,12 @@ import ( "errors" "github.com/bluele/slack" - sensu "github.com/sensu/sensu-go/types" "sensu-sic-handler/recipient" ) // Slack handles slack recipients (recipient.HandlerTypeSlack) -func Slack(recipient *recipient.Recipient, event *sensu.Event, config *Config) error { +func Slack(recipient *recipient.Recipient, event *ExtendedEvent, config *Config) error { if len(config.SlackWebhookURL) == 0 { return errors.New("webhook url is empty") } @@ -28,8 +27,8 @@ func Slack(recipient *recipient.Recipient, event *sensu.Event, config *Config) e }) } -func slackMessageColor(event *sensu.Event) string { - switch event.Check.Status { +func slackMessageColor(event *ExtendedEvent) string { + switch event.Event.Check.Status { case 0: return "good" case 2: @@ -39,26 +38,26 @@ func slackMessageColor(event *sensu.Event) string { } } -func slackMessageAttachment(event *sensu.Event) *slack.Attachment { +func slackMessageAttachment(event *ExtendedEvent) *slack.Attachment { return &slack.Attachment{ Title: "Description", - Text: event.Check.Output, - Fallback: formattedMessage(event), + Text: event.Event.Check.Output, + Fallback: event.FormattedMessage, Color: slackMessageColor(event), Fields: []*slack.AttachmentField{ { Title: "Status", - Value: messageStatus(event), + Value: messageEventStatus(event.Event), Short: false, }, { Title: "Entity", - Value: event.Entity.Name, + Value: event.Event.Entity.Name, Short: true, }, { Title: "Check", - Value: event.Check.Name, + Value: event.Event.Check.Name, Short: true, }, }, diff --git a/output/types.go b/output/types.go index 972e7b8..47cabd3 100644 --- a/output/types.go +++ b/output/types.go @@ -2,6 +2,10 @@ package output +import ( + sensu "github.com/sensu/sensu-go/types" +) + // Config configuration for handlers type Config struct { SMTPAddress string @@ -9,4 +13,18 @@ type Config struct { SlackWebhookURL string SlackUsername string SlackIconURL string + XMPPServer string + XMPPUsername string + XMPPPassword string +} + +// ExtendedEvent is a helper type for template resolution +type ExtendedEvent struct { + Event *sensu.Event + Status string + EventAction string + EventKey string + Output string + FullOutput string + FormattedMessage string } diff --git a/output/xmpp.go b/output/xmpp.go index 87d51a4..e021441 100644 --- a/output/xmpp.go +++ b/output/xmpp.go @@ -3,12 +3,85 @@ package output import ( - sensu "github.com/sensu/sensu-go/types" + "crypto/tls" + "errors" + + "github.com/mattn/go-xmpp" "sensu-sic-handler/recipient" ) +var xmppMessageTemplate = "{{ .FormattedMessage }}" + // XMPP handles XMPP recipients (recipient.HandlerTypeXMPP) -func XMPP(recipient *recipient.Recipient, event *sensu.Event, config *Config) error { +func XMPP(recipient *recipient.Recipient, event *ExtendedEvent, config *Config) (rerr error) { + if len(config.XMPPServer) == 0 { + return errors.New("hostname is empty") + } + + if len(config.XMPPUsername) == 0 { + return errors.New("username is empty") + } + + if len(config.XMPPPassword) == 0 { + return errors.New("password is empty") + } + + xmpp.DefaultConfig = tls.Config{ + //ServerName: config.XMPPServer, + //InsecureSkipVerify: false, + } + + clientOptions := xmpp.Options{ + Host: config.XMPPServer, + User: config.XMPPUsername, + Password: config.XMPPPassword, + NoTLS: true, + } + + client, err := clientOptions.NewClient() + if err != nil { + return err + } + + msg, err := resolveTemplate(xmppMessageTemplate, event) + if err != nil { + return err + } + + switch recipient.Args["type"] { + case "user": + err = xmppSendUser(client, recipient.Args["user"], msg) + case "muc": + err = xmppSendMUC(client, recipient.Args["room"], msg) + } + + if err != nil { + return err + } + + return nil +} + +func xmppSendUser(client *xmpp.Client, remote string, msg string) error { + _, err := client.Send(xmpp.Chat{Remote: remote, Type: "chat", Text: msg}) + if err != nil { + return err + } + + return nil +} + +func xmppSendMUC(client *xmpp.Client, remote string, msg string) error { + _, err := client.JoinMUCNoHistory(remote, "sensu") + if err != nil { + return err + } + + _, err = client.Send(xmpp.Chat{Remote: remote, Type: "groupchat", Text: msg}) + if err != nil { + return err + } + return nil } diff --git a/redmine/import.go b/redmine/import.go index b860661..18387d9 100644 --- a/redmine/import.go +++ b/redmine/import.go @@ -7,7 +7,7 @@ import ( "fmt" "github.com/go-redis/redis" - redmine "github.com/mattn/go-redmine" + "github.com/mattn/go-redmine" ) // Import imports memberships and users into redis diff --git a/redmine/types.go b/redmine/types.go index 4a9a6cc..93d35e6 100644 --- a/redmine/types.go +++ b/redmine/types.go @@ -3,7 +3,7 @@ package redmine import ( - redmine "github.com/mattn/go-redmine" + "github.com/mattn/go-redmine" ) type projectMemberships struct {