Compare commits

..

43 Commits
1.2.0 ... 1.3.0

Author SHA1 Message Date
Josiah Baldwin
e0694f980c Merge branch 'release/1.3.0' 2025-09-26 15:58:06 -07:00
Josiah Baldwin
61053549f2 Fixed test for remove device 2025-09-26 15:54:25 -07:00
Josiah Baldwin
fb3d043431 Added Daan to contributors 2025-09-26 15:39:57 -07:00
Josiah Baldwin
c13985739b Added release notes for 1.3.0 2025-09-26 15:37:55 -07:00
Josiah Baldwin
db1914c87b Merge pull request #54 from DaanSelen/feat-remove-dev
feat: remove devices function

resolves #52
2025-09-26 15:32:09 -07:00
Daan Selen
b0d071d87f feat: add remove_device function 2025-09-26 15:29:52 -07:00
Josiah Baldwin
3bcedf5610 Kinda added a test for remove device 2025-09-26 15:20:25 -07:00
Josiah Baldwin
9c7a8c39b0 Modified some implementation details 2025-09-26 15:19:57 -07:00
Daan Selen
7ba6989325 refac: I lied, this is the last... 2025-09-26 14:50:25 -07:00
Daan Selen
748e39d5b4 refac: remove nodeid parameter 2025-09-26 14:50:25 -07:00
Daan Selen
6dae40eb40 refac: copy other style 2025-09-26 14:50:25 -07:00
Daan Selen
c7d628716e refac: renamed and added device class impl 2025-09-26 14:50:25 -07:00
Daan Selen
1f9979ddd1 feat: add remove_device function 2025-09-26 14:50:25 -07:00
d4b9524814 feat(lib): draft function for remove_device 2025-09-26 14:50:25 -07:00
Josiah Baldwin
bc1db8f2b3 Update documentation for files.rm
Resolves #53
2025-09-26 14:40:53 -07:00
Josiah Baldwin
403c0cd0ec Merge branch 'development' of github.com:HuFlungDu/pylibmeshctrl into development 2025-09-26 14:38:34 -07:00
Josiah Baldwin
0b0029563a Maybe fix race condition when using multiple nodes in run_command 2025-09-26 14:38:10 -07:00
Josiah Baldwin
0b32896c88 Merge pull request #60 from HuFlungDu/feat/run_console_commands
Add run_console_command function

resolve #55
2025-09-26 14:17:44 -07:00
Josiah Baldwin
2304810ee6 Merge pull request #59 from HuFlungDu/revert-58-feat/run_console_commands
Revert "Feat/run console commands"

Seriously, I don't want to default merge to main, github.
2025-09-26 14:16:47 -07:00
Josiah Baldwin
4cda54ab60 Revert "Feat/run console commands" 2025-09-26 14:16:18 -07:00
Josiah Baldwin
87fad5aa13 Merge pull request #58 from HuFlungDu/feat/run_console_commands
Add run_console_command function

resolve #55
2025-09-26 14:16:12 -07:00
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
Josiah Baldwin
c450ad7a96 Added test for missing device 2025-09-26 12:57:56 -07:00
Josiah Baldwin
891f7bfc12 Fixed old style run_command failing 2025-09-26 12:57:30 -07:00
Josiah Baldwin
4953d85cdc added reply to run commands 2025-09-24 10:36:42 -07:00
Josiah Baldwin
f5c6e96597 Bumped version 2025-06-25 10:51:18 -07:00
Josiah Baldwin
428a1b31c7 Merge pull request #49 from DaanSelen/user_agent
Added user agent to ws connection.
2025-06-24 14:44:06 -07:00
DaanSelen
16f3f99427 Merge branch 'development' into user_agent 2025-06-24 22:51:10 +02:00
Josiah Baldwin
d21450e463 Merge pull request #48 from DaanSelen/increase_limit
Increase limit
2025-06-24 13:48:43 -07:00
Daan Selen
9e08a1af49 Minor corrections 2025-06-19 22:20:30 +02:00
Daan Selen
e9de43420e draft 2025-06-19 22:13:35 +02:00
Daan Selen
fcdf8add53 Just max_size 2025-06-19 21:59:54 +02:00
Josiah Baldwin
163b776dfc Fixed library __version__ var 2025-06-19 12:38:00 -07:00
Josiah Baldwin
04c8f622de Bumped version 2025-06-14 12:53:26 -07:00
Josiah Baldwin
ccb5f1eb40 Removed catch with print statement 2025-06-14 12:50:22 -07:00
Josiah Baldwin
ce2cf2bfe1 Merge branch 'fix/device-details' into development 2025-06-14 12:47:45 -07:00
Josiah Baldwin
a3b4962e7f Update timeout for WS download, becaule it takes a little longer than http 2025-06-14 12:45:36 -07:00
Josiah Baldwin
5947e48c5b modified node parsing to accept ony number of nested strings 2025-06-14 12:42:59 -07:00
Daan Selen
31a8f00cd0 syntax fix 2025-06-12 16:58:19 +02:00
Daan Selen
871d36b334 Added support for new MeshCentral response type.
2b4ab2b122
2025-06-12 16:35:08 +02:00
8 changed files with 129 additions and 27 deletions

