Added basic Python exection wrapping
This commit is contained in:
@@ -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
38
runner/modules/connect.py
Normal 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
|
||||
34
runner/modules/utilities.py
Normal file
34
runner/modules/utilities.py
Normal 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])
|
||||
@@ -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.
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
|
||||
31
server/src/modules/wrapper/python.go
Normal file
31
server/src/modules/wrapper/python.go
Normal 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)
|
||||
}
|
||||
@@ -8,4 +8,11 @@ secure =
|
||||
api_cert_file =
|
||||
api_key_file =
|
||||
|
||||
interval =
|
||||
interval =
|
||||
|
||||
[ghostrunner]
|
||||
hostname =
|
||||
username =
|
||||
password =
|
||||
|
||||
python_venv_name = venv
|
||||
Reference in New Issue
Block a user