diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 8f40156..69ab04a 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -2,7 +2,21 @@ Changelog ========= +Version 1.1.0 +============= +Features: + * Added overrides for meshcentral files for testing purposes + * Added `users` field to `device` object + +Bugs: + * Fixed connection errors not raising immediately + * Fixed run_commands parsing return from multiple devices incorrectly + * Fixed listening to raw not removing its listener correctly + * Fixed javascript timecodes not being handled in gnu environments + * Changed some fstring formatting that locked the library into python >3.13 + + Version 1.0.0 -=========== +============= First release diff --git a/src/meshctrl/device.py b/src/meshctrl/device.py index bb017c1..f08b521 100644 --- a/src/meshctrl/device.py +++ b/src/meshctrl/device.py @@ -12,6 +12,7 @@ class Device(object): name (str|None): Device name as it is shown on the meshcentral server description (str|None): Device description as it is shown on the meshcentral server. Also accepted as desc. tags (list[str]|None): tags associated with device. + users (list[str]|None): latest known usernames which have logged in. created_at (datetime.Datetime|int|None): Time at which device mas created. Also accepted as agct. computer_name (str|None): Device name as reported from the agent. This may be different from name. Also accepted as rname. icon (~meshctrl.constants.Icon): Icon displayed on the website @@ -38,6 +39,7 @@ class Device(object): name (str|None): Device name as it is shown on the meshcentral server description (str|None): Device description as it is shown on the meshcentral server. tags (list[str]): tags associated with device. + users (list[str]): latest known usernames which have logged in. computer_name (str|None): Device name as reported from the agent. This may be different from name. Also accepted as rname. icon (~meshctrl.constants.Icon): Icon displayed on the website mesh (~meshctrl.mesh.Mesh|None): Mesh object under which this device exists. Is None for individual device access. @@ -56,8 +58,8 @@ class Device(object): ''' def __init__(self, nodeid, session, agent=None, name=None, desc=None, description=None, - tags=None, - agct=None, created_at=None, + tags=None, users=None, + agct=None, created_at=None, rname=None, computer_name=None, icon=constants.Icon.desktop, mesh=None, mtype=None, meshtype=None, groupname=None, meshname=None, domain=None, host=None, ip=None, conn=None, connected=None, @@ -90,6 +92,7 @@ class Device(object): self.description = description if description is not None else desc self.os_description = os_description if os_description is not None else osdesc self.tags = tags if tags is not None else [] + self.users = users if users is not None else [] self.details = details if details is not None else {} created_at = created_at if created_at is not None else agct @@ -340,10 +343,10 @@ class Device(object): def __str__(self): return f"" def __repr__(self): return f"Device(nodeid={repr(self.nodeid)}, session={repr(self._session)}, name={repr(self.name)}, description={repr(self.description)}, computer_name={repr(self.computer_name)}, icon={repr(self.icon)}, "\ f"mesh={repr(self.mesh)}, meshtype={repr(self.meshtype)}, meshname={repr(self.meshname)}, domain={repr(self.domain)}, host={repr(self.host)}, ip={repr(self.ip)}, "\ - f"tags={repr(self.tags)}, details={repr(self.details)} created_at={repr(self.created_at)} lastaddr={repr(self.lastaddr)} lastconnect={repr(self.lastconnect)} "\ + f"tags={repr(self.tags)}, users={repr(self.users)}, details={repr(self.details)} created_at={repr(self.created_at)} lastaddr={repr(self.lastaddr)} lastconnect={repr(self.lastconnect)} "\ f"connected={repr(self.connected)}, powered_on={repr(self.powered_on)}, os_description={repr(self.os_description)}, links={repr(self.links)}, **{repr(self._extra_props)})" diff --git a/src/meshctrl/exceptions.py b/src/meshctrl/exceptions.py index 43ae378..9ac3af2 100644 --- a/src/meshctrl/exceptions.py +++ b/src/meshctrl/exceptions.py @@ -2,7 +2,9 @@ class MeshCtrlError(Exception): """ Base class for Meshctrl errors """ - pass + def __init__(self, message, *args, **kwargs): + self.message = message + super().__init__(message, *args, **kwargs) class ServerError(MeshCtrlError): """ @@ -25,6 +27,7 @@ class FileTransferError(MeshCtrlError): """ def __init__(self, message, stats): self.stats = stats + super().__init__(message) class FileTransferCancelled(FileTransferError): """ diff --git a/src/meshctrl/session.py b/src/meshctrl/session.py index 20bbc31..0672715 100644 --- a/src/meshctrl/session.py +++ b/src/meshctrl/session.py @@ -533,7 +533,7 @@ class Session(object): data = await event_queue.get() yield data finally: - self._eventer.off("server_event", _) + self._eventer.off("raw", _) async def events(self, filter=None): ''' @@ -1463,6 +1463,7 @@ class Session(object): result.setdefault(node, {})["complete"] = True if all(_["complete"] for key, _ in result.items()): break + continue elif (event["value"].startswith("Run commands")): continue result[node]["result"].append(event["value"]) diff --git a/src/meshctrl/util.py b/src/meshctrl/util.py index fe36fc5..b245d59 100644 --- a/src/meshctrl/util.py +++ b/src/meshctrl/util.py @@ -140,17 +140,20 @@ def compare_dict(dict1, dict2): return False def _check_socket(f): + async def _check_errs(self): + if not self.alive and self._main_loop_error is not None: + raise self._main_loop_error + elif not self.alive and self.initialized.is_set(): + raise exceptions.SocketError("Socket Closed") + @functools.wraps(f) async def wrapper(self, *args, **kwargs): try: - async with asyncio.TaskGroup() as tg: - tg.create_task(asyncio.wait_for(self.initialized.wait(), 10)) - tg.create_task(asyncio.wait_for(self._socket_open.wait(), 10)) + await asyncio.wait_for(self.initialized.wait(), 10) + await _check_errs(self) + await asyncio.wait_for(self._socket_open.wait(), 10) finally: - if not self.alive and self._main_loop_error is not None: - raise self._main_loop_error - elif not self.alive and self.initialized.is_set(): - raise exceptions.SocketError("Socket Closed") + await _check_errs(self) return await f(self, *args, **kwargs) return wrapper diff --git a/tests/environment/__init__.py b/tests/environment/__init__.py index 84aef33..7bc9cb3 100644 --- a/tests/environment/__init__.py +++ b/tests/environment/__init__.py @@ -62,7 +62,7 @@ class TestEnvironment(object): return self # Destroy the env in case it wasn't killed correctly last time. subprocess.check_call(["docker", "compose", "down"], stdout=subprocess.DEVNULL, cwd=thisdir) - self._subp = _docker_process = subprocess.Popen(["docker", "compose", "up", "--build", "--force-recreate", "--no-deps"], stdout=subprocess.DEVNULL, cwd=thisdir) + self._subp = _docker_process = subprocess.Popen(["docker", "compose", "up", "--build", "--force-recreate", "--no-deps"], cwd=thisdir) if not self._wait_for_meshcentral(): self.__exit__(None, None, None) raise Exception("Failed to create docker instance") diff --git a/tests/environment/config/meshcentral/overrides/.gitignore b/tests/environment/config/meshcentral/overrides/.gitignore new file mode 100644 index 0000000..86d0cb2 --- /dev/null +++ b/tests/environment/config/meshcentral/overrides/.gitignore @@ -0,0 +1,4 @@ +# Ignore everything in this directory +* +# Except this file +!.gitignore \ No newline at end of file diff --git a/tests/environment/meshcentral.dockerfile b/tests/environment/meshcentral.dockerfile index b2e213a..af192ae 100644 --- a/tests/environment/meshcentral.dockerfile +++ b/tests/environment/meshcentral.dockerfile @@ -4,4 +4,5 @@ RUN apk add python3 WORKDIR /opt/meshcentral/ COPY ./scripts/meshcentral ./scripts COPY ./config/meshcentral/data /opt/meshcentral/meshcentral-data +COPY ./config/meshcentral/overrides /opt/meshcentral/meshcentral CMD ["python3", "/opt/meshcentral/scripts/create_users.py"] \ No newline at end of file diff --git a/tests/test_session.py b/tests/test_session.py index 74d6e88..3bc6b7e 100644 --- a/tests/test_session.py +++ b/tests/test_session.py @@ -5,6 +5,8 @@ import meshctrl import requests import random import io +import traceback +import time thisdir = os.path.dirname(os.path.realpath(__file__)) async def test_admin(env): @@ -77,6 +79,17 @@ async def test_users(env): pass else: raise Exception("Connected with no password") + + start = time.time() + try: + async with meshctrl.Session(env.mcurl, user="admin", password="The wrong password", ignore_ssl=True) as admin_session: + pass + except* meshctrl.exceptions.ServerError as eg: + assert str(eg.exceptions[0]) == "Invalid Auth" or eg.exceptions[0].message == "Invalid Auth", "Didn't get invalid auth message" + assert time.time() - start < 10, "Invalid auth wasn't raised until after timeout" + pass + else: + raise Exception("Connected with bad password") async with meshctrl.Session(env.mcurl+"/", user="admin", password=env.users["admin"], ignore_ssl=True) as admin_session,\ meshctrl.Session(env.mcurl, user="privileged", password=env.users["privileged"], ignore_ssl=True) as privileged_session,\ meshctrl.Session(env.mcurl, user="unprivileged", password=env.users["unprivileged"], ignore_ssl=True) as unprivileged_session: @@ -187,21 +200,22 @@ async def test_mesh_device(env): assert r[0].description == "New description", "Description either failed to change, or was changed by a user without permission to do so" - with env.create_agent(mesh.short_meshid) as agent: + with env.create_agent(mesh.short_meshid) as agent,\ + env.create_agent(mesh.short_meshid) as agent2: # Test agent added to device group being propagated correctly # Create agent isn't so good at waiting for the agent to show in the sessions. Give it a couple seconds to appear. for i in range(3): try: r = await admin_session.list_devices(timeout=10) print("\ninfo list_devices: {}\n".format(r)) - assert len(r) == 1, "Incorrect number of agents connected" + assert len(r) == 2, "Incorrect number of agents connected" except: if i == 2: raise await asyncio.sleep(1) else: break - assert len(await privileged_session.list_devices(timeout=10)) == 1, "Incorrect number of agents connected" + assert len(await privileged_session.list_devices(timeout=10)) == 2, "Incorrect number of agents connected" assert len(await unprivileged_session.list_devices(timeout=10)) == 0, "Unprivileged account has access to agent it should not" r = await admin_session.list_devices(details=True, timeout=10) @@ -221,9 +235,12 @@ async def test_mesh_device(env): assert await admin_session.edit_device(agent.nodeid, consent=meshctrl.constants.ConsentFlags.none, timeout=10), "Failed to edit device info" # Test run_commands - r = await admin_session.run_command(agent.nodeid, "ls", timeout=10) + r = await admin_session.run_command([agent.nodeid, agent2.nodeid], "ls", timeout=10) print("\ninfo run_command: {}\n".format(r)) assert "meshagent" in r[agent.nodeid]["result"], "ls gave incorrect data" + assert "meshagent" in r[agent2.nodeid]["result"], "ls gave incorrect data" + assert "Run commands completed." not in r[agent.nodeid]["result"], "Didn't parse run command ending correctly" + assert "Run commands completed." not in r[agent2.nodeid]["result"], "Didn't parse run command ending correctly" assert "meshagent" in (await privileged_session.run_command(agent.nodeid, "ls", timeout=10))[agent.nodeid]["result"], "ls gave incorrect data" # Test run commands with ndividual device permissions