package main
import (
"bytes"
"database/sql"
"encoding/json"
"flag"
"io/ioutil"
"log"
"sync"
"text/template"
"time"
_ "github.com/jackc/pgx/v5/stdlib"
)
var (
configPath = flag.String("config", "monwork.conf", "path to the config file")
)
type (
Config struct {
DB string `json:"db"`
CheckInterval string `json:"interval"`
}
)
func main() {
flag.Parse()
raw, err := ioutil.ReadFile(*configPath)
if err != nil {
log.Fatalf("could not read config: %s", err)
}
config := Config{}
if err := json.Unmarshal(raw, &config); err != nil {
log.Fatalf("could not parse config: %s", err)
}
checkInterval, err := time.ParseDuration(config.CheckInterval)
if err != nil {
log.Fatalf("could not parse check interval: %s", err)
}
db, err := sql.Open("pgx", config.DB)
if err != nil {
log.Fatalf("could not open database connection: %s", err)
}
go startNodeGen(db, checkInterval)
go startCommandGen(db, checkInterval)
go startConfigGen(db, checkInterval)
// don't exit, we have work to do
wg := sync.WaitGroup{}
wg.Add(1)
wg.Wait()
}
func startNodeGen(db *sql.DB, checkInterval time.Duration) {
for {
tx, err := db.Begin()
if err != nil {
log.Printf("could not create transaction: %s", err)
time.Sleep(checkInterval)
continue
}
_, err = tx.Exec(`update checks c
set updated = n.updated
from nodes n
where c.node_id = n.id
and c.last_refresh < n.updated;`)
if err != nil {
log.Printf("could not update nodes: %s", err)
tx.Rollback()
time.Sleep(checkInterval)
continue
}
if err := tx.Commit(); err != nil {
log.Printf("could not commit node updates: %s", err)
tx.Rollback()
}
time.Sleep(checkInterval)
}
}
func startCommandGen(db *sql.DB, checkInterval time.Duration) {
for {
tx, err := db.Begin()
if err != nil {
log.Printf("could not create transaction: %s", err)
time.Sleep(checkInterval)
continue
}
_, err = tx.Exec(`update checks c
set updated = co.updated
from commands co
where c.command_id = co.id
and c.last_refresh < co.updated;`)
if err != nil {
log.Printf("could not update checks: %s", err)
tx.Rollback()
time.Sleep(checkInterval)
continue
}
if err := tx.Commit(); err != nil {
log.Printf("could not commit command updates: %s", err)
tx.Rollback()
}
time.Sleep(checkInterval)
}
}
func startConfigGen(db *sql.DB, checkInterval time.Duration) {
for {
tx, err := db.Begin()
if err != nil {
log.Printf("could not create transaction: %s", err)
time.Sleep(checkInterval)
continue
}
rows, err := tx.Query(SQLGetConfigUpdates)
if err != nil {
log.Printf("could not get updates: %s", err)
tx.Rollback()
time.Sleep(checkInterval)
continue
}
var (
check_id int
command string
options []byte
)
for rows.Next() {
if rows.Err() != nil {
log.Printf("could not receive rows: %s", err)
break
}
if err := rows.Scan(&check_id, &command, &options); err != nil {
log.Printf("could not scan row: %s", err)
break
}
}
if check_id == 0 {
tx.Rollback()
time.Sleep(checkInterval)
continue
}
tmpl, err := template.New("command").Parse(command)
if err != nil {
tx.Rollback()
log.Printf("could not parse command for check '%d': %s", check_id, err)
time.Sleep(checkInterval)
continue
}
var cmd bytes.Buffer
var opts map[string]interface{}
if err := json.Unmarshal(options, &opts); err != nil {
tx.Rollback()
log.Printf("could not parse options for check '%d': %s", check_id, err)
time.Sleep(checkInterval)
continue
}
if err := tmpl.Execute(&cmd, opts); err != nil {
tx.Rollback()
log.Printf("could not complete command for check '%d': %s", check_id, err)
time.Sleep(checkInterval)
continue
}
if _, err := tx.Exec(SQLRefreshActiveCheck, check_id, stringToShellFields(cmd.Bytes())); err != nil {
tx.Rollback()
log.Printf("could not refresh check '%d': %s", check_id, err)
continue
}
if _, err := tx.Exec(SQLUpdateLastRefresh, check_id); err != nil {
tx.Rollback()
log.Printf("could not update timestamp for check '%d': %s", check_id, err)
continue
}
if err := tx.Commit(); err != nil {
tx.Rollback()
log.Printf("could not commit changes: %s", err)
}
}
}
func stringToShellFields(in []byte) [][]byte {
if len(in) == 0 {
return [][]byte{}
}
fields := bytes.Fields(in)
result := [][]byte{}
var quote byte
for _, field := range fields {
if quote == 0 && (field[0] != '\'' && field[0] != '"') {
result = append(result, field)
continue
}
if quote == 0 && (field[0] == '\'' || field[0] == '"') {
quote = field[0]
if field[len(field)-1] == quote {
result = append(result, field[1:len(field)-1])
quote = 0
continue
}
result = append(result, field[1:])
continue
}
idx := len(result) - 1
if bytes.HasSuffix(field, []byte{quote}) {
result[idx] = append(result[idx], append([]byte(" "), field[:len(field)-1]...)...)
quote = 0
continue
}
result[idx] = append(result[idx], append([]byte(" "), field...)...)
}
return result
}
var (
SQLGetConfigUpdates = `select c.id, co.command, c.options
from checks c
join commands co on c.command_id = co.id
where c.last_refresh < c.updated or c.last_refresh is null
limit 1
for update of c skip locked;`
SQLRefreshActiveCheck = `insert into active_checks(check_id, cmdline, intval, enabled, msg, mapping_id, checker_id)
select c.id, $2, c.intval, c.enabled, case when ac.msg is null then '' else ac.msg end, case when c.mapping_id is not null then c.mapping_id when n.mapping_id is not null then n.mapping_id else 1 end, c.checker_id
from checks c
left join active_checks ac on c.id = ac.check_id
left join nodes n on c.node_id = n.id
where c.id = $1
on conflict(check_id)
do update set cmdline = $2, intval = excluded.intval, enabled = excluded.enabled, mapping_id = excluded.mapping_id, checker_id = excluded.checker_id;`
SQLUpdateLastRefresh = `update checks set last_refresh = now() where id = $1;`
)