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