From 0e569ae0cb29a06aab4c76fdefbd05ad21aeb096 Mon Sep 17 00:00:00 2001 From: Josiah Baldwin Date: Fri, 26 Sep 2025 14:11:39 -0700 Subject: [PATCH] Added support for run_console_commands --- src/meshctrl/session.py | 75 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 74 insertions(+), 1 deletion(-) diff --git a/src/meshctrl/session.py b/src/meshctrl/session.py index 5b7bdcb..cf1b737 100644 --- a/src/meshctrl/session.py +++ b/src/meshctrl/session.py @@ -1496,7 +1496,6 @@ class Session(object): result[node]["result"].append(event["result"]) if all(_["complete"] for key, _ in result.items()): return True - if start_data is not None: if _parse_event(start_data): return @@ -1540,6 +1539,80 @@ class Session(object): return {n: v | {"result": "".join(v["result"])} for n,v in result.items()} + async def run_console_command(self, nodeids, command, powershell=False, runasuser=False, runasuseronly=False, ignore_output=False, timeout=None): + ''' + Run a mesh console command on any number of nodes. WARNING: Non namespaced call. Calling this function again before it returns may cause unintended consequences. + + Args: + nodeids (str|list[str]): Unique ids of nodes on which to run the command + command (str): Command to run + ignore_output (bool): Don't bother trying to get the output. Every device will return an empty string for its result. + timeout (int): duration in seconds to wait for a response before throwing an error + + Returns: + dict[str, ~meshctrl.types.RunCommandResponse]: Dict containing mapped output of the commands by device + + Raises: + :py:class:`~meshctrl.exceptions.ServerError`: Error text from server if there is a failure + :py:class:`~meshctrl.exceptions.SocketError`: Info about socket closure + ValueError: `Invalid device id` if device is not found + asyncio.TimeoutError: Command timed out + ''' + if isinstance(nodeids, str): + nodeids = [nodeids] + + def match_nodeid(id, ids): + for nid in ids: + if (nid == id): + return nid + if (nid[6:] == id): + return nid + if (f"node//{nid}" == id): + return nid + + result = {n: {"complete": False, "result": [], "command": command} for n in nodeids} + async def _console(): + async for event in self.events({"action": "msg", "type": "console"}): + node = match_nodeid(event["nodeid"], nodeids) + if node: + result[node]["result"].append(event["value"]) + result.setdefault(node, {})["complete"] = True + if all(_["complete"] for key, _ in result.items()): + break + async def __(command, tg, tasks): + data = await self._send_command(command, "run_console_command", timeout=timeout) + + if data.get("type", None) != "runcommands" and data.get("result", "ok").lower() != "ok": + raise exceptions.ServerError(data["result"]) + elif data.get("type", None) != "runcommands" and data.get("result", "ok").lower() == "ok": + expect_response = False + console_task = tg.create_task(asyncio.wait_for(_console(), timeout=timeout)) + if not ignore_output: + userid = (await self.user_info())["_id"] + for n in nodeids: + device_info = await self.device_info(n, timeout=timeout) + try: + permissions = device_info.mesh.links.get(userid, {}).get("rights",constants.DeviceRights.norights)\ + # This should work for device rights, but it only seems to work for mesh rights. Not sure why, but I can't get the events to show up when the user only has individual device rights + # |device_info.get("links", {}).get(userid, {}).get("rights", constants.DeviceRights.norights) + # If we don't have agentconsole rights, we won't be able te read the output, so fill in blanks on this node + if not permissions&constants.DeviceRights.agentconsole: + result[n]["complete"] = True + else: + expect_response = True + except AttributeError: + result[n]["complete"] = True + if expect_response: + tasks.append(console_task) + else: + console_task.cancel() + + tasks = [] + async with asyncio.TaskGroup() as tg: + tasks.append(tg.create_task(__({ "action": 'runcommands', "nodeids": nodeids, "type": 4, "cmds": command}, tg, tasks))) + + return {n: v | {"result": "".join(v["result"])} for n,v in result.items()} + def shell(self, nodeid): ''' Get a terminal shell on the given device