2016-02-26 17:44:10 +00:00
|
|
|
package main
|
|
|
|
|
|
|
|
import (
|
|
|
|
"fmt"
|
2017-09-13 11:12:02 +00:00
|
|
|
"net/http"
|
2016-02-26 17:44:10 +00:00
|
|
|
"os"
|
|
|
|
"os/exec"
|
2018-04-15 11:54:39 +00:00
|
|
|
"regexp"
|
2016-02-26 17:44:10 +00:00
|
|
|
"strings"
|
|
|
|
"time"
|
|
|
|
|
|
|
|
"github.com/Luzifer/rconfig"
|
2018-04-15 11:54:39 +00:00
|
|
|
"github.com/elastic/beats/libbeat/common/dtfmt"
|
|
|
|
"github.com/olivere/elastic"
|
2016-02-26 17:44:10 +00:00
|
|
|
"github.com/robfig/cron"
|
2018-04-15 11:54:39 +00:00
|
|
|
log "github.com/sirupsen/logrus"
|
2017-09-13 11:12:02 +00:00
|
|
|
"golang.org/x/net/context"
|
2018-04-15 11:54:39 +00:00
|
|
|
elogrus "gopkg.in/sohlich/elogrus.v3"
|
2017-09-13 11:12:02 +00:00
|
|
|
"gopkg.in/yaml.v2"
|
2016-02-26 17:44:10 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
var (
|
|
|
|
cfg = struct {
|
2017-09-13 11:12:02 +00:00
|
|
|
ConfigFile string `flag:"config" default:"config.yaml" description:"Cron definition file"`
|
|
|
|
Hostname string `flag:"hostname" description:"Overwrite system hostname"`
|
|
|
|
PingTimout time.Duration `flag:"ping-timeout" default:"1s" description:"Timeout for success / failure pings"`
|
2016-02-26 17:44:10 +00:00
|
|
|
}{}
|
|
|
|
|
2018-04-15 11:54:39 +00:00
|
|
|
version = "dev"
|
2016-02-26 17:44:10 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
type cronConfig struct {
|
2018-04-15 11:54:39 +00:00
|
|
|
Elasticsearch struct {
|
|
|
|
Auth []string `yaml:"auth"`
|
|
|
|
Index string `yaml:"index"`
|
|
|
|
Servers []string `yaml:"servers"`
|
|
|
|
} `yaml:"elasticsearch"`
|
|
|
|
Jobs []cronJob `yaml:"jobs"`
|
2016-02-26 17:44:10 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
type cronJob struct {
|
2017-09-13 11:12:02 +00:00
|
|
|
Name string `yaml:"name"`
|
|
|
|
Schedule string `yaml:"schedule"`
|
|
|
|
Command string `yaml:"cmd"`
|
|
|
|
Arguments []string `yaml:"args"`
|
|
|
|
PingSuccess string `yaml:"ping_success"`
|
|
|
|
PingFailure string `yaml:"ping_failure"`
|
2016-02-26 17:44:10 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func init() {
|
2018-04-15 11:54:39 +00:00
|
|
|
rconfig.ParseAndValidate(&cfg)
|
2016-02-26 17:44:10 +00:00
|
|
|
|
|
|
|
if cfg.Hostname == "" {
|
|
|
|
hostname, _ := os.Hostname()
|
|
|
|
cfg.Hostname = hostname
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-04-15 11:54:39 +00:00
|
|
|
func readConfig() (*cronConfig, error) {
|
|
|
|
fp, err := os.Open(cfg.ConfigFile)
|
2016-02-26 17:44:10 +00:00
|
|
|
if err != nil {
|
2018-04-15 11:54:39 +00:00
|
|
|
return nil, err
|
2016-02-26 17:44:10 +00:00
|
|
|
}
|
2018-04-15 11:54:39 +00:00
|
|
|
defer fp.Close()
|
2016-02-26 17:44:10 +00:00
|
|
|
|
2018-04-15 11:54:39 +00:00
|
|
|
cc := &cronConfig{}
|
|
|
|
cc.Elasticsearch.Index = "elastic_cron-%{+YYYY.MM.dd}"
|
|
|
|
return cc, yaml.NewDecoder(fp).Decode(cc)
|
|
|
|
}
|
|
|
|
|
|
|
|
func main() {
|
|
|
|
cc, err := readConfig()
|
|
|
|
if err != nil {
|
|
|
|
log.Fatalf("Unable to read config file: %s", err)
|
2016-02-26 17:44:10 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
c := cron.New()
|
|
|
|
|
|
|
|
for i := range cc.Jobs {
|
|
|
|
job := cc.Jobs[i]
|
|
|
|
if err := c.AddFunc(job.Schedule, getJobExecutor(job)); err != nil {
|
|
|
|
log.Fatalf("Unable to add job '%s': %s", job.Name, err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-04-15 11:54:39 +00:00
|
|
|
opts := []elastic.ClientOptionFunc{
|
|
|
|
elastic.SetURL(cc.Elasticsearch.Servers...),
|
|
|
|
}
|
|
|
|
|
|
|
|
if cc.Elasticsearch.Auth != nil && len(cc.Elasticsearch.Auth) == 2 && cc.Elasticsearch.Auth[0] != "" {
|
|
|
|
opts = append(opts, elastic.SetBasicAuth(cc.Elasticsearch.Auth[0], cc.Elasticsearch.Auth[1]))
|
|
|
|
}
|
|
|
|
|
|
|
|
esClient, err := elastic.NewSimpleClient(opts...)
|
|
|
|
if err != nil {
|
|
|
|
log.WithError(err).Fatal("Unable to create elasticsearch client")
|
|
|
|
}
|
2016-02-26 17:44:10 +00:00
|
|
|
|
2018-04-15 11:54:39 +00:00
|
|
|
hook, err := elogrus.NewElasticHookWithFunc(esClient, cfg.Hostname, log.InfoLevel, getIndexNameFunc(cc))
|
2016-02-26 17:44:10 +00:00
|
|
|
if err != nil {
|
2018-04-15 11:54:39 +00:00
|
|
|
log.WithError(err).Fatal("Unable to create elasticsearch log hook")
|
|
|
|
}
|
|
|
|
log.AddHook(hook)
|
|
|
|
|
|
|
|
c.Run()
|
|
|
|
}
|
|
|
|
|
|
|
|
func getIndexNameFunc(cc *cronConfig) func() string {
|
|
|
|
if !strings.Contains(cc.Elasticsearch.Index, `%{+`) {
|
|
|
|
// Simple string without date expansion
|
|
|
|
return func() string { return cc.Elasticsearch.Index }
|
|
|
|
}
|
|
|
|
|
|
|
|
return func() string {
|
|
|
|
rex := regexp.MustCompile(`%{\+([^}]+)}`)
|
|
|
|
return rex.ReplaceAllStringFunc(cc.Elasticsearch.Index, func(f string) string {
|
|
|
|
f = strings.TrimSuffix(strings.TrimPrefix(f, `%{+`), `}`)
|
|
|
|
d, _ := dtfmt.Format(time.Now(), f)
|
|
|
|
return d
|
|
|
|
})
|
2016-02-26 17:44:10 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func getJobExecutor(job cronJob) func() {
|
|
|
|
return func() {
|
2018-04-15 11:54:39 +00:00
|
|
|
logger := log.WithFields(log.Fields{
|
|
|
|
"job": job.Name,
|
|
|
|
})
|
2016-02-26 17:44:10 +00:00
|
|
|
|
2018-04-15 11:54:39 +00:00
|
|
|
stdout := logger.WriterLevel(log.InfoLevel)
|
|
|
|
defer stdout.Close()
|
|
|
|
stderr := logger.WriterLevel(log.ErrorLevel)
|
|
|
|
defer stderr.Close()
|
2016-02-26 17:44:10 +00:00
|
|
|
|
|
|
|
fmt.Fprintln(stdout, "[SYS] Starting job")
|
|
|
|
|
|
|
|
cmd := exec.Command(job.Command, job.Arguments...)
|
|
|
|
cmd.Stdout = stdout
|
|
|
|
cmd.Stderr = stderr
|
|
|
|
|
|
|
|
err := cmd.Run()
|
|
|
|
switch err.(type) {
|
|
|
|
case nil:
|
2018-04-15 11:54:39 +00:00
|
|
|
logger.Info("[SYS] Command execution successful")
|
2017-09-13 11:12:02 +00:00
|
|
|
go func(url string) {
|
|
|
|
if err := doPing(url); err != nil {
|
2018-04-15 11:54:39 +00:00
|
|
|
logger.WithError(err).Errorf("[SYS] Ping to URL %q caused an error", url)
|
2017-09-13 11:12:02 +00:00
|
|
|
}
|
|
|
|
}(job.PingSuccess)
|
|
|
|
|
2016-02-26 17:44:10 +00:00
|
|
|
case *exec.ExitError:
|
2018-04-15 11:54:39 +00:00
|
|
|
logger.Info("[SYS] Command exited with unexpected exit code != 0")
|
2017-09-13 11:12:02 +00:00
|
|
|
go func(url string) {
|
|
|
|
if err := doPing(url); err != nil {
|
2018-04-15 11:54:39 +00:00
|
|
|
logger.WithError(err).Errorf("[SYS] Ping to URL %q caused an error", url)
|
2017-09-13 11:12:02 +00:00
|
|
|
}
|
|
|
|
}(job.PingFailure)
|
|
|
|
|
2016-02-26 17:44:10 +00:00
|
|
|
default:
|
2018-04-15 11:54:39 +00:00
|
|
|
logger.WithError(err).Error("[SYS] Execution caused error")
|
2017-09-13 11:12:02 +00:00
|
|
|
go func(url string) {
|
|
|
|
if err := doPing(url); err != nil {
|
2018-04-15 11:54:39 +00:00
|
|
|
logger.WithError(err).Errorf("[SYS] Ping to URL %q caused an error", url)
|
2017-09-13 11:12:02 +00:00
|
|
|
}
|
|
|
|
}(job.PingFailure)
|
|
|
|
|
2016-02-26 17:44:10 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-09-13 11:12:02 +00:00
|
|
|
func doPing(url string) error {
|
|
|
|
if url == "" {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), cfg.PingTimout)
|
|
|
|
defer cancel()
|
|
|
|
|
|
|
|
req, err := http.NewRequest("GET", url, nil)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
resp, err := http.DefaultClient.Do(req.WithContext(ctx))
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
if resp.StatusCode > 299 {
|
|
|
|
return fmt.Errorf("Expected HTTP2xx status, got HTTP%d", resp.StatusCode)
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|