diff --git a/README.md b/README.md
index 1c91943..8cdff24 100644
--- a/README.md
+++ b/README.md
@@ -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.
Python executor/runner which actually executes the commands, Python was chosen because of the availability of: [LibMeshCtrl Python Library](https://pypi.org/project/libmeshctrl/).
+Create a python virtual environment inside the `runner` folder.
+
# JSON Templates:
TokenBody:
diff --git a/runner/modules/connect.py b/runner/modules/connect.py
new file mode 100644
index 0000000..22fcf52
--- /dev/null
+++ b/runner/modules/connect.py
@@ -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
\ No newline at end of file
diff --git a/runner/modules/utilities.py b/runner/modules/utilities.py
new file mode 100644
index 0000000..0305936
--- /dev/null
+++ b/runner/modules/utilities.py
@@ -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])
diff --git a/runner/runner.py b/runner/runner.py
index adf3763..3f0cb6d 100644
--- a/runner/runner.py
+++ b/runner/runner.py
@@ -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()
\ No newline at end of file
+ asyncio.run(main())
\ No newline at end of file
diff --git a/server/ghostrunner-server b/server/ghostrunner-server
index eab52f8..53c2120 100755
Binary files a/server/ghostrunner-server and b/server/ghostrunner-server differ
diff --git a/server/src/main.go b/server/src/main.go
index 13c2f20..5c36a78 100644
--- a/server/src/main.go
+++ b/server/src/main.go
@@ -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)
}
diff --git a/server/src/modules/database/handlers.go b/server/src/modules/database/handlers.go
index 7d435c9..ae72d15 100644
--- a/server/src/modules/database/handlers.go
+++ b/server/src/modules/database/handlers.go
@@ -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 {
diff --git a/server/src/modules/database/statements.go b/server/src/modules/database/statements.go
index 8109d22..4d3f7b4 100644
--- a/server/src/modules/database/statements.go
+++ b/server/src/modules/database/statements.go
@@ -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)
diff --git a/server/src/modules/restapi/handlers.go b/server/src/modules/restapi/handlers.go
index 451c741..9b868ed 100644
--- a/server/src/modules/restapi/handlers.go
+++ b/server/src/modules/restapi/handlers.go
@@ -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)
}
diff --git a/server/src/modules/restapi/init.go b/server/src/modules/restapi/init.go
index 0a35d51..bcbc7ed 100644
--- a/server/src/modules/restapi/init.go
+++ b/server/src/modules/restapi/init.go
@@ -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")
diff --git a/server/src/modules/utilities/confread.go b/server/src/modules/utilities/confread.go
index c471b26..0c30eea 100644
--- a/server/src/modules/utilities/confread.go
+++ b/server/src/modules/utilities/confread.go
@@ -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
}
diff --git a/server/src/modules/utilities/structs.go b/server/src/modules/utilities/structs.go
index ee632b3..1cc60d5 100644
--- a/server/src/modules/utilities/structs.go
+++ b/server/src/modules/utilities/structs.go
@@ -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"`
+}
diff --git a/server/src/modules/wrapper/python.go b/server/src/modules/wrapper/python.go
new file mode 100644
index 0000000..9839b61
--- /dev/null
+++ b/server/src/modules/wrapper/python.go
@@ -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)
+}
diff --git a/server/templates/ghostrunner.conf.template b/server/templates/ghostserver.conf.template
similarity index 54%
rename from server/templates/ghostrunner.conf.template
rename to server/templates/ghostserver.conf.template
index da68761..90505ce 100644
--- a/server/templates/ghostrunner.conf.template
+++ b/server/templates/ghostserver.conf.template
@@ -8,4 +8,11 @@ secure =
api_cert_file =
api_key_file =
-interval =
\ No newline at end of file
+interval =
+
+[ghostrunner]
+hostname =
+username =
+password =
+
+python_venv_name = venv
\ No newline at end of file