View File

@@ -3,3 +3,4 @@ Contributors
============ ============
* Josiah Baldwin <jbaldwin8889@gmail.com> * Josiah Baldwin <jbaldwin8889@gmail.com>
* Daan Selen <https://github.com/DaanSelen>

View File

@@ -2,6 +2,33 @@
Changelog Changelog
========= =========
version 1.3.0
=============
Improvements:
* Improved how run_commands was handled (#51)
* Added remove device functionality (#52)
* Added run_console_commands functionality (#55)
Bugs:
* Silly documentation being wrong (#53)
version 1.2.2
=============
Improvements:
* Added user agent to websocket headers
Bugs:
* Fixed library's __version__ implementation
* Fixed data from certain devices not showing up due to overloading websocket packet sizes
version 1.2.1
=============
Bugs:
* Fixed handling of meshcentral's list_devices return with details=True
version 1.2.0 version 1.2.0
============= =============

View File

@@ -8,7 +8,7 @@ else:
try: try:
# Change here if project is renamed and does not equal the package name # Change here if project is renamed and does not equal the package name
dist_name = "meshctrl" dist_name = "libmeshctrl"
__version__ = version(dist_name) __version__ = version(dist_name)
except PackageNotFoundError: # pragma: no cover except PackageNotFoundError: # pragma: no cover
__version__ = "unknown" __version__ = "unknown"
@@ -24,4 +24,4 @@ from . import files
from . import exceptions from . import exceptions
from . import device from . import device
from . import mesh from . import mesh
from . import user_group from . import user_group

View File

@@ -295,6 +295,23 @@ class Device(object):
''' '''
return await self._session.reset_devices(self.nodeid, timeout=timeout) return await self._session.reset_devices(self.nodeid, timeout=timeout)
async def remove(self, timeout=None):
'''
Remove device from MeshCentral
Args:
nodeids (str|list[str]): nodeid(s) of the device(s) that have to be removed
timeout (int): duration in seconds to wait for a response before throwing an error
Returns:
bool: True on success, raise otherwise
Raises:
:py:class:`~meshctrl.exceptions.SocketError`: Info about socket closure
asyncio.TimeoutError: Command timed out
'''
return self._session.remove_devices(self.nodeid, timeout)
async def sleep(self, timeout=None): async def sleep(self, timeout=None):
''' '''
Sleep device Sleep device

View File

@@ -157,7 +157,7 @@ class Files(tunnel.Tunnel):
async def rm(self, path, files, recursive=False, timeout=None): async def rm(self, path, files, recursive=False, timeout=None):
""" """
Create a directory on the device. This API doesn't error if the file doesn't exist. Remove a set of files or directories from the device. This API doesn't error if the file doesn't exist.
Args: Args:
path (str): Directory from which to delete files path (str): Directory from which to delete files

View File

@@ -10,6 +10,8 @@ import io
import ssl import ssl
import urllib import urllib
from python_socks.async_.asyncio import Proxy from python_socks.async_.asyncio import Proxy
from platform import python_version
from . import __version__
from . import constants from . import constants
from . import exceptions from . import exceptions
from . import util from . import util
@@ -45,7 +47,8 @@ class Session(object):
closed (asyncio.Event): Event that occurs when the session closes permanently closed (asyncio.Event): Event that occurs when the session closes permanently
''' '''
def __init__(self, url, user=None, domain=None, password=None, loginkey=None, proxy=None, token=None, ignore_ssl=False, auto_reconnect=False): def __init__(self, url, user=None, domain=None, password=None, loginkey=None, proxy=None, token=None, ignore_ssl=False, auto_reconnect=False, user_agent_header=None):
default_user_agent_header = f"Python/{python_version()} websockets/{websockets.__version__} pylibmeshctrl/{__version__}"
parsed = urllib.parse.urlparse(url) parsed = urllib.parse.urlparse(url)
if parsed.scheme not in ("wss", "ws"): if parsed.scheme not in ("wss", "ws"):
@@ -106,6 +109,10 @@ class Session(object):
self._file_tunnels = {} self._file_tunnels = {}
self._ignore_ssl = ignore_ssl self._ignore_ssl = ignore_ssl
self.auto_reconnect = auto_reconnect self.auto_reconnect = auto_reconnect
if user_agent_header:
self.user_agent_header = user_agent_header
else:
self.user_agent_header = default_user_agent_header
self._eventer = util.Eventer() self._eventer = util.Eventer()
@@ -144,7 +151,7 @@ class Session(object):
options["additional_headers"] = headers options["additional_headers"] = headers
async for websocket in websockets.asyncio.client.connect(self.url, proxy=self._proxy, process_exception=util._process_websocket_exception, **options): async for websocket in websockets.asyncio.client.connect(self.url, proxy=self._proxy, process_exception=util._process_websocket_exception, max_size=None, user_agent_header=self.user_agent_header, **options):
self.alive = True self.alive = True
self._socket_open.set() self._socket_open.set()
try: try:
@@ -478,7 +485,14 @@ class Session(object):
if "result" in res0: if "result" in res0:
raise exceptions.ServerError(res0["result"]) raise exceptions.ServerError(res0["result"])
if details: if details:
nodes = json.loads(res0["data"]) nodes = res0["data"]
# Accept any number of nested strings, meshcentral is odd
while True:
try:
nodes = json.loads(nodes)
except TypeError:
break
for node in nodes: for node in nodes:
if node["node"].get("meshid", None): if node["node"].get("meshid", None):
node["node"]["mesh"] = mesh.Mesh(node["node"].get("meshid"), self) node["node"]["mesh"] = mesh.Mesh(node["node"].get("meshid"), self)
@@ -557,7 +571,7 @@ class Session(object):
while True: while True:
data = await event_queue.get() data = await event_queue.get()
if filter and not util.compare_dict(filter, data): if filter and not util.compare_dict(filter, data):
continue continue
yield data yield data
finally: finally:
self._eventer.off("server_event", _) self._eventer.off("server_event", _)
@@ -1048,6 +1062,30 @@ class Session(object):
raise exceptions.ServerError(data["result"]) raise exceptions.ServerError(data["result"])
return True return True
async def remove_devices(self, nodeids, timeout=None):
'''
Remove device(s) from MeshCentral
Args:
nodeids (str|list[str]): nodeid(s) of the device(s) that have to be removed
timeout (int): duration in seconds to wait for a response before throwing an error
Returns:
bool: True on success, raise otherwise
Raises:
:py:class:`~meshctrl.exceptions.ServerError`: Error text from server if there is a failure
:py:class:`~meshctrl.exceptions.SocketError`: Info about socket closure
asyncio.TimeoutError: Command timed out
'''
if isinstance(nodeids, str):
nodeids = [nodeids]
data = await self._send_command({ "action": 'removedevices', "nodeids": nodeids}, "remove_devices", timeout=timeout)
if data.get("result", "ok").lower() != "ok":
raise exceptions.ServerError(data["result"])
return True
async def add_device_group(self, name, description="", amtonly=False, features=0, consent=0, timeout=None): async def add_device_group(self, name, description="", amtonly=False, features=0, consent=0, timeout=None):
''' '''
@@ -1474,25 +1512,35 @@ class Session(object):
async def __(command): async def __(command):
data = await self._send_command(command, "run_command", timeout=timeout) data = await self._send_command(command, "run_command", timeout=timeout)
if data.get("result", "ok").lower() != "ok": if data.get("type", None) != "runcommands" and data.get("result", "ok").lower() != "ok":
raise exceptions.ServerError(data["result"]) raise exceptions.ServerError(data["result"])
elif data.get("type", None) != "runcommands" and data.get("result", "ok").lower() == "ok":
expect_response = False expect_response = False
if not ignore_output: console_task = tg.create_task(asyncio.wait_for(_console(), timeout=timeout))
userid = (await self.user_info())["_id"] if not ignore_output:
for n in nodeids: userid = (await self.user_info())["_id"]
device_info = await self.device_info(n, timeout=timeout) for n in nodeids:
try: device_info = await self.device_info(n, timeout=timeout)
permissions = device_info.mesh.links.get(userid, {}).get("rights",constants.DeviceRights.norights)\ try:
# 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 permissions = device_info.mesh.links.get(userid, {}).get("rights",constants.DeviceRights.norights)\
# |device_info.get("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
# If we don't have agentconsole rights, we won't be able te read the output, so fill in blanks on this node # |device_info.get("links", {}).get(userid, {}).get("rights", constants.DeviceRights.norights)
if not permissions&constants.DeviceRights.agentconsole: # If we don't have agentconsole rights, we won't be able te read the output, so fill in blanks on this node
result[n]["complete"] = True if not permissions&constants.DeviceRights.agentconsole:
else: result[n]["complete"] = True
expect_response = True else:
except AttributeError: expect_response = True
result[n]["complete"] = True except AttributeError:
result[n]["complete"] = True
if expect_response:
tasks.append(console_task)
else:
console_task.cancel()
elif data.get("type", None) == "runcommands" and not ignore_output:
tasks.append(tg.create_task(asyncio.wait_for(_reply(data["responseid"], start_data=data), timeout=timeout)))
# Force this to run immediately? This might be odd; but we want to make sure we get don't lose the race condition with the srever.
# Not sure if this actually works but I haven't yet seen it fail. *shrug*
await asyncio.sleep(0)
tasks = [] tasks = []
async with asyncio.TaskGroup() as tg: async with asyncio.TaskGroup() as tg:
@@ -1969,4 +2017,4 @@ class _FileExplorerWrapper:
return await self._files.__aenter__() return await self._files.__aenter__()
async def __aexit__(self, exc_t, exc_v, exc_tb): async def __aexit__(self, exc_t, exc_v, exc_tb):
return await self._files.__aexit__(exc_t, exc_v, exc_tb) return await self._files.__aexit__(exc_t, exc_v, exc_tb)

View File

@@ -120,7 +120,7 @@ async def test_upload_download(env):
downfilestream.seek(0) downfilestream.seek(0)
start = time.perf_counter() start = time.perf_counter()
r = await files.download(f"{pwd}/test", downfilestream, skip_http_attempt=True, timeout=5) r = await files.download(f"{pwd}/test", downfilestream, skip_http_attempt=True, timeout=20)
print("\ninfo files_download: {}\n".format(r)) print("\ninfo files_download: {}\n".format(r))
assert r["result"] == True, "Download failed" assert r["result"] == True, "Download failed"
assert r["size"] == len(randdata), "Downloaded wrong number of bytes" assert r["size"] == len(randdata), "Downloaded wrong number of bytes"

View File

@@ -313,6 +313,15 @@ async def test_mesh_device(env):
r = await admin_session.remove_users_from_device_group((await privileged_session.user_info())["_id"], mesh.meshid, timeout=10) r = await admin_session.remove_users_from_device_group((await privileged_session.user_info())["_id"], mesh.meshid, timeout=10)
print("\ninfo remove_users_from_device_group: {}\n".format(r)) print("\ninfo remove_users_from_device_group: {}\n".format(r))
assert (r[(await privileged_session.user_info())["_id"]]["success"]), "Failed to remove user from device group" assert (r[(await privileged_session.user_info())["_id"]]["success"]), "Failed to remove user from device group"
await admin_session.remove_devices(agent2.nodeid, timeout=10)
try:
await admin_session.device_info(agent2.nodeid, timeout=10)
except ValueError:
pass
else:
raise Exception("Device not deleted")
assert (await admin_session.remove_users_from_device(agent.nodeid, (await unprivileged_session.user_info())["_id"], timeout=10)), "Failed to remove user from device" assert (await admin_session.remove_users_from_device(agent.nodeid, (await unprivileged_session.user_info())["_id"], timeout=10)), "Failed to remove user from device"