Added basic Python exection wrapping

This commit is contained in:
Daan Selen
2025-05-26 16:24:06 +02:00
parent 42c0392a20
commit 04fafeef0b
14 changed files with 253 additions and 16 deletions

View File

@@ -8,6 +8,8 @@ The way to accomplish this is to create a tracked task list, and keep track of i
Go(lang) backend server which exposes an HTTP API which can be used to add tasks to the process.<br>
Python executor/runner which actually executes the commands, Python was chosen because of the availability of: [LibMeshCtrl Python Library](https://pypi.org/project/libmeshctrl/).<br>
Create a python virtual environment inside the `runner` folder.
# JSON Templates:
TokenBody:

38
runner/modules/connect.py Normal file
View File

@@ -0,0 +1,38 @@
import meshctrl
from json import dumps
class connect:
@staticmethod
async def connect(hostname: str, username: str, password: str) -> meshctrl.Session:
session = meshctrl.Session(
hostname,
user=username,
password=password
)
await session.initialized.wait()
return session
@staticmethod
async def list_online(session: meshctrl.Session,
mode: str = "online") -> dict: # Default is return online devices, but function can also return the offline devices if specified.
raw_device_list = await session.list_devices()
complete_list = {}
complete_list["online_devices"] = []
complete_list["offline_devices"] = []
for raw_device in raw_device_list:
if raw_device.connected:
complete_list["online_devices"].append({
"name": raw_device.name,
"nodeid": raw_device.nodeid
})
else:
complete_list["offline_devices"].append({
"name": raw_device.name,
"nodeid": raw_device.nodeid
})
complete_list["total_devices"] = len(complete_list["online_devices"]) + len(complete_list["offline_devices"])
return complete_list

View File

@@ -0,0 +1,34 @@
#!/bin/python3
#
#
from configparser import ConfigParser
import os
#
#
#
class utilities:
@staticmethod
def load_config(segment: str = 'ghostrunner') -> dict:
'''
Function that loads the segment from the config.conf (by default) file and returns the it in a dict.
'''
conf_file = "./conf/ghostserver.conf"
if not os.path.exists(conf_file):
print(f'Missing config file {conf_file}. Provide an alternative path.')
os._exit(1)
config = ConfigParser()
try:
config.read(conf_file)
except Exception as err:
print(f"Error reading configuration file '{conf_file}': {err}")
os._exit(1)
if segment not in config:
print(f'Segment "{segment}" not found in config file {conf_file}.')
os._exit(1)
return dict(config[segment])

View File

@@ -1,9 +1,44 @@
#!/bin/python3
import meshctrl
import argparse
import asyncio
from json import dumps
def main():
print("Hello World")
from modules.connect import connect
from modules.utilities import utilities
def cmd_flags() -> argparse.Namespace:
parser = argparse.ArgumentParser(description="Process command-line arguments")
parser.add_argument("-lo", "--list-online", action='store_true', help="Specify if the program needs to list online devices.")
parser.add_argument("-rc", "--run", action='store_true', help="Make the program run a command.")
parser.add_argument("--command", type=str, help="Specify the actual command that is going to run.")
parser.add_argument("--nodeids", type=str, help="Specify which nodes the command is going to be run on.")
parser.add_argument("-i", "--indent", action='store_true', help="Specify whether the output needs to be indented.")
return parser.parse_args()
async def main():
args = cmd_flags()
credentials = utilities.load_config()
session = await connect.connect(credentials["hostname"],
credentials["username"],
credentials["password"])
if args.list_online:
online_devices = await connect.list_online(session)
if args.indent:
print(dumps(online_devices,indent=4))
else:
print(dumps(online_devices))
else:
print("No LO flag.")
if args.run:
print("run command")
await session.close()
if __name__ == "__main__":
main()
asyncio.run(main())

Binary file not shown.

View File

@@ -7,6 +7,7 @@ import (
"ghostrunner-server/modules/restapi"
"ghostrunner-server/modules/timekeeper"
"ghostrunner-server/modules/utilities"
"ghostrunner-server/modules/wrapper"
"log"
)
@@ -29,5 +30,8 @@ func main() {
log.Println(utilities.InfoTag, "Components should have started.")
log.Println(utilities.InfoTag, "Letting TimeKeeper take over...")
log.Println(utilities.InfoTag, fmt.Sprintf("Interval set at: %d seconds.", cfg.Interval))
wrapper.PyListOnline(cfg.PyVenvName)
timekeeper.KeepTime(cfg.Interval)
}

View File

@@ -8,10 +8,12 @@ import (
"ghostrunner-server/modules/encrypt"
"ghostrunner-server/modules/utilities"
"log"
"strings"
)
func insertAdminToken(token string, hmacKey []byte) error {
var adminTokenName string = "Self-Generated Admin Token"
adminTokenName = strings.ToLower(adminTokenName)
hashedToken := encrypt.CreateHMAC(token, hmacKey)
_, err := db.Exec(declStat.AdminTokenCreate, adminTokenName, hashedToken)
@@ -69,6 +71,26 @@ func RetrieveTokens() []string {
return tokens
}
func RetrieveTokenNames() []string {
rows, err := db.Query(declStat.RetrieveTokenNames)
if err != nil {
log.Println(utilities.ErrTag, err)
}
defer rows.Close()
var tokenNames []string
for rows.Next() {
var singleTokenName string
err = rows.Scan(&singleTokenName)
if err != nil {
log.Println(utilities.ErrTag, err)
}
tokenNames = append(tokenNames, singleTokenName)
}
return tokenNames
}
func InsertTask(name, command string, nodeids []string, date, status string) error {
pNodeIds, err := json.Marshal(nodeids)
if err != nil {

View File

@@ -3,11 +3,12 @@ package database
type Statements struct {
SetupDatabase string
AdminTokenCreate string
GetTokenID string
CreateToken string
DeleteToken string
RetrieveTokens string
AdminTokenCreate string
GetTokenID string
CreateToken string
DeleteToken string
RetrieveTokens string
RetrieveTokenNames string
CreateTask string
DeleteTask string
@@ -44,6 +45,8 @@ var declStat = Statements{
name = ?;`,
RetrieveTokens: `
SELECT token FROM tokens`,
RetrieveTokenNames: `
SELECT name FROM tokens`,
CreateTask: `
INSERT INTO tasks (name, command, nodeids, creation, status)

View File

@@ -7,6 +7,7 @@ import (
"ghostrunner-server/modules/utilities"
"log"
"net/http"
"strings"
"time"
"slices"
@@ -114,9 +115,42 @@ func deleteTokenHandler(hmacKey []byte) http.HandlerFunc {
}
}
func listTokenHandler(hmacKey []byte) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
authHeader := r.Header.Get("Authorization")
if authHeader == "" {
http.Error(w, "Missing Authorization header", http.StatusUnauthorized)
return
}
const prefix = "Bearer "
if len(authHeader) <= len(prefix) || authHeader[:len(prefix)] != prefix {
http.Error(w, "Invalid Authorization header format", http.StatusUnauthorized)
return
}
tokenCandidate := authHeader[len(prefix):]
securedCandidate := encrypt.CreateHMAC(tokenCandidate, hmacKey)
if !generalAuth(w, securedCandidate) {
return
}
data := database.RetrieveTokenNames()
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(utilities.InfoResponse{
Status: http.StatusOK,
Message: "Succesfully Retrieved Tokens",
Data: data,
})
}
}
func createToken(tokenName string, hmacKey []byte) (string, error) {
randomString := utilities.GenRandString(64)
securedString := encrypt.CreateHMAC(randomString, hmacKey)
tokenName = strings.ToLower(tokenName)
if err := database.InsertToken(tokenName, securedString); err != nil {
return "", err
@@ -124,9 +158,8 @@ func createToken(tokenName string, hmacKey []byte) (string, error) {
return randomString, nil
}
func deleteToken(candToken string, hmacKey []byte) error {
securedToken := encrypt.CreateHMAC(candToken, hmacKey)
return database.RemoveToken(securedToken)
func deleteToken(tokenName string, hmacKey []byte) error {
return database.RemoveToken(tokenName)
}
/*
@@ -212,6 +245,7 @@ func listTasksHandler(hmacKey []byte) http.HandlerFunc {
func createTask(taskName, command string, nodeids []string) error {
creationDate := time.Now().Format("02-01-2006 15:04:05")
creationStatus := constCreationStatus
taskName = strings.ToLower(taskName)
return database.InsertTask(taskName, command, nodeids, creationDate, creationStatus)
}

View File

@@ -49,7 +49,7 @@ func InitApiServer(cfg utilities.ConfigStruct, hmacKey []byte) {
}
defer srv.Close()
}()
//utilities.ConsoleLog("Successfully started the GhostServer goroutine.")
log.Println(utilities.InfoTag, "Successfully started the GhostServer goroutine at:", cfg.Address)
}
func createRouter(hmacKey []byte) *mux.Router {
@@ -59,6 +59,7 @@ func createRouter(hmacKey []byte) *mux.Router {
r.HandleFunc("/token/create", createTokenHandler(hmacKey)).Methods("POST")
r.HandleFunc("/token/delete", deleteTokenHandler(hmacKey)).Methods("DELETE")
r.HandleFunc("/token/list", listTokenHandler(hmacKey)).Methods("GET")
r.HandleFunc("/task/create", createTaskHandler(hmacKey)).Methods("POST")
r.HandleFunc("/task/delete", deleteTaskHandler(hmacKey)).Methods("DELETE")

View File

@@ -7,7 +7,8 @@ import (
)
const (
configSection = "ghostserver"
serverSection = "ghostserver"
runnerSection = "ghostrunner"
)
func ReadConf(configPath string) ConfigStruct {
@@ -16,7 +17,7 @@ func ReadConf(configPath string) ConfigStruct {
log.Println(ErrTag, err)
}
section := inidata.Section(configSection)
section := inidata.Section(serverSection)
var config ConfigStruct
@@ -43,5 +44,13 @@ func ReadConf(configPath string) ConfigStruct {
log.Println(ErrTag, err)
}
section = inidata.Section(runnerSection)
config.MeshHostname = section.Key("hostname").String()
config.MeshUsername = section.Key("username").String()
config.MeshPassword = section.Key("password").String()
config.PyVenvName = section.Key("python_venv_name").String()
return config
}

View File

@@ -8,6 +8,11 @@ type ConfigStruct struct {
ApiCertFile string
ApiKeyFile string
Interval int
MeshHostname string
MeshUsername string
MeshPassword string
PyVenvName string
}
type InfoResponse struct {
@@ -41,3 +46,15 @@ type TaskBody struct {
AuthToken string `json:"authtoken"`
Details TaskData `json:"details"`
}
// Python wrapper objects.
type Device struct {
Name string `json:"name"`
NodeID string `json:"nodeid"`
}
type PyOnlineDevices struct {
OnlineDevices []Device `json:"online_devices"`
TotalDevices int `json:"total_devices"`
}

View File

@@ -0,0 +1,31 @@
package wrapper
import (
"encoding/json"
"fmt"
"ghostrunner-server/modules/utilities"
"log"
"os"
"os/exec"
)
func PyListOnline(venvName string) {
pythonBin := fmt.Sprintf("./../runner/%s/bin/python", venvName)
cmd := exec.Command(pythonBin, "./../runner/runner.py", "-lo")
data, err := cmd.CombinedOutput()
if err != nil {
log.Println(utilities.ErrTag, err, data)
cwd, _ := os.Getwd()
log.Println("Working directory:", cwd)
return
}
var status utilities.PyOnlineDevices
if err := json.Unmarshal(data, &status); err != nil {
fmt.Println("Error unmarshaling:", err)
return
}
fmt.Printf("Parsed Struct: %+v\n", status)
}

View File

@@ -8,4 +8,11 @@ secure =
api_cert_file =
api_key_file =
interval =
interval =
[ghostrunner]
hostname =
username =
password =
python_venv_name = venv