Rewrite working now only needs output formatting.

This commit is contained in:
2025-01-08 16:56:25 +01:00
parent e5e4aba47e
commit 6a0127be78
7 changed files with 128 additions and 56 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
--- ---
name: Refresh the apt cache name: Refresh the apt cache
company: "Temp-Agents" group: "Dev"
variables: variables:
- name: package_manager - name: package_manager
value: "apt" value: "apt"
+4 -2
View File
@@ -1,6 +1,6 @@
--- ---
name: Echo some text in the terminal of the device name: Echo some text in the terminal of the device
device: "raspberrypi5" group: "Kubernetes"
variables: variables:
- name: var1 - name: var1
value: "Testing" value: "Testing"
@@ -8,4 +8,6 @@ variables:
value: "with a slash!" value: "with a slash!"
tasks: tasks:
- name: Echo! - name: Echo!
command: "echo {{ var1 }}/{{ var2 }}" command: "echo {{ var1 }}/{{ var2 }}"
- name: Echo 2!
command: "echo womp womp"
+1 -1
View File
@@ -1,6 +1,6 @@
--- ---
name: Refresh the apt cache name: Refresh the apt cache
company: "Temp-Agents" group: "Temp-Agents"
variables: variables:
- name: package_manager - name: package_manager
value: "apt" value: "apt"
+1 -1
View File
@@ -1,6 +1,6 @@
--- ---
name: Ping Multiple Points name: Ping Multiple Points
company: "Temp-Agents" group: "Temp-Agents"
variables: variables:
- name: host1 - name: host1
value: "1.1.1.1" value: "1.1.1.1"
+1
View File
@@ -1,3 +1,4 @@
argparse
asyncio asyncio
configparser configparser
pyyaml pyyaml
+119 -50
View File
@@ -10,86 +10,155 @@ import meshctrl
import os import os
import yaml import yaml
'''
Script utilities are handled in the following section.
'''
class ScriptEndTrigger(Exception): class ScriptEndTrigger(Exception):
"""Custom Exception to handle script termination events."""
pass pass
def load_config(conffile: str = None, segment: str = 'meshcentral-account') -> ConfigParser: async def load_config(conf_file: str = './api.conf', segment: str = 'meshcentral-account') -> ConfigParser:
conffile = conffile or './api.conf' if not os.path.exists(conf_file):
raise ScriptEndTrigger(f'Missing config file {conf_file}. Provide an alternative path.')
if not os.path.exists(conffile):
raise ScriptEndTrigger(f'Missing config file {conffile}. Provide an alternative path.')
config = ConfigParser() config = ConfigParser()
try: try:
config.read(conffile) config.read(conf_file)
except Exception as err: 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: 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] return config[segment]
class meshbook(): async def init_connection(credentials: dict) -> meshctrl.Session:
@staticmethod session = meshctrl.Session(
def compile_book(pb_path) -> dict: credentials['websocket_url'],
playbook = meshbook.read_yaml(pb_path) user=credentials['username'],
playbook = meshbook.replace_placeholders(playbook) password=credentials['password']
return playbook )
await session.initialized.wait()
return session
@staticmethod def output(args: argparse.Namespace, message: str):
def read_yaml(file_path: str) -> dict: if not args.silent or args.information:
with open(file_path, 'r') as file: print(message)
return yaml.safe_load(file)
@staticmethod '''
def replace_placeholders(playbook: dict) -> dict: Creation and compilation happends in the following section, where the yaml gets read in, and edited accordingly.
variables = {var["name"]: var["value"] for var in playbook.get("variables", [])} '''
for task in playbook.get("tasks", []): async def compile_book(playbook_file: dict) -> dict:
command = task.get("command", "") playbook = open(playbook_file, 'r')
for var_name, var_value in variables.items(): playbook = await replace_placeholders(yaml.safe_load(playbook))
placeholder = f"{{{{ {var_name} }}}}" # Create the placeholder string like "{{ host1 }}" return playbook
command = command.replace(placeholder, var_value)
task["command"] = command
return playbook
async def run_book(creds): async def replace_placeholders(playbook: dict) -> dict:
print(creds) 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(): async def main():
parser = argparse.ArgumentParser(description="Process command-line arguments") 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("-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).", required=False)
parser.add_argument("--conf", type=str, help="Path for the API configuration file (default: ./api.conf).") parser.add_argument("--noout", action="store_true", help="Makes the program not output response data.", required=False)
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.", required=False)
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.", required=False)
parser.add_argument("-i", "--information", action="store_true", help="Add the calculations and other informational data to the output.")
args = parser.parse_args() args = parser.parse_args()
try: try:
credentials = load_config(args.conf) output(args, "Trying to load the MeshCentral account credential file...")
playbook = meshbook.compile_book(args.playbook) output(args, "Trying to load the Playbook yaml file and compile it into something workable...")
session = meshctrl.Session( credentials, playbook = await asyncio.gather(
credentials['websocket_url'], (load_config() if args.conf is None else load_config(args.conf)),
user=credentials['username'], (compile_book(args.playbook))
password=credentials['password']
) )
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() await session.close()
except ScriptEndTrigger as e: except ScriptEndTrigger as message:
if not args.silent or args.information: output(args, message)
print(e)
if __name__ == "__main__": if __name__ == "__main__":
asyncio.run(main()) asyncio.run(main())
+1 -1
View File
@@ -1,4 +1,4 @@
[meshcentral-service] [meshcentral-account]
websocket_url = websocket_url =
username = username =
password = password =