From 54747ee6186cd8215f07ba7fb4e6c9a8386de84c Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Wed, 18 May 2016 18:37:18 +0200 Subject: [PATCH] Support for link event, fix link not correctly deleted --- docs/general.rst | 3 ++ gns3server/compute/base_node.py | 2 - gns3server/controller/__init__.py | 2 +- gns3server/controller/link.py | 6 +++ gns3server/controller/project.py | 18 ++++++- gns3server/controller/udp_link.py | 19 ++++--- .../handlers/api/controller/link_handler.py | 4 +- gns3server/schemas/link.py | 7 +++ tests/controller/test_link.py | 5 +- tests/controller/test_node.py | 4 +- tests/controller/test_project.py | 50 ++++++++++++++----- tests/controller/test_udp_link.py | 4 +- 12 files changed, 92 insertions(+), 32 deletions(-) diff --git a/docs/general.rst b/docs/general.rst index e897fac4..333bf273 100644 --- a/docs/general.rst +++ b/docs/general.rst @@ -208,6 +208,9 @@ The available notification are: * node.created * node.updated * node.deleted + * link.created + * link.updated + * link.deleted * log.error * log.warning * log.info diff --git a/gns3server/compute/base_node.py b/gns3server/compute/base_node.py index 492f9ab4..a933efb0 100644 --- a/gns3server/compute/base_node.py +++ b/gns3server/compute/base_node.py @@ -223,7 +223,6 @@ class BaseNode: log.info("{module}: {name} [{id}] created".format(module=self.manager.module_name, name=self.name, id=self.id)) - self._project.emit("node.created", self) @asyncio.coroutine def delete(self): @@ -231,7 +230,6 @@ class BaseNode: Delete the node (including all its files). """ - self._project.emit("node.deleted", self) directory = self.project.node_working_directory(self) if os.path.exists(directory): try: diff --git a/gns3server/controller/__init__.py b/gns3server/controller/__init__.py index fc8e99c2..488269c4 100644 --- a/gns3server/controller/__init__.py +++ b/gns3server/controller/__init__.py @@ -151,7 +151,7 @@ class Controller: :param kwargs: See the documentation of Project """ if project_id not in self._projects: - project = Project(project_id=project_id, **kwargs) + project = Project(project_id=project_id, controller=self, **kwargs) self._projects[project.id] = project for compute_server in self._computes.values(): yield from project.add_compute(compute_server) diff --git a/gns3server/controller/link.py b/gns3server/controller/link.py index 321a7dcf..302b4fa7 100644 --- a/gns3server/controller/link.py +++ b/gns3server/controller/link.py @@ -45,6 +45,8 @@ class Link: "adapter_number": adapter_number, "port_number": port_number }) + if len(self._nodes) == 2: + self._project.controller.notification.emit("link.created", self.__json__()) @asyncio.coroutine def create(self): @@ -70,6 +72,7 @@ class Link: self._capturing = True self._capture_file_name = capture_file_name self._streaming_pcap = asyncio.async(self._start_streaming_pcap()) + self._project.controller.notification.emit("link.updated", self.__json__()) @asyncio.coroutine def _start_streaming_pcap(self): @@ -94,6 +97,8 @@ class Link: Stop capture on the link """ self._capturing = False + self._project.controller.notification.emit("link.updated", self.__json__()) + @asyncio.coroutine def read_pcap_from_source(self): @@ -142,6 +147,7 @@ class Link: }) return { "nodes": res, "link_id": self._id, + "project_id": self._project.id, "capturing": self._capturing, "capture_file_name": self._capture_file_name, "capture_file_path": self.capture_file_path diff --git a/gns3server/controller/project.py b/gns3server/controller/project.py index 7213806e..cc16be51 100644 --- a/gns3server/controller/project.py +++ b/gns3server/controller/project.py @@ -37,8 +37,9 @@ class Project: :param temporary: boolean to tell if the project is a temporary project (destroy when closed) """ - def __init__(self, name=None, project_id=None, path=None, temporary=False): + def __init__(self, name=None, project_id=None, path=None, temporary=False, controller=None): + self._controller = controller self._name = name if project_id is None: self._id = str(uuid4()) @@ -61,6 +62,10 @@ class Project: # Create the project on demand on the compute node self._project_created_on_compute = set() + @property + def controller(self): + return self._controller + @property def name(self): return self._name @@ -134,6 +139,7 @@ class Project: self._project_created_on_compute.add(compute) yield from node.create() self._nodes[node.id] = node + self.controller.notification.emit("node.created", node.__json__()) return node return self._nodes[node_id] @@ -141,7 +147,8 @@ class Project: def delete_node(self, node_id): node = self.get_node(node_id) del self._nodes[node.id] - yield from node.delete() + yield from node.destroy() + self.controller.notification.emit("node.deleted", node.__json__()) def get_node(self, node_id): """ @@ -168,6 +175,13 @@ class Project: self._links[link.id] = link return link + @asyncio.coroutine + def delete_link(self, link_id): + link = self.get_link(link_id) + del self._links[link.id] + yield from link.delete() + self.controller.notification.emit("link.deleted", link.__json__()) + def get_link(self, link_id): """ Return the Link or raise a 404 if the link is unknown diff --git a/gns3server/controller/udp_link.py b/gns3server/controller/udp_link.py index 4543cfd1..e5a2d4c0 100644 --- a/gns3server/controller/udp_link.py +++ b/gns3server/controller/udp_link.py @@ -69,14 +69,21 @@ class UDPLink(Link): """ Delete the link and free the resources """ - node1 = self._nodes[0]["node"] - adapter_number1 = self._nodes[0]["adapter_number"] - port_number1 = self._nodes[0]["port_number"] - node2 = self._nodes[1]["node"] - adapter_number2 = self._nodes[1]["adapter_number"] - port_number2 = self._nodes[1]["port_number"] + try: + node1 = self._nodes[0]["node"] + adapter_number1 = self._nodes[0]["adapter_number"] + port_number1 = self._nodes[0]["port_number"] + except IndexError: + return yield from node1.delete("/adapters/{adapter_number}/ports/{port_number}/nio".format(adapter_number=adapter_number1, port_number=port_number1)) + + try: + node2 = self._nodes[1]["node"] + adapter_number2 = self._nodes[1]["adapter_number"] + port_number2 = self._nodes[1]["port_number"] + except IndexError: + return yield from node2.delete("/adapters/{adapter_number}/ports/{port_number}/nio".format(adapter_number=adapter_number2, port_number=port_number2)) @asyncio.coroutine diff --git a/gns3server/handlers/api/controller/link_handler.py b/gns3server/handlers/api/controller/link_handler.py index 3d227c5b..30ec9db4 100644 --- a/gns3server/handlers/api/controller/link_handler.py +++ b/gns3server/handlers/api/controller/link_handler.py @@ -128,10 +128,8 @@ class LinkHandler: controller = Controller.instance() project = controller.get_project(request.match_info["project_id"]) - link = project.get_link(request.match_info["link_id"]) - yield from link.delete() + yield from project.delete_link(request.match_info["link_id"]) response.set_status(204) - response.json(link) @Route.get( r"/projects/{project_id}/links/{link_id}/pcap", diff --git a/gns3server/schemas/link.py b/gns3server/schemas/link.py index 6b3cf658..5e7b07c4 100644 --- a/gns3server/schemas/link.py +++ b/gns3server/schemas/link.py @@ -28,6 +28,13 @@ LINK_OBJECT_SCHEMA = { "maxLength": 36, "pattern": "^[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}$" }, + "project_id": { + "description": "Project UUID", + "type": "string", + "minLength": 36, + "maxLength": 36, + "pattern": "^[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}$" + }, "nodes": { "description": "List of the VMS", "type": "array", diff --git a/tests/controller/test_link.py b/tests/controller/test_link.py index 17acbb8b..ba19deda 100644 --- a/tests/controller/test_link.py +++ b/tests/controller/test_link.py @@ -30,8 +30,8 @@ from tests.utils import AsyncioBytesIO @pytest.fixture -def project(): - return Project() +def project(controller): + return Project(controller=controller) @pytest.fixture @@ -73,6 +73,7 @@ def test_json(async_run, project, compute): async_run(link.add_node(node2, 1, 3)) assert link.__json__() == { "link_id": link.id, + "project_id": project.id, "nodes": [ { "node_id": node1.id, diff --git a/tests/controller/test_node.py b/tests/controller/test_node.py index dfcfcadd..0990d282 100644 --- a/tests/controller/test_node.py +++ b/tests/controller/test_node.py @@ -35,8 +35,8 @@ def compute(): @pytest.fixture -def node(compute): - project = Project(str(uuid.uuid4())) +def node(compute, controller): + project = Project(str(uuid.uuid4()), controller=controller) node = Node(project, compute, name="demo", node_id=str(uuid.uuid4()), diff --git a/tests/controller/test_project.py b/tests/controller/test_project.py index a4983bb5..ee03850c 100644 --- a/tests/controller/test_project.py +++ b/tests/controller/test_project.py @@ -27,6 +27,10 @@ from uuid import uuid4 from gns3server.controller.project import Project from gns3server.config import Config +@pytest.fixture +def project(controller): + return Project(controller=controller) + def test_affect_uuid(): p = Project() @@ -76,13 +80,14 @@ def test_add_compute(async_run): assert compute in project._computes -def test_add_node_local(async_run): +def test_add_node_local(async_run, controller): """ For a local server we send the project path """ compute = MagicMock() compute.id = "local" - project = Project() + project = Project(controller=controller) + controller._notification = MagicMock() response = MagicMock() response.json = {"console": 2048} @@ -102,15 +107,17 @@ def test_add_node_local(async_run): 'startup_config': 'test.cfg', 'name': 'test'}) assert compute in project._project_created_on_compute + controller.notification.emit.assert_any_call("node.created", node.__json__()) -def test_add_node_non_local(async_run): +def test_add_node_non_local(async_run, controller): """ For a non local server we do not send the project path """ compute = MagicMock() compute.id = "remote" - project = Project() + project = Project(controller=controller) + controller._notification = MagicMock() response = MagicMock() response.json = {"console": 2048} @@ -128,14 +135,16 @@ def test_add_node_non_local(async_run): 'startup_config': 'test.cfg', 'name': 'test'}) assert compute in project._project_created_on_compute + controller.notification.emit.assert_any_call("node.created", node.__json__()) -def test_delete_node(async_run): +def test_delete_node(async_run, controller): """ For a local server we send the project path """ compute = MagicMock() - project = Project() + project = Project(controller=controller) + controller._notification = MagicMock() response = MagicMock() response.json = {"console": 2048} @@ -147,11 +156,12 @@ def test_delete_node(async_run): assert node.id not in project._nodes compute.delete.assert_any_call('/projects/{}/vpcs/nodes/{}'.format(project.id, node.id)) + controller.notification.emit.assert_any_call("node.deleted", node.__json__()) -def test_getVM(async_run): +def test_getVM(async_run, controller): compute = MagicMock() - project = Project() + project = Project(controller=controller) response = MagicMock() response.json = {"console": 2048} @@ -164,9 +174,8 @@ def test_getVM(async_run): project.get_node("test") -def test_addLink(async_run): +def test_addLink(async_run, project, controller): compute = MagicMock() - project = Project() response = MagicMock() response.json = {"console": 2048} @@ -174,15 +183,16 @@ def test_addLink(async_run): vm1 = async_run(project.add_node(compute, None, name="test1", node_type="vpcs", properties={"startup_config": "test.cfg"})) vm2 = async_run(project.add_node(compute, None, name="test2", node_type="vpcs", properties={"startup_config": "test.cfg"})) + controller._notification = MagicMock() link = async_run(project.add_link()) async_run(link.add_node(vm1, 3, 1)) async_run(link.add_node(vm2, 4, 2)) assert len(link._nodes) == 2 + controller.notification.emit.assert_any_call("link.created", link.__json__()) -def test_getLink(async_run): +def test_getLink(async_run, project): compute = MagicMock() - project = Project() response = MagicMock() response.json = {"console": 2048} @@ -193,3 +203,19 @@ def test_getLink(async_run): with pytest.raises(aiohttp.web_exceptions.HTTPNotFound): project.get_link("test") + + +def test_deleteLink(async_run, project, controller): + compute = MagicMock() + + response = MagicMock() + response.json = {"console": 2048} + compute.post = AsyncioMagicMock(return_value=response) + + assert len(project._links) == 0 + link = async_run(project.add_link()) + assert len(project._links) == 1 + controller._notification = MagicMock() + async_run(project.delete_link(link.id)) + controller.notification.emit.assert_any_call("link.deleted", link.__json__()) + assert len(project._links) == 0 diff --git a/tests/controller/test_udp_link.py b/tests/controller/test_udp_link.py index 6b769958..f00c4674 100644 --- a/tests/controller/test_udp_link.py +++ b/tests/controller/test_udp_link.py @@ -27,8 +27,8 @@ from gns3server.controller.node import Node @pytest.fixture -def project(): - return Project() +def project(controller): + return Project(controller=controller) def test_create(async_run, project):