From 2f73907005351e11e79e9f466736cb8405c45950 Mon Sep 17 00:00:00 2001 From: Christian Pointner Date: Tue, 17 Oct 2023 00:21:48 +0200 Subject: [PATCH] notifier: start to implemtn sms-modem and email backends --- cmd/whawty-alerts/config.go | 2 +- contrib/sample-cfg.yml | 12 ++++ go.mod | 3 + go.sum | 9 +++ notifier/backend_email.go | 69 +++++++++++++++++++ notifier/backend_smsmodem.go | 129 +++++++++++++++++++++++++++++++++++ notifier/notifier.go | 54 +++++++++++++-- notifier/types.go | 52 ++++++++++++-- 8 files changed, 318 insertions(+), 12 deletions(-) create mode 100644 notifier/backend_email.go create mode 100644 notifier/backend_smsmodem.go diff --git a/cmd/whawty-alerts/config.go b/cmd/whawty-alerts/config.go index 25a498d..70094c6 100644 --- a/cmd/whawty-alerts/config.go +++ b/cmd/whawty-alerts/config.go @@ -46,7 +46,7 @@ type WebConfig struct { type Config struct { Store store.Config `yaml:"store"` - Notifier notifier.Config `yaml:"notifer"` + Notifier notifier.Config `yaml:"notifier"` Web WebConfig `yaml:"web"` } diff --git a/contrib/sample-cfg.yml b/contrib/sample-cfg.yml index 83b33d0..0bca1af 100644 --- a/contrib/sample-cfg.yml +++ b/contrib/sample-cfg.yml @@ -1,2 +1,14 @@ store: path: ./contrib/test.db +notifier: + backends: + - name: mail-foo + email: + from: noreply@example.com + smarthost: mailrelay.example.com + - name: sms-bar + smsModem: + device: /dev/ttyUSB0 + baudrate: 115200 + timeout: 10s + pin: 1234 diff --git a/go.mod b/go.mod index c21518e..c873035 100644 --- a/go.mod +++ b/go.mod @@ -62,9 +62,12 @@ require ( github.com/shurcooL/httpfs v0.0.0-20230704072500-f1e31cf0ba5c // indirect github.com/shurcooL/vfsgen v0.0.0-20230704071429-0000e147ea92 // indirect github.com/spreadspace/tlsconfig v0.0.0-20230726215100-56bbcafa5d60 // indirect + github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/ugorji/go/codec v1.2.11 // indirect github.com/urfave/cli v1.22.14 // indirect + github.com/warthog618/modem v0.4.0 // indirect + github.com/warthog618/sms v0.3.0 // indirect go.etcd.io/bbolt v1.3.7 // indirect golang.org/x/arch v0.3.0 // indirect golang.org/x/crypto v0.9.0 // indirect diff --git a/go.sum b/go.sum index 952f91e..28d50ec 100644 --- a/go.sum +++ b/go.sum @@ -246,6 +246,7 @@ github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjY github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f h1:KUppIJq7/+SVif2QVs3tOP0zanoHgBEVAwHxUSIzRqU= github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/oklog/run v1.1.0 h1:GEenZ1cK0+q0+wsJew9qUg/DyD8k3JzYsZAi5gYi2mA= github.com/oklog/run v1.1.0/go.mod h1:sVPdnTZT1zYwAJeCMu2Th4T21pA3FPOQRfWjQlk7DVU= github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4= @@ -325,6 +326,8 @@ github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07 h1:UyzmZLoiDWMRywV4DUYb9Fbt8uiOSooupjTq10vpvnU= +github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07/go.mod h1:kDXzergiv9cbyO7IOYJZWg1U88JhDg3PB6klq9Hg2pA= github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM= github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= @@ -332,6 +335,10 @@ github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4d github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= github.com/urfave/cli v1.22.14 h1:ebbhrRiGK2i4naQJr+1Xj92HXZCrK7MsyTS/ob3HnAk= github.com/urfave/cli v1.22.14/go.mod h1:X0eDS6pD6Exaclxm99NJ3FiCDRED7vIHpx2mDOHLvkA= +github.com/warthog618/modem v0.4.0 h1:QmNRbVopcJYTEalWePSgKd5rhpQW5x/yptojTFVdjjg= +github.com/warthog618/modem v0.4.0/go.mod h1:9b3nNrk7JZRskP+TpHQppfz5QxRKkQGdgeq2Fi0QHcI= +github.com/warthog618/sms v0.3.0 h1:LYAb5ngmu2qjNExgji3B7xi2tIZ9+DsuE9pC5xs4wwc= +github.com/warthog618/sms v0.3.0/go.mod h1:+bYZGeBxu003sxD5xhzsrIPBAjPBzTABsRTwSpd7ld4= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= @@ -476,6 +483,7 @@ golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200413165638-669c56c373c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -645,6 +653,7 @@ gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLks gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/notifier/backend_email.go b/notifier/backend_email.go new file mode 100644 index 0000000..6596362 --- /dev/null +++ b/notifier/backend_email.go @@ -0,0 +1,69 @@ +// +// Copyright (c) 2023 whawty contributors (see AUTHORS file) +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are met: +// +// - Redistributions of source code must retain the above copyright notice, this +// list of conditions and the following disclaimer. +// +// - Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// +// - Neither the name of whawty.alerts nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +// + +package notifier + +import ( + "context" + "fmt" + "log" + + "github.com/whawty/alerts/store" +) + +type EMailBackend struct { + infoLog *log.Logger + dbgLog *log.Logger + name string + conf *NotifierBackendConfigEMail + // TODO: add client config +} + +func NewEMailBackend(name string, conf *NotifierBackendConfigEMail, infoLog, dbgLog *log.Logger) *EMailBackend { + return &EMailBackend{name: name, conf: conf, infoLog: infoLog, dbgLog: dbgLog} +} + +func (emb *EMailBackend) Init() (err error) { + return fmt.Errorf("not yet implemented!") +} + +func (smb *EMailBackend) Ready() bool { + // TODO: implement this + return false +} + +func (emb *EMailBackend) Notify(ctx context.Context, target NotifierTarget, alert *store.Alert) error { + return fmt.Errorf("not yet implemented!") +} + +func (emb *EMailBackend) Close() error { + // TODO: close client? + return nil +} diff --git a/notifier/backend_smsmodem.go b/notifier/backend_smsmodem.go new file mode 100644 index 0000000..17d0262 --- /dev/null +++ b/notifier/backend_smsmodem.go @@ -0,0 +1,129 @@ +// +// Copyright (c) 2023 whawty contributors (see AUTHORS file) +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are met: +// +// - Redistributions of source code must retain the above copyright notice, this +// list of conditions and the following disclaimer. +// +// - Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// +// - Neither the name of whawty.alerts nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +// + +package notifier + +import ( + "context" + "fmt" + "io" + "log" + "time" + + "github.com/warthog618/modem/at" + "github.com/warthog618/modem/gsm" + "github.com/warthog618/modem/serial" + "github.com/whawty/alerts/store" +) + +type SMSModemBackend struct { + infoLog *log.Logger + dbgLog *log.Logger + name string + conf *NotifierBackendConfigSMSModem + modem io.ReadWriteCloser + sms *gsm.GSM +} + +func NewSMSModemBackend(name string, conf *NotifierBackendConfigSMSModem, infoLog, dbgLog *log.Logger) *SMSModemBackend { + if conf.Timeout <= 0 { + conf.Timeout = 5 * time.Second + } + return &SMSModemBackend{name: name, conf: conf, infoLog: infoLog, dbgLog: dbgLog} +} + +func (smb *SMSModemBackend) Init() (err error) { + smb.modem, err = serial.New(serial.WithPort(smb.conf.Device), serial.WithBaud(smb.conf.Baudrate)) + if err != nil { + smb.modem = nil + return + } + + a := at.New(smb.modem, at.WithTimeout(smb.conf.Timeout)) + if smb.conf.Pin != nil { + var resp []string + resp, err = a.Command(fmt.Sprintf("+CPIN=%d", smb.conf.Pin)) + if err != nil { + smb.modem.Close() + smb.modem = nil + return + } + smb.dbgLog.Printf("SMSModem(%s): enter pin code respone: %v", smb.name, resp) + } + + smb.sms = gsm.New(a) + err = smb.sms.Init() + if err != nil { + smb.modem.Close() + smb.modem = nil + smb.sms = nil + return + } + + err = smb.sms.StartMessageRx( + func(msg gsm.Message) { + smb.infoLog.Printf("SMSModem(%s): got SMS from '%s': %s", smb.name, msg.Number, msg.Message) + }, + func(err error) { + smb.infoLog.Printf("SMSModem(%s): got SMS rx error: %v", smb.name, err) + }) + + if err != nil { + smb.modem.Close() + smb.modem = nil + smb.sms = nil + return + } + return nil +} + +func (smb *SMSModemBackend) Ready() bool { + return smb.modem != nil && smb.sms != nil +} + +func (smb *SMSModemBackend) Notify(ctx context.Context, target NotifierTarget, alert *store.Alert) error { + // TODO: improve alert formatting + message := fmt.Sprintf("%s / %s / %s", alert.State, alert.Severity, alert.Name) + + resp, err := smb.sms.SendLongMessage(target.SMS.Number, message) + if err != nil { + return err + } + smb.dbgLog.Printf("SMSModem(%s): send sms response: %v", smb.name, resp) + return nil +} + +func (smb *SMSModemBackend) Close() error { + smb.sms.StopMessageRx() + smb.modem.Close() + smb.modem = nil + smb.sms = nil + return nil +} diff --git a/notifier/notifier.go b/notifier/notifier.go index 3666609..50c5a47 100644 --- a/notifier/notifier.go +++ b/notifier/notifier.go @@ -40,12 +40,13 @@ import ( ) type Notifier struct { - conf *Config - store *store.Store - infoLog *log.Logger - dbgLog *log.Logger - ctx context.Context - cancel context.CancelFunc + conf *Config + store *store.Store + infoLog *log.Logger + dbgLog *log.Logger + ctx context.Context + cancel context.CancelFunc + backends map[string]NotifierBackend } func (n *Notifier) Close() error { @@ -67,8 +68,47 @@ func NewNotifier(conf *Config, st *store.Store, infoLog, dbgLog *log.Logger) (n n.conf.Interval = 1 * time.Minute } + n.backends = make(map[string]NotifierBackend) + for idx, backend := range n.conf.Backends { + if backend.Name == "" { + infoLog.Printf("notifier: ignoring unnamed backend at index %d", idx) + continue // make this an permanent error?? + } + if _, ok := n.backends[backend.Name]; ok { + infoLog.Printf("notifier: ignoring duplicate backend name at index %d", idx) + continue // make this an permanent error?? + } + + var b NotifierBackend + cnt := 0 + if backend.EMail != nil { + b = NewEMailBackend(backend.Name, backend.EMail, infoLog, dbgLog) + cnt = cnt + 1 + } + if backend.SMSModem != nil { + b = NewSMSModemBackend(backend.Name, backend.SMSModem, infoLog, dbgLog) + cnt = cnt + 1 + } + if cnt == 0 { + infoLog.Printf("notifier: no valid backend config found for backend '%s'", backend.Name) + continue + } + if cnt > 1 { + infoLog.Printf("notifier: ambiguous backend config '%s'", backend.Name) + continue + } + n.backends[backend.Name] = b + + if err := b.Init(); err != nil { + infoLog.Printf("notifier: failed to initialize backend '%s': %v", backend.Name, err) + } else { + infoLog.Printf("notifier: backend '%s' successfully initialized", backend.Name) + } + } + + // TODO: start go-routine to re-initialize failed backends // TODO: start go-routine to handle notfications - infoLog.Printf("notifier: started with evaluation interval %s", conf.Interval.String()) + infoLog.Printf("notifier: started with %d backends and evaluation interval %s", len(n.backends), conf.Interval.String()) return } diff --git a/notifier/types.go b/notifier/types.go index 7704fd5..2367187 100644 --- a/notifier/types.go +++ b/notifier/types.go @@ -31,15 +31,59 @@ package notifier import ( + "context" "time" + + "github.com/whawty/alerts/store" ) +type NotifierBackendConfigEMail struct { + From string `yaml:"from"` + Smarthost string `yaml:"smarthost"` + // TODO: add auth and TLS support +} + +type NotifierBackendConfigSMSModem struct { + Device string `yaml:"device"` + Baudrate int `yaml:"baudrate"` + Timeout time.Duration `yaml:"timeout"` + Pin *uint `yaml:"pin"` +} + +type NotifierBackendConfig struct { + Name string + EMail *NotifierBackendConfigEMail `yaml:"email"` + SMSModem *NotifierBackendConfigSMSModem `yaml:"smsModem"` +} + +type NotifierTargetSMS struct { + Number string `yaml:"number"` +} + +type NotifierTargetEMail struct { + TO []string `yaml:"to"` + CC []string `yaml:"cc"` + BCC []string `yaml:"bcc"` +} + type NotifierTarget struct { - Type string `yaml:"type"` - // TODO: other fields? + Name string `yaml:"name"` + Backend string `yaml:"backend"` + EMail *NotifierTargetEMail `yaml:"email"` + SMS *NotifierTargetSMS `yaml:"sms"` } type Config struct { - Interval time.Duration `yaml:"interval"` - Targets []NotifierTarget `yaml:"targets"` + Interval time.Duration `yaml:"interval"` + Backends []NotifierBackendConfig `yaml:"backends"` + Targets []NotifierTarget `yaml:"targets"` +} + +// Interfaces + +type NotifierBackend interface { + Init() error + Ready() bool + Notify(context.Context, NotifierTarget, *store.Alert) error + Close() error }