datagraph/script.go
2025-08-08 15:36:32 +02:00

283 lines
5.4 KiB
Go

package main
import (
// External
werr "git.gibonuddevalla.se/go/wrappederror"
"github.com/jmoiron/sqlx"
// Standard
"crypto/md5"
"database/sql"
"encoding/hex"
"encoding/json"
"strings"
"time"
)
type Script struct {
ID int
Group string
Name string
Source string
Updated time.Time
}
type Hook struct {
ID int
Node Node
Script Script
SSH string
Env map[string]string
}
func GetScript(scriptID int) (script Script, err error) { // {{{
row := db.QueryRowx(`SELECT * FROM script WHERE id=$1`, scriptID)
err = row.StructScan(&script)
if err != nil {
err = werr.Wrap(err)
return
}
return
} // }}}
func GetScripts() (scripts []Script, err error) { // {{{
scripts = []Script{}
var rows *sqlx.Rows
rows, err = db.Queryx(`
SELECT *
FROM script
ORDER BY
"group" ASC,
name ASC
`)
if err != nil {
err = werr.Wrap(err)
return
}
defer rows.Close()
for rows.Next() {
var script Script
err = rows.StructScan(&script)
if err != nil {
err = werr.Wrap(err)
return
}
scripts = append(scripts, script)
}
return
} // }}}
func UpdateScript(scriptID int, data []byte) (script Script, err error) { // {{{
err = json.Unmarshal(data, &script)
if err != nil {
err = werr.Wrap(err)
return
}
script.ID = scriptID
script.Group = strings.TrimSpace(script.Group)
script.Name = strings.TrimSpace(script.Name)
if script.Group == "" || script.Name == "" {
err = werr.New("Group and name must be provided.")
return
}
if script.ID < 1 {
row := db.QueryRowx(`
INSERT INTO script("group", "name", "source")
VALUES($1, $2, $3)
RETURNING
id
`,
strings.TrimSpace(script.Group),
strings.TrimSpace(script.Name),
script.Source,
)
err = row.Scan(&script.ID)
} else {
_, err = db.Exec(`
UPDATE script
SET
"group" = $2,
"name" = $3,
"source" = $4
WHERE
id = $1
`,
scriptID,
strings.TrimSpace(script.Group),
strings.TrimSpace(script.Name),
script.Source,
)
}
if err != nil {
err = werr.Wrap(err)
return
}
return
} // }}}
func DeleteScript(scriptID int) (err error) { // {{{
_, err = db.Exec(`DELETE FROM script WHERE id = $1`, scriptID)
if err != nil {
err = werr.Wrap(err)
return
}
return
} // }}}
func SearchScripts(search string) (scripts []Script, err error) { // {{{
scripts = []Script{}
row := db.QueryRow(`
SELECT
json_agg(script) AS scripts
FROM public.script
WHERE
name ILIKE $1
ORDER BY
"group" ASC,
name ASC
`,
search,
)
var jsonBody []byte
err = row.Scan(&jsonBody)
if err != nil {
err = werr.Wrap(err)
return
}
err = json.Unmarshal(jsonBody, &scripts)
if err != nil {
err = werr.Wrap(err)
return
}
return
} // }}}
func HookScript(nodeID, scriptID int) (err error) { // {{{
_, err = db.Exec(`INSERT INTO hook(node_id, script_id, ssh) VALUES($1, $2, '<host>')`, nodeID, scriptID)
return
} // }}}
func GetHook(hookID int) (hook Hook, err error) { // {{{
row := db.QueryRow(`
SELECT
to_json(res)
FROM (
SELECT
h.id,
h.ssh,
h.env,
(SELECT to_json(node) FROM node WHERE id = h.node_id) AS node,
(SELECT to_json(script) FROM script WHERE id = h.script_id) AS script
FROM hook h
WHERE
h.id = $1
) res
`,
hookID,
)
var data []byte
if err = row.Scan(&data); err != nil {
err = werr.Wrap(err)
}
err = json.Unmarshal(data, &hook)
if err != nil {
err = werr.Wrap(err)
}
return
} // }}}
func UpdateHook(hook Hook) (err error) { // {{{
j, _ := json.Marshal(hook.Env)
_, err = db.Exec(`UPDATE hook SET ssh=$2, env=$3 WHERE id=$1`, hook.ID, strings.TrimSpace(hook.SSH), j)
if err != nil {
err = werr.Wrap(err)
return
}
return
} // }}}
func DeleteHook(hookID int) (err error) { // {{{
_, err = db.Exec(`DELETE FROM hook WHERE id=$1`, hookID)
if err != nil {
err = werr.Wrap(err)
return
}
return
} // }}}
func ScheduleHook(hookID int) (err error) { // {{{
/* Script source is needed to preserve the full execution
against changing of the script at a later date.
To not waste db disk, the md5sum of the script is
calculated and the changed version of the script is just
stored once.
*/
hook, err := GetHook(hookID)
if err != nil {
err = werr.Wrap(err)
return
}
scriptLogID, err := ScriptPreservedID(hook.Script.Source)
if err != nil {
err = werr.Wrap(err)
return
}
// The node tree data is retrieved and sent as input to the script.
var topNode *Node
topNode, err = GetNodeTree(hook.Node.ID, 8, true)
if err != nil {
err = werr.Wrap(err)
return
}
nodeData, err := json.Marshal(topNode)
if err != nil {
err = werr.Wrap(err)
return
}
j, _ := json.Marshal(hook.Env)
_, err = db.Exec(`INSERT INTO execution(script_log_id, data, ssh, env) VALUES($1, $2, $3, $4)`, scriptLogID, nodeData, hook.SSH, j)
if err != nil {
err = werr.Wrap(err)
return
}
go func() {
scriptScheduler.EventQueue <- "SCRIPT_SCHEDULED"
}()
return
} // }}}
func ScriptPreservedID(source string) (id int, err error) { // {{{
sum := md5.Sum([]byte(source))
md5sum := hex.EncodeToString(sum[:])
row := db.QueryRow(`SELECT id FROM script_log WHERE md5sum=$1`, md5sum)
if err = row.Scan(&id); err == nil {
return
}
if err != sql.ErrNoRows {
err = werr.Wrap(err)
return
}
row = db.QueryRow(`INSERT INTO script_log(md5sum, source) VALUES($1, $2) RETURNING id`, md5sum, source)
err = row.Scan(&id)
if err != nil {
err = werr.Wrap(err)
}
return
} // }}}