Compare commits

..

4 Commits

Author SHA1 Message Date
Josiah Baldwin
6daaa91758 Added test for run_console_command 2025-09-26 14:12:56 -07:00
Josiah Baldwin
078e07cb4f Added mesh agent hex ID to agent server return value 2025-09-26 14:12:22 -07:00
Josiah Baldwin
0e569ae0cb Added support for run_console_commands 2025-09-26 14:11:39 -07:00
Josiah Baldwin
62fdc79aeb Merge pull request #57 from HuFlungDu/fix/run_commands_response
Add handling for runcommands `reply: true` option and make it the default.

Resolves #51
2025-09-26 13:08:31 -07:00
4 changed files with 91 additions and 3 deletions

View File

@@ -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

View File

@@ -37,7 +37,9 @@ class Agent(object):
self._clienturl = clienturl
self._dockerurl = dockerurl
r = requests.post(f"{self._clienturl}/add-agent", json={"url": f"{self._dockerurl}", "meshid": self.meshid})
self.nodeid = r.json()["id"]
agent_json = r.json()
self.nodeid = agent_json["id"]
self.nodehex = agent_json["hex"]
def __enter__(self):
return self

View File

@@ -53,7 +53,7 @@ def add_agent():
time.sleep(.1)
else:
raise Exception(f"Failed to start agent: {text}")
return {"id": agent_id}
return {"id": agent_id, "hex": agent_hex}
@api.route('/remove-agent/<agentid>', methods=['POST'])
def remove_agent(agentid):

View File

@@ -259,6 +259,19 @@ async def test_mesh_device(env):
else:
raise Exception("Run command on a device that doesn't exist did not raise an exception")
r = await admin_session.run_console_command([agent.nodeid, agent2.nodeid], "info", timeout=10)
print("\ninfo run_console_command: {}\n".format(r))
assert agent.nodeid in r[agent.nodeid]["result"], "Run console command gave bad response"
assert agent2.nodeid in r[agent2.nodeid]["result"], "Run console command gave bad response"
# Test run_commands missing device
try:
await admin_session.run_console_command([agent.nodeid, "notanid"], "info", timeout=10)
except* (meshctrl.exceptions.ServerError, ValueError):
pass
else:
raise Exception("Run console command on a device that doesn't exist did not raise an exception")
# Test run commands with individual device permissions
try:
await unprivileged_session.run_command(agent.nodeid, "ls", timeout=10)