diff --git a/examples/apt_upgrade.yaml b/examples/apt_upgrade.yaml index 259b865..649b800 100644 --- a/examples/apt_upgrade.yaml +++ b/examples/apt_upgrade.yaml @@ -1,6 +1,6 @@ --- name: Refresh the apt cache -company: "Temp-Agents" +group: "Dev" variables: - name: package_manager value: "apt" diff --git a/examples/echo.yaml b/examples/echo.yaml index 3ba62e9..26ec626 100644 --- a/examples/echo.yaml +++ b/examples/echo.yaml @@ -1,6 +1,6 @@ --- name: Echo some text in the terminal of the device -device: "raspberrypi5" +group: "Kubernetes" variables: - name: var1 value: "Testing" @@ -8,4 +8,6 @@ variables: value: "with a slash!" tasks: - name: Echo! - command: "echo {{ var1 }}/{{ var2 }}" \ No newline at end of file + command: "echo {{ var1 }}/{{ var2 }}" + - name: Echo 2! + command: "echo womp womp" \ No newline at end of file diff --git a/examples/refresh_aptcache.yaml b/examples/refresh_aptcache.yaml index 5aec392..efc72d0 100644 --- a/examples/refresh_aptcache.yaml +++ b/examples/refresh_aptcache.yaml @@ -1,6 +1,6 @@ --- name: Refresh the apt cache -company: "Temp-Agents" +group: "Temp-Agents" variables: - name: package_manager value: "apt" diff --git a/examples/variable_example.yaml b/examples/variable_example.yaml index b5cb97b..baa8c31 100644 --- a/examples/variable_example.yaml +++ b/examples/variable_example.yaml @@ -1,6 +1,6 @@ --- name: Ping Multiple Points -company: "Temp-Agents" +group: "Temp-Agents" variables: - name: host1 value: "1.1.1.1" diff --git a/meshbook/requirements.txt b/meshbook/requirements.txt index 1e9b527..d717b48 100644 --- a/meshbook/requirements.txt +++ b/meshbook/requirements.txt @@ -1,3 +1,4 @@ +argparse asyncio configparser pyyaml diff --git a/meshbook/rewrite.py b/meshbook/rewrite.py index 39adb4e..87f56c3 100644 --- a/meshbook/rewrite.py +++ b/meshbook/rewrite.py @@ -10,86 +10,155 @@ import meshctrl import os import yaml +''' +Script utilities are handled in the following section. +''' + class ScriptEndTrigger(Exception): - """Custom Exception to handle script termination events.""" pass -def load_config(conffile: str = None, segment: str = 'meshcentral-account') -> ConfigParser: - conffile = conffile or './api.conf' - - if not os.path.exists(conffile): - raise ScriptEndTrigger(f'Missing config file {conffile}. Provide an alternative path.') +async def load_config(conf_file: str = './api.conf', segment: str = 'meshcentral-account') -> ConfigParser: + if not os.path.exists(conf_file): + raise ScriptEndTrigger(f'Missing config file {conf_file}. Provide an alternative path.') config = ConfigParser() try: - config.read(conffile) + config.read(conf_file) except Exception as err: - raise ConfigError(f"Error reading configuration file '{conffile}': {err}") + raise ScriptEndTrigger(f"Error reading configuration file '{conf_file}': {err}") if segment not in config: - raise ScriptEndTrigger(f'Segment "{segment}" not found in config file {conffile}.') + raise ScriptEndTrigger(f'Segment "{segment}" not found in config file {conf_file}.') return config[segment] -class meshbook(): - @staticmethod - def compile_book(pb_path) -> dict: - playbook = meshbook.read_yaml(pb_path) - playbook = meshbook.replace_placeholders(playbook) - return playbook +async def init_connection(credentials: dict) -> meshctrl.Session: + session = meshctrl.Session( + credentials['websocket_url'], + user=credentials['username'], + password=credentials['password'] + ) + await session.initialized.wait() + return session - @staticmethod - def read_yaml(file_path: str) -> dict: - with open(file_path, 'r') as file: - return yaml.safe_load(file) +def output(args: argparse.Namespace, message: str): + if not args.silent or args.information: + print(message) - @staticmethod - def replace_placeholders(playbook: dict) -> dict: - variables = {var["name"]: var["value"] for var in playbook.get("variables", [])} +''' +Creation and compilation happends in the following section, where the yaml gets read in, and edited accordingly. +''' - for task in playbook.get("tasks", []): - command = task.get("command", "") - for var_name, var_value in variables.items(): - placeholder = f"{{{{ {var_name} }}}}" # Create the placeholder string like "{{ host1 }}" - command = command.replace(placeholder, var_value) - task["command"] = command - return playbook +async def compile_book(playbook_file: dict) -> dict: + playbook = open(playbook_file, 'r') + playbook = await replace_placeholders(yaml.safe_load(playbook)) + return playbook -async def run_book(creds): - print(creds) +async def replace_placeholders(playbook: dict) -> dict: + variables = {var["name"]: var["value"] for var in playbook.get("variables", [])} + + for task in playbook.get("tasks", []): + command = task.get("command", "") + for var_name, var_value in variables.items(): + placeholder = f"{{{{ {var_name} }}}}" # Create the placeholder string like "{{ host1 }}" + command = command.replace(placeholder, var_value) + task["command"] = command + return playbook + +''' +Creation and compilation of the MeshCentral nodes list (list of all nodes available to the user in the configuration) is handled in the following section. +''' + +async def compile_group_list(session: meshctrl.Session) -> dict: + devices_response = await session.list_devices(details=False, timeout=10) + + local_device_list = {} + for device in devices_response: + if device.meshname not in local_device_list: + local_device_list[device.meshname] = [] + + local_device_list[device.meshname].append({ + "device_id": device.nodeid, + "device_name": device.name, + "device_os": device.os_description, + "device_tags": device.tags, + "reachable": device.connected + }) + + return local_device_list + +async def gather_targets(group_list: dict, playbook: dict) -> dict: + target_list = [] + + if "device" in playbook and "group" not in playbook: + pseudo_target = playbook["device"] + + for group in group_list: + for device in group_list[group]: + if device["reachable"] and pseudo_target == device["device_name"]: + target_list.append(device["device_id"]) + + elif "group" in playbook and "device" not in playbook: + pseudo_target = playbook["group"] + + for group in group_list: + if pseudo_target == group: + for device in group_list[group]: + if device["reachable"]: + target_list.append(device["device_id"]) + + return target_list + +async def execute_playbook(session: meshctrl.Session, targets: dict, playbook: dict): + + for task in playbook["tasks"]: + print("Running:", task["name"]) + response = await session.run_command(nodeids=targets, command=task["command"], timeout=300) + + print(json.dumps(response,indent=4)) + for device in response: + device_result = str(response[device]["result"]) + device_result = device_result.replace("Run commands completed.", "") + print("AFTER", device_result) async def main(): parser = argparse.ArgumentParser(description="Process command-line arguments") parser.add_argument("-pb", "--playbook", type=str, help="Path to the playbook file.", required=True) - - parser.add_argument("--conf", type=str, help="Path for the API configuration file (default: ./api.conf).") - parser.add_argument("--nojson", action="store_true", help="Makes the program not output the JSON response data.") - parser.add_argument("-s", "--silent", action="store_true", help="Suppress terminal output.") - parser.add_argument("-i", "--information", action="store_true", help="Add the calculations and other informational data to the output.") + parser.add_argument("--conf", type=str, help="Path for the API configuration file (default: ./api.conf).", required=False) + parser.add_argument("--noout", action="store_true", help="Makes the program not output response data.", required=False) + parser.add_argument("-s", "--silent", action="store_true", help="Suppress terminal output.", required=False) + parser.add_argument("-i", "--information", action="store_true", help="Add the calculations and other informational data to the output.", required=False) args = parser.parse_args() try: - credentials = load_config(args.conf) - playbook = meshbook.compile_book(args.playbook) + output(args, "Trying to load the MeshCentral account credential file...") + output(args, "Trying to load the Playbook yaml file and compile it into something workable...") - session = meshctrl.Session( - credentials['websocket_url'], - user=credentials['username'], - password=credentials['password'] + credentials, playbook = await asyncio.gather( + (load_config() if args.conf is None else load_config(args.conf)), + (compile_book(args.playbook)) ) - await session.initialized.wait() - raw_device_list = await session.list_devices(timeout=10) - device_list = raw_device_list - print(device_list) + output(args, "Connecting to MeshCentral and establish a session using variables from previous credential file.") + session = await init_connection(credentials) + + output(args, "Generating group list with nodes and reference the targets from that.") + group_list = await compile_group_list(session) + targets_list = await gather_targets(group_list, playbook) + + if len(targets_list) == 0: + output(args, "No targets found or targets unreachable, quitting.") + else: + output(args, "Executing playbook on the targets.") + output(args, json.dumps(targets_list,indent=4)) + await execute_playbook(session, targets_list, playbook) await session.close() - except ScriptEndTrigger as e: - if not args.silent or args.information: - print(e) + except ScriptEndTrigger as message: + output(args, message) if __name__ == "__main__": - asyncio.run(main()) + asyncio.run(main()) \ No newline at end of file diff --git a/templates/api.conf.template b/templates/api.conf.template index 925f597..dc1cee3 100644 --- a/templates/api.conf.template +++ b/templates/api.conf.template @@ -1,4 +1,4 @@ -[meshcentral-service] +[meshcentral-account] websocket_url = username = password = \ No newline at end of file