diff --git a/README.md b/README.md deleted file mode 100644 index 8cdff24..0000000 --- a/README.md +++ /dev/null @@ -1,33 +0,0 @@ -# GhostRunner - -This project aims to create a way to schedule commands to be run as soon as possible when they offline initialy.
-The way to accomplish this is to create a tracked task list, and keep track of it has been successfully done. - -# Technical details. - -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: -```json -{ - "tokenname": "NewUsableToken", - "authtoken": "admintokengoeshereofcourse", - "selectedtoken": "thetokenyouwanttoremove" -} -``` -TaskBody: -```json -{ - "name": "DeployTask", - "authtoken": "abc123securetoken", - "data": { - "command": "deploy-app", - "nodeids": "node1,node2,node3" - } -} -``` \ No newline at end of file diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..11beb19 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,128 @@ +# GhostRunner + +This project aims to create a way to schedule commands to be run as soon as possible when they offline initialy.
+The way to accomplish this is to create a tracked task list, and keep track of it has been successfully done. + +# Technical details. + +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: + +Following is mock data. + +### `InfoResponse` + +```json +{ + "status": 200, + "message": "Request successful", + "data": { + "example": "This is some mock data" + } +} +``` + +--- + +### `TokenCreateDetails` + +```json +{ + "name": "DeploymentToken123" +} +``` + +--- + +### `TokenCreateBody` + +```json +{ + "authtoken": "abc123securetoken", + "details": { + "name": "DeploymentToken123" + } +} +``` + +--- + +### `TokenListBody` + +```json +{ + "authtoken": "abc123securetoken" +} +``` + +--- + +### `TaskData` + +```json +{ + "name": "UpdateScript", + "command": "sudo apt update && sudo apt upgrade -y", + "nodeids": ["node-001", "node-002", "node-003"], + "creation": "2025-05-27T10:15:30Z", + "status": "pending" +} +``` + +--- + +### `TaskBody` + +```json +{ + "authtoken": "abc123securetoken", + "details": { + "name": "UpdateScript", + "command": "sudo apt update && sudo apt upgrade -y", + "nodeids": ["node-001", "node-002", "node-003"], + "creation": "2025-05-27T10:15:30Z", + "status": "pending" + } +} +``` + +--- + +### `Device` + +```json +{ + "name": "RaspberryPi-01", + "nodeid": "node-001" +} +``` + +--- + +### `PyOnlineDevices` + +```json +{ + "online_devices": [ + { + "name": "RaspberryPi-01", + "nodeid": "node-001" + }, + { + "name": "Server-02", + "nodeid": "node-005" + } + ], + "offline_devices": [ + { + "name": "IoT-Gateway", + "nodeid": "node-003" + } + ], + "total_devices": 3 +} +``` \ No newline at end of file diff --git a/docs/RUNNER.md b/docs/RUNNER.md new file mode 100644 index 0000000..098e908 --- /dev/null +++ b/docs/RUNNER.md @@ -0,0 +1 @@ +# GhostRunner Python Runner module. \ No newline at end of file diff --git a/server/SERVER.md b/docs/SERVER.md similarity index 100% rename from server/SERVER.md rename to docs/SERVER.md diff --git a/runner/modules/connect.py b/runner/modules/connect.py index 22fcf52..d6c37b4 100644 --- a/runner/modules/connect.py +++ b/runner/modules/connect.py @@ -2,6 +2,10 @@ import meshctrl from json import dumps class connect: + @staticmethod + async def quit(session: meshctrl.Session) -> None: # Function to use when quitting, but gracefully. + await session.close() + @staticmethod async def connect(hostname: str, username: str, password: str) -> meshctrl.Session: session = meshctrl.Session( @@ -13,8 +17,21 @@ class connect: 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. + async def run(session: meshctrl.Session, command: str, nodeids: list[str]) -> None: + try: + response = await session.run_command(nodeids=nodeids, + command=command, + ignore_output=False, + timeout=900) + except Exception as error: + print("Run Command failed.", error) + return + + for device in response: + print(dumps(response[device]["result"])) + + @staticmethod + async def list_online(session: meshctrl.Session) -> dict: # Default is return online devices, but function can also return the offline devices if specified. raw_device_list = await session.list_devices() diff --git a/runner/runner.py b/runner/runner.py index 9039e50..ef441e3 100644 --- a/runner/runner.py +++ b/runner/runner.py @@ -1,6 +1,7 @@ #!/bin/python3 import argparse +from ast import literal_eval import asyncio from json import dumps @@ -13,18 +14,25 @@ def cmd_flags() -> argparse.Namespace: 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('--nodeids', nargs='+', help='List of node IDs') parser.add_argument("-i", "--indent", action='store_true', help="Specify whether the output needs to be indented.") return parser.parse_args() -async def main(): +async def prepare_command(command: str, nodeids: list[str]) -> list[str]: # Have some checks so it happens correctly. + if len(nodeids) < 1 or len(command) < 1: + print("No nodeids or command passed... quiting.") + return [] + + return nodeids + +async def main() -> None: args = cmd_flags() credentials = utilities.load_config() session = await connect.connect(credentials["hostname"], - credentials["username"], - credentials["password"]) + credentials["username"], + credentials["password"]) if args.list_online: online_devices = await connect.list_online(session) @@ -32,12 +40,18 @@ async def main(): print(dumps(online_devices,indent=4)) else: print(dumps(online_devices)) + return await connect.quit(session) # Exit gracefully. Because python. if args.run: if not args.command or not args.nodeids: - return - - print(args.nodeids) + print("When using run, also use --comand and --nodeids") + return await connect.quit(session) # Exit gracefully. Because python. + + command = args.command + nodeids = args.nodeids + nodeids = await prepare_command(command, nodeids) + + await connect.run(session, command, nodeids) await session.close() diff --git a/server/ghostrunner-server b/server/ghostrunner-server index c035592..8bdd4de 100755 Binary files a/server/ghostrunner-server and b/server/ghostrunner-server differ diff --git a/server/python.sh b/server/python.sh index 414809c..9c3c88b 100755 --- a/server/python.sh +++ b/server/python.sh @@ -1,3 +1,3 @@ #!/bin/bash -./../runner/venv/bin/python3 ./../runner/runner.py +./../runner/venv/bin/python3 ./../runner/runner.py "$@" \ No newline at end of file diff --git a/server/src/modules/utilities/confread.go b/server/src/modules/utilities/confread.go index 0c30eea..8d43dbc 100644 --- a/server/src/modules/utilities/confread.go +++ b/server/src/modules/utilities/confread.go @@ -46,6 +46,7 @@ func ReadConf(configPath string) ConfigStruct { section = inidata.Section(runnerSection) + // MeshCentral config.MeshHostname = section.Key("hostname").String() config.MeshUsername = section.Key("username").String() config.MeshPassword = section.Key("password").String()