From e3493870b26294f86c314ad3bef3b29e6bc0a452 Mon Sep 17 00:00:00 2001 From: grossmj Date: Fri, 12 Jan 2024 13:16:55 +1100 Subject: [PATCH] Add project.created, project.opened and project.deleted controller notification stream. Move project.updated and project.closed from project notification to controller notification stream. --- gns3server/controller/project.py | 24 +++++++-- tests/controller/test_compute.py | 4 +- tests/controller/test_project.py | 87 ++++++++++++++++++------------- tests/controller/test_topology.py | 61 +++++++++++----------- 4 files changed, 103 insertions(+), 73 deletions(-) diff --git a/gns3server/controller/project.py b/gns3server/controller/project.py index c4dc3b95..eed15198 100644 --- a/gns3server/controller/project.py +++ b/gns3server/controller/project.py @@ -130,16 +130,27 @@ class Project: self._iou_id_lock = asyncio.Lock() log.debug('Project "{name}" [{id}] loaded'.format(name=self.name, id=self._id)) + self.emit_controller_notification("project.created", self.__json__()) def emit_notification(self, action, event): """ - Emit a notification to all clients using this project. + Emit a project notification to all clients using this project. :param action: Action name :param event: Event to send """ - self.controller.notification.project_emit(action, event, project_id=self.id) + self._controller.notification.project_emit(action, event, project_id=self.id) + + def emit_controller_notification(self, action, event): + """ + Emit a controller notification, all clients will see it. + + :param action: Action name + :param event: Event to send + """ + + self._controller.notification.controller_emit(action, event) async def update(self, **kwargs): """ @@ -154,7 +165,7 @@ class Project: # We send notif only if object has changed if old_json != self.__json__(): - self.emit_notification("project.updated", self.__json__()) + self.emit_controller_notification("project.updated", self.__json__()) self.dump() # update on computes @@ -803,7 +814,8 @@ class Project: self._clean_pictures() self._status = "closed" if not ignore_notification: - self.emit_notification("project.closed", self.__json__()) + self.emit_controller_notification("project.closed", self.__json__()) + self.reset() self._closing = False @@ -857,6 +869,7 @@ class Project: shutil.rmtree(self.path) except OSError as e: raise aiohttp.web.HTTPConflict(text="Cannot delete project directory {}: {}".format(self.path, str(e))) + self.emit_controller_notification("project.deleted", self.__json__()) async def delete_on_computes(self): """ @@ -976,7 +989,7 @@ class Project: await self.add_drawing(dump=False, **drawing_data) self.dump() - # We catch all error to be able to rollback the .gns3 to the previous state + # We catch all error to be able to roll back the .gns3 to the previous state except Exception as e: for compute in list(self._project_created_on_compute): try: @@ -1001,6 +1014,7 @@ class Project: pass self._loading = False + self.emit_controller_notification("project.opened", self.__json__()) # Should we start the nodes when project is open if self._auto_start: # Start all in the background without waiting for completion diff --git a/tests/controller/test_compute.py b/tests/controller/test_compute.py index d7f5329c..b9b911b3 100644 --- a/tests/controller/test_compute.py +++ b/tests/controller/test_compute.py @@ -213,7 +213,9 @@ async def test_compute_httpQuery_project(compute): response = MagicMock() with asyncio_patch("aiohttp.ClientSession.request", return_value=response) as mock: response.status = 200 - project = Project(name="Test") + with patch('gns3server.controller.project.Project.emit_controller_notification') as mock_notification: + project = Project(name="Test") + mock_notification.assert_called() await compute.post("/projects", project) mock.assert_called_with("POST", "https://example.com:84/v2/compute/projects", data=json.dumps(project.__json__()), headers={'content-type': 'application/json'}, auth=None, chunked=None, timeout=20) await compute.close() diff --git a/tests/controller/test_project.py b/tests/controller/test_project.py index bc729fee..fb8bcfeb 100644 --- a/tests/controller/test_project.py +++ b/tests/controller/test_project.py @@ -47,15 +47,19 @@ async def node(controller, project): async def test_affect_uuid(): - p = Project(name="Test") - assert len(p.id) == 36 - p = Project(project_id='00010203-0405-0607-0809-0a0b0c0d0e0f', name="Test 2") - assert p.id == '00010203-0405-0607-0809-0a0b0c0d0e0f' + with patch('gns3server.controller.project.Project.emit_controller_notification') as mock_notification: + p = Project(name="Test") + mock_notification.assert_called() + assert len(p.id) == 36 + p = Project(project_id='00010203-0405-0607-0809-0a0b0c0d0e0f', name="Test 2") + assert p.id == '00010203-0405-0607-0809-0a0b0c0d0e0f' async def test_json(): - p = Project(name="Test") + with patch('gns3server.controller.project.Project.emit_controller_notification') as mock_notification: + p = Project(name="Test") + mock_notification.assert_called() assert p.__json__() == { "name": "Test", @@ -83,11 +87,11 @@ async def test_json(): async def test_update(controller): project = Project(controller=controller, name="Hello") - project.emit_notification = MagicMock() + project.emit_controller_notification = MagicMock() assert project.name == "Hello" await project.update(name="World") assert project.name == "World" - project.emit_notification.assert_any_call("project.updated", project.__json__()) + project.emit_controller_notification.assert_any_call("project.updated", project.__json__()) async def test_update_on_compute(controller): @@ -106,7 +110,9 @@ async def test_path(projects_dir): directory = projects_dir with patch("gns3server.utils.path.get_default_project_directory", return_value=directory): - p = Project(project_id=str(uuid4()), name="Test") + with patch('gns3server.controller.project.Project.emit_controller_notification') as mock_notification: + p = Project(project_id=str(uuid4()), name="Test") + mock_notification.assert_called() assert p.path == os.path.join(directory, p.id) assert os.path.exists(os.path.join(directory, p.id)) @@ -124,23 +130,27 @@ def test_path_exist(tmpdir): async def test_init_path(tmpdir): - p = Project(path=str(tmpdir), project_id=str(uuid4()), name="Test") - assert p.path == str(tmpdir) + with patch('gns3server.controller.project.Project.emit_controller_notification') as mock_notification: + p = Project(path=str(tmpdir), project_id=str(uuid4()), name="Test") + mock_notification.assert_called() + assert p.path == str(tmpdir) @pytest.mark.skipif(sys.platform.startswith("win"), reason="Not supported on Windows") async def test_changing_path_with_quote_not_allowed(tmpdir): with pytest.raises(aiohttp.web.HTTPForbidden): - p = Project(project_id=str(uuid4()), name="Test") - p.path = str(tmpdir / "project\"53") + with patch('gns3server.controller.project.Project.emit_controller_notification'): + p = Project(project_id=str(uuid4()), name="Test") + p.path = str(tmpdir / "project\"53") async def test_captures_directory(tmpdir): - p = Project(path=str(tmpdir / "capturestest"), name="Test") - assert p.captures_directory == str(tmpdir / "capturestest" / "project-files" / "captures") - assert os.path.exists(p.captures_directory) + with patch('gns3server.controller.project.Project.emit_controller_notification'): + p = Project(path=str(tmpdir / "capturestest"), name="Test") + assert p.captures_directory == str(tmpdir / "capturestest" / "project-files" / "captures") + assert os.path.exists(p.captures_directory) async def test_add_node_local(controller): @@ -649,36 +659,39 @@ async def test_dump(projects_dir): directory = projects_dir with patch("gns3server.utils.path.get_default_project_directory", return_value=directory): - p = Project(project_id='00010203-0405-0607-0809-0a0b0c0d0e0f', name="Test") - p.dump() - with open(os.path.join(directory, p.id, "Test.gns3")) as f: - content = f.read() - assert "00010203-0405-0607-0809-0a0b0c0d0e0f" in content + with patch('gns3server.controller.project.Project.emit_controller_notification'): + p = Project(project_id='00010203-0405-0607-0809-0a0b0c0d0e0f', name="Test") + p.dump() + with open(os.path.join(directory, p.id, "Test.gns3")) as f: + content = f.read() + assert "00010203-0405-0607-0809-0a0b0c0d0e0f" in content async def test_open_close(controller): - project = Project(controller=controller, name="Test") - assert project.status == "opened" - await project.close() - project.start_all = AsyncioMagicMock() - await project.open() - assert not project.start_all.called - assert project.status == "opened" - project.emit_notification = MagicMock() - await project.close() - assert project.status == "closed" - project.emit_notification.assert_any_call("project.closed", project.__json__()) + with patch('gns3server.controller.project.Project.emit_controller_notification'): + project = Project(controller=controller, name="Test") + assert project.status == "opened" + await project.close() + project.start_all = AsyncioMagicMock() + await project.open() + assert not project.start_all.called + assert project.status == "opened" + project.emit_controller_notification = MagicMock() + await project.close() + assert project.status == "closed" + project.emit_controller_notification.assert_any_call("project.closed", project.__json__()) async def test_open_auto_start(controller): - project = Project(controller=controller, name="Test", auto_start=True) - assert project.status == "opened" - await project.close() - project.start_all = AsyncioMagicMock() - await project.open() - assert project.start_all.called + with patch('gns3server.controller.project.Project.emit_controller_notification'): + project = Project(controller=controller, name="Test", auto_start=True) + assert project.status == "opened" + await project.close() + project.start_all = AsyncioMagicMock() + await project.open() + assert project.start_all.called def test_is_running(project, node): diff --git a/tests/controller/test_topology.py b/tests/controller/test_topology.py index 4ac24259..4499ddce 100644 --- a/tests/controller/test_topology.py +++ b/tests/controller/test_topology.py @@ -19,7 +19,7 @@ import json import uuid import pytest import aiohttp -from unittest.mock import MagicMock +from unittest.mock import MagicMock, patch from tests.utils import asyncio_patch from gns3server.controller.project import Project @@ -30,35 +30,36 @@ from gns3server.version import __version__ async def test_project_to_topology_empty(tmpdir): - project = Project(name="Test") - topo = project_to_topology(project) - assert topo == { - "project_id": project.id, - "name": "Test", - "auto_start": False, - "auto_close": True, - "auto_open": False, - "scene_width": 2000, - "scene_height": 1000, - "revision": GNS3_FILE_FORMAT_REVISION, - "zoom": 100, - "show_grid": False, - "show_interface_labels": False, - "show_layers": False, - "snap_to_grid": False, - "grid_size": 75, - "drawing_grid_size": 25, - "topology": { - "nodes": [], - "links": [], - "computes": [], - "drawings": [] - }, - "type": "topology", - "supplier": None, - "variables": None, - "version": __version__ - } + with patch('gns3server.controller.project.Project.emit_controller_notification'): + project = Project(name="Test") + topo = project_to_topology(project) + assert topo == { + "project_id": project.id, + "name": "Test", + "auto_start": False, + "auto_close": True, + "auto_open": False, + "scene_width": 2000, + "scene_height": 1000, + "revision": GNS3_FILE_FORMAT_REVISION, + "zoom": 100, + "show_grid": False, + "show_interface_labels": False, + "show_layers": False, + "snap_to_grid": False, + "grid_size": 75, + "drawing_grid_size": 25, + "topology": { + "nodes": [], + "links": [], + "computes": [], + "drawings": [] + }, + "type": "topology", + "supplier": None, + "variables": None, + "version": __version__ + } async def test_basic_topology(controller):