1241 lines
30 KiB
Go
1241 lines
30 KiB
Go
package main
|
|
|
|
import (
|
|
// External
|
|
ws "git.gibonuddevalla.se/go/webservice"
|
|
"git.gibonuddevalla.se/go/webservice/session"
|
|
werr "git.gibonuddevalla.se/go/wrappederror"
|
|
|
|
// Internal
|
|
"smon/notification"
|
|
|
|
// Standard
|
|
"embed"
|
|
"encoding/json"
|
|
"flag"
|
|
"fmt"
|
|
"html/template"
|
|
"io"
|
|
"io/fs"
|
|
"log/slog"
|
|
"net/http"
|
|
"net/url"
|
|
"os"
|
|
"path"
|
|
"slices"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
const VERSION = "v27"
|
|
|
|
var (
|
|
logger *slog.Logger
|
|
flagConfigFile string
|
|
flagDev bool
|
|
flagVersion bool
|
|
service *ws.Service
|
|
logFile *os.File
|
|
parsedTemplates map[string]*template.Template
|
|
componentFilenames []string
|
|
notificationManager notification.Manager
|
|
fileConf FileConfiguration
|
|
|
|
//go:embed sql
|
|
sqlFS embed.FS
|
|
|
|
//go:embed views
|
|
viewFS embed.FS
|
|
|
|
//go:embed static
|
|
staticFS embed.FS
|
|
)
|
|
|
|
func init() { // {{{
|
|
opts := slog.HandlerOptions{}
|
|
logger = slog.New(slog.NewJSONHandler(os.Stdout, &opts))
|
|
|
|
confDir, err := os.UserConfigDir()
|
|
if err != nil {
|
|
logger.Error("application", "error", werr.Wrap(err))
|
|
}
|
|
cfgPath := path.Join(confDir, "smon.yaml")
|
|
flag.StringVar(&flagConfigFile, "config", cfgPath, "Path and filename of the YAML configuration file")
|
|
flag.BoolVar(&flagDev, "dev", false, "reload templates on each request")
|
|
flag.BoolVar(&flagVersion, "version", false, "Display version and exit")
|
|
flag.Parse()
|
|
|
|
componentFilenames, err = getComponentFilenames(viewFS)
|
|
if err != nil {
|
|
logger.Error("application", "error", err)
|
|
os.Exit(1)
|
|
}
|
|
parsedTemplates = make(map[string]*template.Template)
|
|
} // }}}
|
|
|
|
func main() { // {{{
|
|
if flagVersion {
|
|
fmt.Println(VERSION)
|
|
os.Exit(0)
|
|
}
|
|
|
|
var err error
|
|
|
|
werr.Init()
|
|
werr.SetLogCallback(logHandler)
|
|
|
|
service, err = ws.New(flagConfigFile, VERSION, logger)
|
|
if err != nil {
|
|
logger.Error("application", "error", err)
|
|
os.Exit(1)
|
|
}
|
|
|
|
j, _ := json.Marshal(service.Config.Application)
|
|
json.Unmarshal(j, &fileConf)
|
|
logFile, err = os.OpenFile(fileConf.LogFile, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0600)
|
|
if err != nil {
|
|
logger.Error("application", "error", err)
|
|
return
|
|
}
|
|
if fileConf.NodataInterval < 10 {
|
|
logger.Error("application → nodata_interval has to be larger or equal to 10.")
|
|
return
|
|
}
|
|
|
|
service.SetDatabase(sqlProvider)
|
|
service.SetStaticFS(staticFS, "static")
|
|
service.SetStaticDirectory("static", flagDev)
|
|
service.InitDatabaseConnection()
|
|
err = service.Db.Connect()
|
|
if err != nil {
|
|
logger.Error("application", "error", err)
|
|
return
|
|
}
|
|
|
|
notificationManager, err = InitNotificationManager()
|
|
if err != nil {
|
|
err = werr.Wrap(err).Log()
|
|
logger.Error("notification", "error", err)
|
|
}
|
|
|
|
service.Register("/", false, false, staticHandler)
|
|
|
|
service.Register("/area/new/{name}", false, false, actionAreaNew)
|
|
service.Register("/area/rename/{id}/{name}", false, false, actionAreaRename)
|
|
service.Register("/area/delete/{id}", false, false, actionAreaDelete)
|
|
|
|
service.Register("/section/new/{areaID}/{name}", false, false, actionSectionNew)
|
|
service.Register("/section/rename/{id}/{name}", false, false, actionSectionRename)
|
|
service.Register("/section/delete/{id}", false, false, actionSectionDelete)
|
|
|
|
service.Register("/problems", false, false, pageProblems)
|
|
service.Register("/problem/acknowledge/{id}", false, false, actionProblemAcknowledge)
|
|
service.Register("/problem/unacknowledge/{id}", false, false, actionProblemUnacknowledge)
|
|
|
|
service.Register("/datapoints", false, false, pageDatapoints)
|
|
service.Register("/datapoint/edit/{id}", false, false, pageDatapointEdit)
|
|
service.Register("/datapoint/update/{id}", false, false, actionDatapointUpdate)
|
|
service.Register("/datapoint/delete/{id}", false, false, actionDatapointDelete)
|
|
service.Register("/datapoint/values/{id}", false, false, pageDatapointValues)
|
|
service.Register("/datapoint/json/{id}", false, false, actionDatapointJson)
|
|
|
|
service.Register("/triggers", false, false, pageTriggers)
|
|
service.Register("/trigger/create/{sectionID}/{name}", false, false, actionTriggerCreate)
|
|
service.Register("/trigger/edit/{id}", false, false, pageTriggerEdit)
|
|
service.Register("/trigger/edit/{id}/{sectionID}", false, false, pageTriggerEdit)
|
|
service.Register("/trigger/addDatapoint/{id}/{datapointName}", false, false, actionTriggerDatapointAdd)
|
|
service.Register("/trigger/update/{id}", false, false, actionTriggerUpdate)
|
|
service.Register("/trigger/run/{id}", false, false, actionTriggerRun)
|
|
service.Register("/trigger/delete/{id}", false, false, actionTriggerDelete)
|
|
|
|
service.Register("/notifications", false, false, pageNotifications)
|
|
|
|
service.Register("/configuration", false, false, pageConfiguration)
|
|
service.Register("/configuration/theme", false, false, actionConfigurationTheme)
|
|
service.Register("/configuration/timezone", false, false, actionConfigurationTimezone)
|
|
service.Register("/configuration/notification", false, false, pageConfigurationNotification)
|
|
service.Register("/configuration/notification/update/{prio}", false, false, actionConfigurationNotificationUpdate)
|
|
service.Register("/configuration/notification/delete/{prio}", false, false, actionConfigurationNotificationDelete)
|
|
service.Register("/entry/{datapoint}", false, false, actionEntryDatapoint)
|
|
|
|
go nodataLoop()
|
|
|
|
smonConfig, err = SmonConfigInit()
|
|
if err != nil {
|
|
logger.Error("configuration", "error", werr.Wrap(err))
|
|
os.Exit(1)
|
|
}
|
|
|
|
err = smonConfig.Validate()
|
|
if err != nil {
|
|
logger.Error("configuration", "error", werr.Wrap(err))
|
|
os.Exit(1)
|
|
}
|
|
|
|
err = service.Start()
|
|
if err != nil {
|
|
logger.Error("webserver", "error", werr.Wrap(err))
|
|
os.Exit(1)
|
|
}
|
|
|
|
} // }}}
|
|
|
|
func sqlProvider(dbname string, version int) (sql []byte, found bool) { // {{{
|
|
var err error
|
|
sql, err = sqlFS.ReadFile(fmt.Sprintf("sql/%05d.sql", version))
|
|
if err != nil {
|
|
return
|
|
}
|
|
found = true
|
|
return
|
|
} // }}}
|
|
func logHandler(err werr.Error) { // {{{
|
|
j, _ := json.Marshal(err)
|
|
logFile.Write(j)
|
|
logFile.Write([]byte("\n"))
|
|
} // }}}
|
|
|
|
func httpError(w http.ResponseWriter, err error) { // {{{
|
|
resp := struct {
|
|
OK bool
|
|
Error string
|
|
}{
|
|
false,
|
|
err.Error(),
|
|
}
|
|
j, _ := json.Marshal(resp)
|
|
w.Write(j)
|
|
} // }}}
|
|
func pageError(w http.ResponseWriter, redirectURL string, pageErr error) { // {{{
|
|
u, err := url.Parse(redirectURL)
|
|
if err != nil {
|
|
httpError(w, err)
|
|
return
|
|
}
|
|
|
|
values := u.Query()
|
|
values.Add("_err", pageErr.Error())
|
|
u.RawQuery = values.Encode()
|
|
|
|
w.Header().Add("Location", u.String())
|
|
w.WriteHeader(302)
|
|
} // }}}
|
|
|
|
func staticHandler(w http.ResponseWriter, r *http.Request, sess *session.T) { // {{{
|
|
if flagDev && !reloadTemplates(w) {
|
|
return
|
|
}
|
|
|
|
if r.URL.Path == "/" {
|
|
pageIndex(w, r, sess)
|
|
return
|
|
}
|
|
|
|
service.StaticHandler(w, r, sess)
|
|
} // }}}
|
|
func actionEntryDatapoint(w http.ResponseWriter, r *http.Request, sess *session.T) { // {{{
|
|
dpoint := r.PathValue("datapoint")
|
|
value, _ := io.ReadAll(r.Body)
|
|
|
|
err := DatapointAdd(dpoint, value)
|
|
if err != nil {
|
|
httpError(w, werr.Wrap(err).Log())
|
|
return
|
|
}
|
|
|
|
var triggers []Trigger
|
|
triggers, err = TriggersRetrieveByDatapoint(dpoint)
|
|
if err != nil {
|
|
logger.Error("entry", "error", err)
|
|
}
|
|
|
|
// Multiple triggers can use the same datapoint.
|
|
for _, trigger := range triggers {
|
|
var out any
|
|
out, err = trigger.Run()
|
|
if err != nil {
|
|
err = werr.Wrap(err).Log()
|
|
logger.Error("entry", "error", err)
|
|
|
|
}
|
|
logger.Debug("entry", "datapoint", dpoint, "value", value, "trigger", trigger, "result", out)
|
|
|
|
var problemID int
|
|
var problemState string
|
|
switch v := out.(type) {
|
|
case bool:
|
|
// Trigger returning true - a problem occurred
|
|
if v {
|
|
problemID, err = ProblemStart(trigger)
|
|
if err != nil {
|
|
err = werr.Wrap(err).Log()
|
|
logger.Error("entry", "error", err)
|
|
}
|
|
problemState = "PROBLEM"
|
|
} else {
|
|
// A problem didn't occur.
|
|
problemID, err = ProblemClose(trigger)
|
|
if err != nil {
|
|
err = werr.Wrap(err).Log()
|
|
logger.Error("entry", "error", err)
|
|
}
|
|
problemState = "OK"
|
|
}
|
|
|
|
// Has a change in problem state happened?
|
|
if problemID == 0 && err == nil {
|
|
logger.Debug("notification", "trigger", trigger.ID, "state", "no change")
|
|
continue
|
|
} else {
|
|
logger.Debug("notification", "trigger", trigger.ID, "state", "change")
|
|
}
|
|
|
|
err = notificationManager.Send(problemID, []byte(problemState+": "+trigger.Name), func(notificationService *notification.Service, err error) {
|
|
logger.Info(
|
|
"notification",
|
|
"service", (*notificationService).GetType(),
|
|
"problemID", problemID,
|
|
"prio", (*notificationService).GetPrio(),
|
|
"ok", true,
|
|
)
|
|
|
|
var errBody any
|
|
if err != nil {
|
|
errBody, _ = json.Marshal(err)
|
|
} else {
|
|
errBody = nil
|
|
}
|
|
_, err = service.Db.Conn.Exec(
|
|
`
|
|
INSERT INTO notification_send(notification_id, problem_id, uuid, ok, error)
|
|
SELECT
|
|
id, $3, '', $4, $5
|
|
FROM notification
|
|
WHERE
|
|
service=$1 AND
|
|
prio=$2
|
|
`,
|
|
(*notificationService).GetType(),
|
|
(*notificationService).GetPrio(),
|
|
problemID,
|
|
err == nil,
|
|
errBody,
|
|
)
|
|
if err != nil {
|
|
err = werr.Wrap(err).Log()
|
|
logger.Error("entry", "error", err)
|
|
}
|
|
})
|
|
if err != nil {
|
|
err = werr.Wrap(err).Log()
|
|
logger.Error("notification", "error", err)
|
|
}
|
|
|
|
default:
|
|
err := fmt.Errorf(`Expression for trigger %s not returning bool (%T)`, trigger.Name, v)
|
|
logger.Info("entry", "error", err)
|
|
werr.Wrap(err).WithData(v).Log()
|
|
}
|
|
}
|
|
|
|
j, _ := json.Marshal(struct{ OK bool }{true})
|
|
w.Write(j)
|
|
} // }}}
|
|
|
|
func reloadTemplates(w http.ResponseWriter) bool { // {{{
|
|
parsedTemplates = make(map[string]*template.Template)
|
|
return true
|
|
} // }}}
|
|
func getComponentFilenames(efs fs.FS) (files []string, err error) { // {{{
|
|
files = []string{}
|
|
if err := fs.WalkDir(efs, "views/components", func(path string, d fs.DirEntry, err error) error {
|
|
if d == nil {
|
|
return nil
|
|
}
|
|
if d.IsDir() {
|
|
return nil
|
|
}
|
|
files = append(files, path)
|
|
return nil
|
|
}); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return files, nil
|
|
} // }}}
|
|
func getPage(layout, page string) (tmpl *template.Template, err error) { // {{{
|
|
layoutFilename := fmt.Sprintf("views/layouts/%s.gotmpl", layout)
|
|
pageFilename := fmt.Sprintf("views/pages/%s.gotmpl", page)
|
|
|
|
if tmpl, found := parsedTemplates[page]; found {
|
|
return tmpl, nil
|
|
}
|
|
|
|
funcMap := template.FuncMap{
|
|
"format_time": func(t time.Time) template.HTML {
|
|
return template.HTML(
|
|
t.In(smonConfig.Timezone()).Format(`<span class="date">2006-01-02</span> <span class="time">15:04:05<span class="seconds">:05</span></span>`),
|
|
)
|
|
},
|
|
}
|
|
|
|
filenames := []string{layoutFilename, pageFilename}
|
|
filenames = append(filenames, componentFilenames...)
|
|
logger.Info("template", "op", "parse", "layout", layout, "page", page, "filenames", filenames)
|
|
if flagDev {
|
|
tmpl, err = template.New(layout+".gotmpl").Funcs(funcMap).ParseFS(os.DirFS("."), filenames...)
|
|
} else {
|
|
tmpl, err = template.New(layout+".gotmpl").Funcs(funcMap).ParseFS(viewFS, filenames...)
|
|
}
|
|
if err != nil {
|
|
err = werr.Wrap(err).Log()
|
|
return
|
|
}
|
|
|
|
parsedTemplates[page] = tmpl
|
|
return
|
|
} // }}}
|
|
|
|
func pageIndex(w http.ResponseWriter, r *http.Request, _ *session.T) { // {{{
|
|
page := Page{
|
|
LAYOUT: "main",
|
|
PAGE: "index",
|
|
CONFIG: smonConfig.Settings,
|
|
}
|
|
page.Render(w, r)
|
|
} // }}}
|
|
|
|
func actionAreaNew(w http.ResponseWriter, r *http.Request, _ *session.T) { // {{{
|
|
name := r.PathValue("name")
|
|
err := AreaCreate(name)
|
|
if err != nil {
|
|
httpError(w, werr.Wrap(err).Log())
|
|
return
|
|
}
|
|
|
|
w.Header().Add("Location", "/configuration")
|
|
w.WriteHeader(302)
|
|
return
|
|
} // }}}
|
|
func actionAreaRename(w http.ResponseWriter, r *http.Request, _ *session.T) { // {{{
|
|
idStr := r.PathValue("id")
|
|
id, err := strconv.Atoi(idStr)
|
|
if err != nil {
|
|
httpError(w, werr.Wrap(err).Log())
|
|
return
|
|
}
|
|
|
|
name := r.PathValue("name")
|
|
err = AreaRename(id, name)
|
|
if err != nil {
|
|
httpError(w, werr.Wrap(err).Log())
|
|
return
|
|
}
|
|
|
|
w.Header().Add("Location", "/configuration")
|
|
w.WriteHeader(302)
|
|
return
|
|
} // }}}
|
|
func actionAreaDelete(w http.ResponseWriter, r *http.Request, _ *session.T) { // {{{
|
|
idStr := r.PathValue("id")
|
|
id, err := strconv.Atoi(idStr)
|
|
if err != nil {
|
|
httpError(w, werr.Wrap(err).WithData(idStr).Log())
|
|
return
|
|
}
|
|
|
|
err = AreaDelete(id)
|
|
if err != nil {
|
|
httpError(w, werr.Wrap(err).WithData(id).Log())
|
|
return
|
|
}
|
|
|
|
w.Header().Add("Location", "/configuration")
|
|
w.WriteHeader(302)
|
|
return
|
|
} // }}}
|
|
|
|
func actionSectionNew(w http.ResponseWriter, r *http.Request, _ *session.T) { // {{{
|
|
idStr := r.PathValue("areaID")
|
|
areaID, err := strconv.Atoi(idStr)
|
|
if err != nil {
|
|
httpError(w, werr.Wrap(err).Log())
|
|
return
|
|
}
|
|
|
|
name := r.PathValue("name")
|
|
err = SectionCreate(areaID, name)
|
|
if err != nil {
|
|
httpError(w, werr.Wrap(err).Log())
|
|
return
|
|
}
|
|
|
|
w.Header().Add("Location", "/configuration")
|
|
w.WriteHeader(302)
|
|
return
|
|
} // }}}
|
|
func actionSectionRename(w http.ResponseWriter, r *http.Request, _ *session.T) { // {{{
|
|
idStr := r.PathValue("id")
|
|
id, err := strconv.Atoi(idStr)
|
|
if err != nil {
|
|
httpError(w, werr.Wrap(err).Log())
|
|
return
|
|
}
|
|
|
|
name := r.PathValue("name")
|
|
err = SectionRename(id, name)
|
|
if err != nil {
|
|
httpError(w, werr.Wrap(err).Log())
|
|
return
|
|
}
|
|
|
|
w.Header().Add("Location", "/configuration")
|
|
w.WriteHeader(302)
|
|
return
|
|
} // }}}
|
|
func actionSectionDelete(w http.ResponseWriter, r *http.Request, _ *session.T) { // {{{
|
|
idStr := r.PathValue("id")
|
|
id, err := strconv.Atoi(idStr)
|
|
if err != nil {
|
|
httpError(w, werr.Wrap(err).WithData(idStr).Log())
|
|
return
|
|
}
|
|
|
|
err = SectionDelete(id)
|
|
if err != nil {
|
|
httpError(w, werr.Wrap(err).WithData(id).Log())
|
|
return
|
|
}
|
|
|
|
w.Header().Add("Location", "/configuration")
|
|
w.WriteHeader(302)
|
|
return
|
|
} // }}}
|
|
|
|
func pageProblems(w http.ResponseWriter, r *http.Request, _ *session.T) { // {{{
|
|
page := Page{
|
|
LAYOUT: "main",
|
|
PAGE: "problems",
|
|
CONFIG: smonConfig.Settings,
|
|
}
|
|
|
|
problems, err := ProblemsRetrieve()
|
|
if err != nil {
|
|
httpError(w, werr.Wrap(err).Log())
|
|
return
|
|
}
|
|
|
|
var found bool
|
|
problemsGrouped := make(map[string]map[string][]Problem)
|
|
for _, problem := range problems {
|
|
if _, found = problemsGrouped[problem.AreaName]; !found {
|
|
problemsGrouped[problem.AreaName] = make(map[string][]Problem)
|
|
}
|
|
if _, found = problemsGrouped[problem.AreaName][problem.SectionName]; !found {
|
|
problemsGrouped[problem.AreaName][problem.SectionName] = []Problem{}
|
|
}
|
|
|
|
problemsGrouped[problem.AreaName][problem.SectionName] = append(
|
|
problemsGrouped[problem.AreaName][problem.SectionName],
|
|
problem,
|
|
)
|
|
}
|
|
|
|
page.Data = map[string]any{
|
|
"Problems": problems,
|
|
"ProblemsGrouped": problemsGrouped,
|
|
}
|
|
page.Render(w, r)
|
|
return
|
|
} // }}}
|
|
func actionProblemAcknowledge(w http.ResponseWriter, r *http.Request, _ *session.T) { // {{{
|
|
idStr := r.PathValue("id")
|
|
id, err := strconv.Atoi(idStr)
|
|
if err != nil {
|
|
httpError(w, werr.Wrap(err).Log())
|
|
return
|
|
}
|
|
|
|
err = ProblemAcknowledge(id, true)
|
|
if err != nil {
|
|
httpError(w, werr.Wrap(err).Log())
|
|
return
|
|
}
|
|
|
|
w.Header().Add("Location", "/problems")
|
|
w.WriteHeader(302)
|
|
|
|
return
|
|
} // }}}
|
|
func actionProblemUnacknowledge(w http.ResponseWriter, r *http.Request, _ *session.T) { // {{{
|
|
idStr := r.PathValue("id")
|
|
id, err := strconv.Atoi(idStr)
|
|
if err != nil {
|
|
httpError(w, werr.Wrap(err).Log())
|
|
return
|
|
}
|
|
|
|
err = ProblemAcknowledge(id, false)
|
|
if err != nil {
|
|
httpError(w, werr.Wrap(err).Log())
|
|
return
|
|
}
|
|
|
|
w.Header().Add("Location", "/problems")
|
|
w.WriteHeader(302)
|
|
|
|
return
|
|
} // }}}
|
|
|
|
func pageDatapoints(w http.ResponseWriter, r *http.Request, _ *session.T) { // {{{
|
|
page := Page{
|
|
LAYOUT: "main",
|
|
PAGE: "datapoints",
|
|
CONFIG: smonConfig.Settings,
|
|
}
|
|
|
|
datapoints, err := DatapointsRetrieve()
|
|
if err != nil {
|
|
httpError(w, werr.Wrap(err).Log())
|
|
return
|
|
}
|
|
|
|
// The datapoint selector in trigger edit wants the raw data in JSON.
|
|
if r.URL.Query().Get("format") == "json" {
|
|
j, _ := json.Marshal(datapoints)
|
|
w.Write(j)
|
|
return
|
|
}
|
|
|
|
page.Data = map[string]any{
|
|
"Datapoints": datapoints,
|
|
}
|
|
page.Render(w, r)
|
|
return
|
|
} // }}}
|
|
func pageDatapointEdit(w http.ResponseWriter, r *http.Request, _ *session.T) { // {{{
|
|
idStr := r.PathValue("id")
|
|
id, err := strconv.Atoi(idStr)
|
|
if err != nil {
|
|
httpError(w, werr.Wrap(err).Log())
|
|
return
|
|
}
|
|
|
|
var datapoint Datapoint
|
|
if id == 0 {
|
|
datapoint.Name = "new_datapoint"
|
|
datapoint.Datatype = "INT"
|
|
} else {
|
|
datapoint, err = DatapointRetrieve(id, "")
|
|
if err != nil {
|
|
httpError(w, werr.Wrap(err).Log())
|
|
return
|
|
}
|
|
}
|
|
|
|
page := Page{
|
|
LAYOUT: "main",
|
|
PAGE: "datapoint_edit",
|
|
CONFIG: smonConfig.Settings,
|
|
MENU: "datapoints",
|
|
Icon: "datapoints",
|
|
Label: "Datapoint",
|
|
}
|
|
|
|
page.Data = map[string]any{
|
|
"Datapoint": datapoint,
|
|
}
|
|
page.Render(w, r)
|
|
return
|
|
} // }}}
|
|
func actionDatapointUpdate(w http.ResponseWriter, r *http.Request, _ *session.T) { // {{{
|
|
idStr := r.PathValue("id")
|
|
id, err := strconv.Atoi(idStr)
|
|
if err != nil {
|
|
httpError(w, werr.Wrap(err).Log())
|
|
return
|
|
}
|
|
|
|
var nodataSeconds int
|
|
nodataSeconds, _ = strconv.Atoi(r.FormValue("nodata_seconds"))
|
|
|
|
var dp Datapoint
|
|
dp.ID = id
|
|
dp.Group = r.FormValue("group")
|
|
dp.Name = r.FormValue("name")
|
|
dp.Datatype = DatapointType(r.FormValue("datatype"))
|
|
dp.Comment = r.FormValue("comment")
|
|
dp.NodataProblemSeconds = nodataSeconds
|
|
err = dp.Update()
|
|
if err != nil {
|
|
httpError(w, werr.Wrap(err).Log())
|
|
return
|
|
}
|
|
|
|
w.Header().Add("Location", "/datapoints")
|
|
w.WriteHeader(302)
|
|
} // }}}
|
|
func actionDatapointDelete(w http.ResponseWriter, r *http.Request, _ *session.T) { // {{{
|
|
idStr := r.PathValue("id")
|
|
id, err := strconv.Atoi(idStr)
|
|
if err != nil {
|
|
httpError(w, werr.Wrap(err).Log())
|
|
return
|
|
}
|
|
|
|
err = DatapointDelete(id)
|
|
if err != nil {
|
|
httpError(w, werr.Wrap(err).Log())
|
|
return
|
|
}
|
|
|
|
w.Header().Add("Location", "/datapoints")
|
|
w.WriteHeader(302)
|
|
} // }}}
|
|
func pageDatapointValues(w http.ResponseWriter, r *http.Request, _ *session.T) { // {{{
|
|
idStr := r.PathValue("id")
|
|
id, err := strconv.Atoi(idStr)
|
|
if err != nil {
|
|
httpError(w, werr.Wrap(err).Log())
|
|
return
|
|
}
|
|
|
|
var datapoint Datapoint
|
|
datapoint, err = DatapointRetrieve(id, "")
|
|
if err != nil {
|
|
httpError(w, werr.Wrap(err).Log())
|
|
return
|
|
}
|
|
|
|
// GET parameters.
|
|
display := r.URL.Query().Get("display")
|
|
if display == "" && datapoint.Datatype == INT {
|
|
display = "graph"
|
|
}
|
|
|
|
var timeFrom, timeTo time.Time
|
|
yesterday := time.Now().Add(time.Duration(-24 * time.Hour))
|
|
timeFrom, err = parseHTMLDateTime(r.URL.Query().Get("f"), yesterday)
|
|
if err != nil {
|
|
httpError(w, werr.Wrap(err).WithData(r.URL.Query().Get("f")).Log())
|
|
return
|
|
}
|
|
|
|
timeTo, err = parseHTMLDateTime(r.URL.Query().Get("t"), time.Now())
|
|
if err != nil {
|
|
httpError(w, werr.Wrap(err).WithData(r.URL.Query().Get("t")).Log())
|
|
return
|
|
}
|
|
|
|
presetTimeInterval(time.Hour, r.URL.Query().Get("preset"), &timeFrom, &timeTo)
|
|
|
|
// Apply an optionally set offset (in seconds).
|
|
timeFrom = applyTimeOffset(timeFrom, time.Second, r.URL.Query().Get("offset-time"))
|
|
timeTo = applyTimeOffset(timeTo, time.Second, r.URL.Query().Get("offset-time"))
|
|
|
|
// Fetch data point values according to the times.
|
|
var values []DatapointValue
|
|
values, err = DatapointValues(id, timeFrom, timeTo)
|
|
if err != nil {
|
|
httpError(w, werr.Wrap(err).Log())
|
|
return
|
|
}
|
|
|
|
page := Page{
|
|
LAYOUT: "main",
|
|
PAGE: "datapoint_values",
|
|
CONFIG: smonConfig.Settings,
|
|
MENU: "datapoints",
|
|
Icon: "datapoints",
|
|
Label: "Values for " + datapoint.Name,
|
|
}
|
|
|
|
page.Data = map[string]any{
|
|
"Datapoint": datapoint,
|
|
"Values": values,
|
|
"TimeFrom": timeFrom.Format("2006-01-02T15:04:05"),
|
|
"TimeTo": timeTo.Format("2006-01-02T15:04:05"),
|
|
"Display": display,
|
|
}
|
|
page.Render(w, r)
|
|
return
|
|
} // }}}
|
|
func actionDatapointJson(w http.ResponseWriter, r *http.Request, _ *session.T) { // {{{
|
|
idStr := r.PathValue("id")
|
|
id, err := strconv.Atoi(idStr)
|
|
if err != nil {
|
|
httpError(w, werr.Wrap(err).Log())
|
|
return
|
|
}
|
|
|
|
fromStr := r.URL.Query().Get("f")
|
|
from, err := time.ParseInLocation("2006-01-02 15:04:05", fromStr[0:min(19, len(fromStr))], smonConfig.Timezone())
|
|
if err != nil {
|
|
httpError(w, werr.Wrap(err).Log())
|
|
return
|
|
}
|
|
|
|
toStr := r.URL.Query().Get("t")
|
|
to, err := time.ParseInLocation("2006-01-02 15:04:05", toStr[0:min(19, len(toStr))], smonConfig.Timezone())
|
|
if err != nil {
|
|
httpError(w, werr.Wrap(err).Log())
|
|
return
|
|
}
|
|
|
|
values, err := DatapointValues(id, from, to)
|
|
if err != nil {
|
|
httpError(w, werr.Wrap(err).Log())
|
|
return
|
|
}
|
|
|
|
j, _ := json.Marshal(values)
|
|
w.Write(j)
|
|
} // }}}
|
|
|
|
func pageTriggers(w http.ResponseWriter, r *http.Request, _ *session.T) { // {{{
|
|
areas, err := TriggersRetrieve()
|
|
if err != nil {
|
|
httpError(w, werr.Wrap(err).Log())
|
|
return
|
|
}
|
|
|
|
sort.SliceStable(areas, func(i, j int) bool {
|
|
return areas[i].Name < areas[j].Name
|
|
})
|
|
|
|
page := Page{
|
|
LAYOUT: "main",
|
|
PAGE: "triggers",
|
|
CONFIG: smonConfig.Settings,
|
|
Data: map[string]any{
|
|
"Areas": areas,
|
|
},
|
|
}
|
|
|
|
page.Render(w, r)
|
|
} // }}}
|
|
func actionTriggerCreate(w http.ResponseWriter, r *http.Request, _ *session.T) { // {{{
|
|
name := r.PathValue("name")
|
|
sectionIDStr := r.PathValue("sectionID")
|
|
sectionID, err := strconv.Atoi(sectionIDStr)
|
|
if err != nil {
|
|
httpError(w, werr.Wrap(err).WithData(sectionIDStr).Log())
|
|
return
|
|
}
|
|
|
|
t, err := TriggerCreate(sectionID, name)
|
|
if err != nil {
|
|
httpError(w, werr.Wrap(err).WithData(struct {
|
|
SectionID int
|
|
Name string
|
|
}{
|
|
sectionID,
|
|
name,
|
|
}).Log())
|
|
return
|
|
}
|
|
|
|
resp := struct {
|
|
OK bool
|
|
Trigger Trigger
|
|
}{
|
|
true,
|
|
t,
|
|
}
|
|
j, _ := json.Marshal(resp)
|
|
w.Header().Add("Content-Type", "application/json")
|
|
w.Write(j)
|
|
} // }}}
|
|
func pageTriggerEdit(w http.ResponseWriter, r *http.Request, _ *session.T) { // {{{
|
|
idStr := r.PathValue("id")
|
|
id, err := strconv.Atoi(idStr)
|
|
if err != nil {
|
|
httpError(w, werr.Wrap(err).Log())
|
|
return
|
|
}
|
|
|
|
// Creating a new trigger uses the edit function.
|
|
// ID == 0 - create a new trigger.
|
|
// ID > 0 - edit existing trigger.
|
|
var trigger Trigger
|
|
if id > 0 {
|
|
trigger, err = TriggerRetrieve(id)
|
|
if err != nil {
|
|
httpError(w, werr.Wrap(err).Log())
|
|
return
|
|
}
|
|
} else {
|
|
// A new trigger needs to know which section it belongs to.
|
|
sectionIDStr := r.PathValue("sectionID")
|
|
if sectionIDStr != "" {
|
|
trigger.SectionID, err = strconv.Atoi(sectionIDStr)
|
|
if err != nil {
|
|
httpError(w, werr.Wrap(err).Log())
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
datapoints := make(map[string]Datapoint)
|
|
for _, dpname := range trigger.Datapoints {
|
|
dp, err := DatapointRetrieve(0, dpname)
|
|
if err != nil {
|
|
httpError(w, werr.Wrap(err).Log())
|
|
return
|
|
}
|
|
dp.LastDatapointValue.TemplateValue = dp.LastDatapointValue.Value()
|
|
datapoints[dpname] = dp
|
|
}
|
|
|
|
page := Page{
|
|
LAYOUT: "main",
|
|
PAGE: "trigger_edit",
|
|
MENU: "triggers",
|
|
CONFIG: smonConfig.Settings,
|
|
Label: "Trigger",
|
|
Icon: "triggers",
|
|
Data: map[string]any{
|
|
"Trigger": trigger,
|
|
"Datapoints": datapoints,
|
|
},
|
|
}
|
|
|
|
page.Render(w, r)
|
|
} // }}}
|
|
func actionTriggerDatapointAdd(w http.ResponseWriter, r *http.Request, _ *session.T) { // {{{
|
|
triggerID := r.PathValue("id")
|
|
dpName := r.PathValue("datapointName")
|
|
|
|
id, err := strconv.Atoi(triggerID)
|
|
if err != nil {
|
|
httpError(w, werr.Wrap(err).Log())
|
|
return
|
|
}
|
|
|
|
var trigger Trigger
|
|
if id > 0 {
|
|
trigger, err = TriggerRetrieve(id)
|
|
if err != nil {
|
|
httpError(w, werr.Wrap(err).Log())
|
|
return
|
|
}
|
|
}
|
|
|
|
if !slices.Contains(trigger.Datapoints, dpName) {
|
|
trigger.Datapoints = append(trigger.Datapoints, dpName)
|
|
}
|
|
err = trigger.Update()
|
|
if err != nil {
|
|
httpError(w, werr.Wrap(err).Log())
|
|
return
|
|
}
|
|
|
|
j, _ := json.Marshal(struct{ OK bool }{OK: true})
|
|
w.Header().Add("Content-Type", "application/json")
|
|
w.Write(j)
|
|
} // }}}
|
|
func actionTriggerUpdate(w http.ResponseWriter, r *http.Request, _ *session.T) { // {{{
|
|
idStr := r.PathValue("id")
|
|
id, err := strconv.Atoi(idStr)
|
|
if err != nil {
|
|
httpError(w, werr.Wrap(err).Log())
|
|
return
|
|
}
|
|
|
|
var trigger Trigger
|
|
if id > 0 {
|
|
trigger, err = TriggerRetrieve(id)
|
|
if err != nil {
|
|
httpError(w, werr.Wrap(err).Log())
|
|
return
|
|
}
|
|
} else {
|
|
trigger.SectionID, err = strconv.Atoi(r.FormValue("sectionID"))
|
|
if err != nil {
|
|
httpError(w, werr.Wrap(err).Log())
|
|
return
|
|
}
|
|
}
|
|
|
|
trigger.Name = r.FormValue("name")
|
|
trigger.Expression = r.FormValue("expression")
|
|
trigger.Datapoints = r.Form["datapoints[]"]
|
|
err = trigger.Update()
|
|
if err != nil {
|
|
httpError(w, werr.Wrap(err).Log())
|
|
return
|
|
}
|
|
|
|
w.Header().Add("Location", "/triggers")
|
|
w.WriteHeader(302)
|
|
} // }}}
|
|
func actionTriggerRun(w http.ResponseWriter, r *http.Request, _ *session.T) { // {{{
|
|
idStr := r.PathValue("id")
|
|
id, err := strconv.Atoi(idStr)
|
|
if err != nil {
|
|
httpError(w, werr.Wrap(err).Log())
|
|
return
|
|
}
|
|
|
|
var trigger Trigger
|
|
trigger, err = TriggerRetrieve(id)
|
|
if err != nil {
|
|
httpError(w, werr.Wrap(err).Log())
|
|
return
|
|
}
|
|
|
|
expr, _ := io.ReadAll(r.Body)
|
|
trigger.Expression = string(expr)
|
|
|
|
resp := struct {
|
|
OK bool
|
|
Output any
|
|
}{
|
|
OK: true,
|
|
}
|
|
resp.Output, err = trigger.Run()
|
|
if err != nil {
|
|
werr.Wrap(err).Log()
|
|
httpError(w, err)
|
|
return
|
|
}
|
|
|
|
j, _ := json.Marshal(resp)
|
|
w.Header().Add("Content-Type", "application/json")
|
|
w.Write(j)
|
|
} // }}}
|
|
func actionTriggerDelete(w http.ResponseWriter, r *http.Request, _ *session.T) { // {{{
|
|
idStr := r.PathValue("id")
|
|
id, err := strconv.Atoi(idStr)
|
|
if err != nil {
|
|
httpError(w, werr.Wrap(err).Log())
|
|
return
|
|
}
|
|
|
|
err = TriggerDelete(id)
|
|
if err != nil {
|
|
httpError(w, werr.Wrap(err).Log())
|
|
return
|
|
}
|
|
|
|
w.Header().Add("Location", "/triggers")
|
|
w.WriteHeader(302)
|
|
} // }}}
|
|
|
|
func pageConfiguration(w http.ResponseWriter, r *http.Request, _ *session.T) { // {{{
|
|
areas, err := AreaRetrieve()
|
|
if err != nil {
|
|
httpError(w, werr.Wrap(err).Log())
|
|
return
|
|
}
|
|
|
|
sort.SliceStable(areas, func(i, j int) bool {
|
|
return areas[i].Name < areas[j].Name
|
|
})
|
|
|
|
page := Page{
|
|
LAYOUT: "main",
|
|
PAGE: "configuration",
|
|
CONFIG: smonConfig.Settings,
|
|
Data: map[string]any{
|
|
"Areas": areas,
|
|
"NotificationServices": notificationManager.Services(),
|
|
"AvailableServices": notification.AvailableServices(),
|
|
},
|
|
}
|
|
|
|
page.Render(w, r)
|
|
} // }}}
|
|
func actionConfigurationTheme(w http.ResponseWriter, r *http.Request, _ *session.T) { // {{{
|
|
theme := r.FormValue("theme")
|
|
err := smonConfig.SetTheme(theme)
|
|
if err != nil {
|
|
httpError(w, werr.Wrap(err).Log())
|
|
return
|
|
}
|
|
|
|
w.Header().Add("Location", "/configuration")
|
|
w.WriteHeader(302)
|
|
} // }}}
|
|
func actionConfigurationTimezone(w http.ResponseWriter, r *http.Request, _ *session.T) { // {{{
|
|
timezone := r.FormValue("timezone")
|
|
|
|
_, err := time.LoadLocation(timezone)
|
|
if err != nil {
|
|
pageError(w, "/configuration", werr.Wrap(err).Log())
|
|
return
|
|
}
|
|
|
|
err = smonConfig.SetTimezone(timezone)
|
|
if err != nil {
|
|
pageError(w, "/configuration", werr.Wrap(err).Log())
|
|
return
|
|
}
|
|
|
|
w.Header().Add("Location", "/configuration")
|
|
w.WriteHeader(302)
|
|
} // }}}
|
|
func pageConfigurationNotification(w http.ResponseWriter, r *http.Request, _ *session.T) { // {{{
|
|
// This function is either receiving a type when creating a new service,
|
|
// or a prio when editing an existing.
|
|
notificationType := r.URL.Query().Get("type")
|
|
prioStr := r.URL.Query().Get("prio")
|
|
|
|
var service notification.Service
|
|
if notificationType != "" {
|
|
// Create a new instance of the selected notification type.
|
|
service = notification.NewInstance(notificationType)
|
|
logger.Info("FOO", "service", fmt.Sprintf("%p %#v\n", service, service))
|
|
} else {
|
|
// Find the existing service for editing.
|
|
prio, err := strconv.Atoi(prioStr)
|
|
if err != nil {
|
|
httpError(w, werr.Wrap(err).Log())
|
|
return
|
|
}
|
|
service = *notificationManager.GetService(prio)
|
|
}
|
|
|
|
if service == nil {
|
|
data := struct {
|
|
typ string
|
|
prio string
|
|
}{notificationType, prioStr}
|
|
err := fmt.Errorf("Invalid service")
|
|
pageError(w, "/configuration", werr.Wrap(err).WithData(data).Log())
|
|
return
|
|
}
|
|
|
|
// Make it easier for the user by initiating the prio field
|
|
// for new notifications to the highest prio + 1.
|
|
if notificationType != "" {
|
|
prio := 0
|
|
for _, svc := range notificationManager.Services() {
|
|
prio = max(prio, svc.GetPrio()+1)
|
|
}
|
|
service.SetPrio(prio)
|
|
}
|
|
|
|
page := Page{
|
|
LAYOUT: "notification",
|
|
PAGE: "notification/" + strings.ToLower(service.GetType()),
|
|
CONFIG: smonConfig.Settings,
|
|
Data: map[string]any{
|
|
"Service": service,
|
|
},
|
|
}
|
|
|
|
page.Render(w, r)
|
|
} // }}}
|
|
func actionConfigurationNotificationUpdate(w http.ResponseWriter, r *http.Request, _ *session.T) { // {{{
|
|
prioStr := r.PathValue("prio")
|
|
prio, err := strconv.Atoi(prioStr)
|
|
if err != nil {
|
|
pageError(w, "/configuration", werr.Wrap(err).Log())
|
|
return
|
|
}
|
|
|
|
// prio -1 means a new service, not existing in database.
|
|
var svc *notification.Service
|
|
if prio == -1 {
|
|
emptyService := notification.NewInstance(r.PostFormValue("type"))
|
|
svc = &emptyService
|
|
} else {
|
|
svc = notificationManager.GetService(prio)
|
|
if svc == nil {
|
|
pageError(w, "/configuration", werr.New("Service with prio %d not found", prio).Log())
|
|
return
|
|
}
|
|
}
|
|
|
|
// The service is given all data to give it a chance to
|
|
// validate and throwing errors.
|
|
err = r.ParseForm()
|
|
if err != nil {
|
|
pageError(w, "/configuration", werr.Wrap(err).Log())
|
|
return
|
|
}
|
|
|
|
err = (*svc).Update(r.PostForm)
|
|
if err != nil {
|
|
pageError(w, "/configuration", werr.Wrap(err).Log())
|
|
return
|
|
}
|
|
|
|
var created bool
|
|
created, err = UpdateNotificationService(*svc)
|
|
if err != nil {
|
|
pageError(w, "/configuration", werr.Wrap(err).Log())
|
|
return
|
|
}
|
|
|
|
(*svc).Commit()
|
|
|
|
if created {
|
|
notificationManager.AddService(*svc)
|
|
}
|
|
|
|
w.Header().Add("Location", "/configuration")
|
|
w.WriteHeader(302)
|
|
} // }}}
|
|
func actionConfigurationNotificationDelete(w http.ResponseWriter, r *http.Request, _ *session.T) { // {{{
|
|
prioStr := r.PathValue("prio")
|
|
prio, err := strconv.Atoi(prioStr)
|
|
if err != nil {
|
|
pageError(w, "/configuration", werr.Wrap(err).Log())
|
|
return
|
|
}
|
|
|
|
DeleteNotificationService(prio)
|
|
notificationManager.RemoveService(prio)
|
|
|
|
w.Header().Add("Location", "/configuration")
|
|
w.WriteHeader(302)
|
|
} // }}}
|
|
|
|
func pageNotifications(w http.ResponseWriter, r *http.Request, _ *session.T) { // {{{
|
|
var err error
|
|
|
|
// GET parameters.
|
|
var timeFrom, timeTo time.Time
|
|
lastWeek := time.Now().Add(time.Duration(-7 * 24 * time.Hour))
|
|
|
|
timeFrom, err = parseHTMLDateTime(r.URL.Query().Get("f"), lastWeek)
|
|
if err != nil {
|
|
httpError(w, werr.Wrap(err).Log())
|
|
return
|
|
}
|
|
|
|
timeTo, err = parseHTMLDateTime(r.URL.Query().Get("t"), time.Now())
|
|
if err != nil {
|
|
httpError(w, werr.Wrap(err).Log())
|
|
return
|
|
}
|
|
|
|
presetTimeInterval(time.Hour, r.URL.Query().Get("preset"), &timeFrom, &timeTo)
|
|
|
|
// Apply an optionally set offset (in seconds).
|
|
timeFrom = applyTimeOffset(timeFrom, time.Second, r.URL.Query().Get("offset-time"))
|
|
timeTo = applyTimeOffset(timeTo, time.Second, r.URL.Query().Get("offset-time"))
|
|
|
|
nss, err := notificationsSent(timeFrom, timeTo)
|
|
if err != nil {
|
|
pageError(w, "/", werr.Wrap(err).Log())
|
|
}
|
|
|
|
page := Page{
|
|
LAYOUT: "main",
|
|
PAGE: "notifications",
|
|
CONFIG: smonConfig.Settings,
|
|
Data: map[string]any{
|
|
"Notifications": nss,
|
|
"TimeFrom": timeFrom.Format("2006-01-02T15:04:05"),
|
|
"TimeTo": timeTo.Format("2006-01-02T15:04:05"),
|
|
},
|
|
}
|
|
|
|
page.Render(w, r)
|
|
} // }}}
|