From 6d05152ab237ef4c78e503ed5f01ac9a30478579 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Magnus=20=C3=85hall?= Date: Thu, 7 Aug 2025 23:26:15 +0200 Subject: [PATCH] Schedule script run --- script.go | 109 +++++++++++++++++++++++++++++++++++++++++- sql/0013.sql | 20 ++++++++ static/css/main.css | 6 ++- static/js/app.mjs | 8 +++- static/less/main.less | 5 +- webserver.go | 21 ++++++++ 6 files changed, 162 insertions(+), 7 deletions(-) create mode 100644 sql/0013.sql diff --git a/script.go b/script.go index 6f54d03..fa24d38 100644 --- a/script.go +++ b/script.go @@ -6,6 +6,9 @@ import ( "github.com/jmoiron/sqlx" // Standard + "crypto/md5" + "database/sql" + "encoding/hex" "encoding/json" "strings" "time" @@ -26,6 +29,17 @@ type Hook struct { SSH 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{} @@ -146,11 +160,40 @@ func SearchScripts(search string) (scripts []Script, err error) { // {{{ return } // }}} -func HookScript(nodeID, scriptID int) (err error) {// {{{ +func HookScript(nodeID, scriptID int) (err error) { // {{{ _, err = db.Exec(`INSERT INTO hook(node_id, script_id, ssh) VALUES($1, $2, '')`, nodeID, scriptID) return -}// }}} +} // }}} +func GetHook(hookID int) (hook Hook, err error) { // {{{ + row := db.QueryRow(` + SELECT + to_json(res) + FROM ( + SELECT + h.id, + h.ssh, + (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) { // {{{ _, err = db.Exec(`UPDATE hook SET ssh=$2 WHERE id=$1`, hook.ID, strings.TrimSpace(hook.SSH)) if err != nil { @@ -167,3 +210,65 @@ func DeleteHook(hookID int) (err error) { // {{{ } 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, 0, true) + if err != nil { + err = werr.Wrap(err) + return + } + + nodeData, err := json.Marshal(topNode) + if err != nil { + err = werr.Wrap(err) + return + } + + _, err = db.Exec(`INSERT INTO execution(script_log_id, data, ssh) VALUES($1, $2, $3)`, scriptLogID, nodeData, hook.SSH) + if err != nil { + err = werr.Wrap(err) + return + } + 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 +} // }}} diff --git a/sql/0013.sql b/sql/0013.sql new file mode 100644 index 0000000..2d37b81 --- /dev/null +++ b/sql/0013.sql @@ -0,0 +1,20 @@ +CREATE TABLE public.script_log ( + id serial NOT NULL, + md5sum char(32) NOT NULL, + source text NOT NULL, + CONSTRAINT script_log_pk PRIMARY KEY (id) +); + +CREATE TABLE public.execution ( + id serial NOT NULL, + time_start timestamptz NULL, + script_log_id int4 NOT NULL, + "data" jsonb NOT NULL, + ssh varchar NOT NULL, + time_end timestamptz NULL, + output_stdout text NULL, + output_stderr text NULL, + exitcode int NULL, + CONSTRAINT execution_pk PRIMARY KEY (id), + CONSTRAINT execution_script_log_fk FOREIGN KEY (script_log_id) REFERENCES public.script_log(id) ON DELETE RESTRICT ON UPDATE RESTRICT +); diff --git a/static/css/main.css b/static/css/main.css index 19ace99..3c45bcf 100644 --- a/static/css/main.css +++ b/static/css/main.css @@ -260,7 +260,7 @@ select:focus { } #script-hooks .scripts-grid { display: grid; - grid-template-columns: repeat(3, min-content); + grid-template-columns: repeat(4, min-content); align-items: center; grid-gap: 2px 0px; } @@ -276,9 +276,11 @@ select:focus { font-weight: bold; margin-top: 8px; } -#script-hooks .scripts-grid .script-unhook img { +#script-hooks .scripts-grid .script-unhook img, +#script-hooks .scripts-grid .script-run img { display: block; height: 24px; + cursor: pointer; } #script-hooks .scripts-grid .script-name, #script-hooks .scripts-grid .script-ssh { diff --git a/static/js/app.mjs b/static/js/app.mjs index b1cde57..d6d5294 100644 --- a/static/js/app.mjs +++ b/static/js/app.mjs @@ -1067,7 +1067,7 @@ class ScriptHooks extends Component { NodeID: _app.currentNode.ID, ScriptID: script.ID, }) - .then(()=>mbus.dispatch('NODE_HOOKED', _app.currentNode.ID)) + .then(() => mbus.dispatch('NODE_HOOKED', _app.currentNode.ID)) .catch(err => showError(err)) }// }}} } @@ -1085,12 +1085,14 @@ class ScriptHook extends Component {
${this.hook.Script.Name}
+
` this.element_ssh = tmpl.content.querySelector('.script-ssh') this.element_ssh.innerText = this.hook.SSH tmpl.content.querySelector('.script-ssh').addEventListener('click', () => this.update()) tmpl.content.querySelector('.script-unhook').addEventListener('click', () => this.delete()) + tmpl.content.querySelector('.script-run').addEventListener('click', () => this.run()) return tmpl.content }// }}} @@ -1138,6 +1140,10 @@ class ScriptHook extends Component { }) .catch(err => showError(err)) }// }}} + run() {// {{{ + _app.query(`/hooks/schedule/${this.hook.ID}`) + .catch(err => showError(err)) + }// }}} } class ScriptsList extends Component { diff --git a/static/less/main.less b/static/less/main.less index eebfc4c..8254d66 100644 --- a/static/less/main.less +++ b/static/less/main.less @@ -352,7 +352,7 @@ select:focus { } display: grid; - grid-template-columns: repeat(3, min-content); + grid-template-columns: repeat(4, min-content); align-items: center; grid-gap: 2px 0px; @@ -366,10 +366,11 @@ select:focus { margin-top: 8px; } - .script-unhook { + .script-unhook, .script-run { img { display: block; height: 24px; + cursor: pointer; } } diff --git a/webserver.go b/webserver.go index ba2d293..738122e 100644 --- a/webserver.go +++ b/webserver.go @@ -54,6 +54,7 @@ func initWebserver() (err error) { http.HandleFunc("/hooks/search", actionScriptsSearch) http.HandleFunc("/hooks/update", actionHookUpdate) http.HandleFunc("/hooks/delete/{hookID}", actionHookDelete) + http.HandleFunc("/hooks/schedule/{hookID}", actionHookSchedule) err = http.ListenAndServe(address, nil) return @@ -730,5 +731,25 @@ func actionHookDelete(w http.ResponseWriter, r *http.Request) { // {{{ j, _ := json.Marshal(out) w.Write(j) } // }}} +func actionHookSchedule(w http.ResponseWriter, r *http.Request) { // {{{ + hookID := 0 + hookIDStr := r.PathValue("hookID") + hookID, _ = strconv.Atoi(hookIDStr) + + err := ScheduleHook(hookID) + if err != nil { + err = werr.Wrap(err) + httpError(w, err) + return + } + + out := struct { + OK bool + }{ + true, + } + j, _ := json.Marshal(out) + w.Write(j) +} // }}} // vim: foldmethod=marker