diff --git a/gns3server/controller/link.py b/gns3server/controller/link.py index e6929a6c..6f82b144 100644 --- a/gns3server/controller/link.py +++ b/gns3server/controller/link.py @@ -43,18 +43,38 @@ class Link: self._streaming_pcap = None @asyncio.coroutine - def add_node(self, node, adapter_number, port_number): + def add_node(self, node, adapter_number, port_number, label=None): """ Add a node to the link """ + + if label is None: + label = { + "x": -10, + "y": -10, + "text": "{}/{}".format(adapter_number, port_number), + "style": "font-size: 10; font-style: Verdana" + } + self._nodes.append({ "node": node, "adapter_number": adapter_number, - "port_number": port_number + "port_number": port_number, + "label": label }) if len(self._nodes) == 2: self._project.controller.notification.emit("link.created", self.__json__()) + + self._project.dump() + + @asyncio.coroutine + def update_node(self, node, adapter_number, port_number, label=None): + for port in self._nodes: + if port["node"] == node: + if label: + port["label"] = label + self._project.controller.notification.emit("link.updated", self.__json__()) self._project.dump() @asyncio.coroutine @@ -166,7 +186,8 @@ class Link: res.append({ "node_id": side["node"].id, "adapter_number": side["adapter_number"], - "port_number": side["port_number"] + "port_number": side["port_number"], + "label": side["label"] }) if topology_dump: return { diff --git a/gns3server/handlers/api/controller/link_handler.py b/gns3server/handlers/api/controller/link_handler.py index aa0cccd5..c2163c22 100644 --- a/gns3server/handlers/api/controller/link_handler.py +++ b/gns3server/handlers/api/controller/link_handler.py @@ -66,11 +66,38 @@ class LinkHandler: for node in request.json["nodes"]: yield from link.add_node(project.get_node(node["node_id"]), node.get("adapter_number", 0), - node.get("port_number", 0)) + node.get("port_number", 0), + label=node.get("label")) yield from link.create() response.set_status(201) response.json(link) + @Route.put( + r"/projects/{project_id}/links/{link_id}", + parameters={ + "project_id": "Project UUID", + "link_id": "Link UUID" + }, + status_codes={ + 201: "Link updated", + 400: "Invalid request" + }, + description="Update a link instance", + input=LINK_OBJECT_SCHEMA, + output=LINK_OBJECT_SCHEMA) + def update(request, response): + + controller = Controller.instance() + project = controller.get_project(request.match_info["project_id"]) + link = project.get_link(request.match_info["link_id"]) + for node in request.json["nodes"]: + yield from link.update_node(project.get_node(node["node_id"]), + node.get("adapter_number", 0), + node.get("port_number", 0), + label=node.get("label")) + response.set_status(201) + response.json(link) + @Route.post( r"/projects/{project_id}/links/{link_id}/start_capture", parameters={ diff --git a/gns3server/schemas/link.py b/gns3server/schemas/link.py index 5e7b07c4..ba64e854 100644 --- a/gns3server/schemas/link.py +++ b/gns3server/schemas/link.py @@ -15,6 +15,8 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . +from .label import LABEL_OBJECT_SCHEMA + LINK_OBJECT_SCHEMA = { "$schema": "http://json-schema.org/draft-04/schema#", @@ -55,7 +57,8 @@ LINK_OBJECT_SCHEMA = { "port_number": { "description": "Port number", "type": "integer" - } + }, + "label": LABEL_OBJECT_SCHEMA }, "required": ["node_id", "adapter_number", "port_number"], "additionalProperties": False diff --git a/tests/controller/test_link.py b/tests/controller/test_link.py index 91eee08a..afd5ad96 100644 --- a/tests/controller/test_link.py +++ b/tests/controller/test_link.py @@ -50,20 +50,53 @@ def link(async_run, project, compute): return link -def test_addNode(async_run, project, compute): +def test_add_node(async_run, project, compute): node1 = Node(project, compute, "node1") link = Link(project) + link._project.controller.notification.emit = MagicMock() project.dump = AsyncioMagicMock() async_run(link.add_node(node1, 0, 4)) assert link._nodes == [ { "node": node1, "adapter_number": 0, - "port_number": 4 + "port_number": 4, + 'label': { + 'y': -10, + 'text': '0/4', + 'x': -10, + 'style': 'font-size: 10; font-style: Verdana' + } } ] assert project.dump.called + assert not link._project.controller.notification.emit.called + + # We call link.created only when both side are created + node2 = Node(project, compute, "node2") + async_run(link.add_node(node2, 0, 4)) + + link._project.controller.notification.emit.assert_called_with("link.created", link.__json__()) + + +def test_update_node(async_run, project, compute): + node1 = Node(project, compute, "node1") + + link = Link(project) + async_run(link.add_node(node1, 0, 4)) + label = { + 'y': -42, + 'text': '0/4', + 'x': -10, + 'style': 'font-size: 10; font-style: Verdana' + } + project.dump = AsyncioMagicMock() + link._project.controller.notification.emit = MagicMock() + async_run(link.update_node(node1, 0, 4, label=label)) + assert link._nodes[0]["label"]["y"] == -42 + assert project.dump.called + link._project.controller.notification.emit.assert_called_with("link.updated", link.__json__()) def test_json(async_run, project, compute): @@ -80,12 +113,24 @@ def test_json(async_run, project, compute): { "node_id": node1.id, "adapter_number": 0, - "port_number": 4 + "port_number": 4, + 'label': { + 'y': -10, + 'text': '0/4', + 'x': -10, + 'style': 'font-size: 10; font-style: Verdana' + } }, { "node_id": node2.id, "adapter_number": 1, - "port_number": 3 + "port_number": 3, + 'label': { + 'y': -10, + 'text': '1/3', + 'x': -10, + 'style': 'font-size: 10; font-style: Verdana' + } } ], "capturing": False, @@ -98,12 +143,24 @@ def test_json(async_run, project, compute): { "node_id": node1.id, "adapter_number": 0, - "port_number": 4 + "port_number": 4, + 'label': { + 'y': -10, + 'text': '0/4', + 'x': -10, + 'style': 'font-size: 10; font-style: Verdana' + } }, { "node_id": node2.id, "adapter_number": 1, - "port_number": 3 + "port_number": 3, + 'label': { + 'y': -10, + 'text': '1/3', + 'x': -10, + 'style': 'font-size: 10; font-style: Verdana' + } } ] } diff --git a/tests/handlers/api/controller/test_link.py b/tests/handlers/api/controller/test_link.py index eeb4bce3..67780d28 100644 --- a/tests/handlers/api/controller/test_link.py +++ b/tests/handlers/api/controller/test_link.py @@ -62,7 +62,12 @@ def test_create_link(http_controller, tmpdir, project, compute, async_run): { "node_id": node1.id, "adapter_number": 0, - "port_number": 3 + "port_number": 3, + "label": { + "text": "Text", + "x": 42, + "y": 0 + } }, { "node_id": node2.id, @@ -75,6 +80,60 @@ def test_create_link(http_controller, tmpdir, project, compute, async_run): assert response.status == 201 assert response.json["link_id"] is not None assert len(response.json["nodes"]) == 2 + assert response.json["nodes"][0]["label"]["x"] == 42 + + +def test_update_link(http_controller, tmpdir, project, compute, async_run): + response = MagicMock() + response.json = {"console": 2048} + compute.post = AsyncioMagicMock(return_value=response) + + node1 = async_run(project.add_node(compute, "node1", None)) + node2 = async_run(project.add_node(compute, "node2", None)) + + with asyncio_patch("gns3server.controller.udp_link.UDPLink.create") as mock: + response = http_controller.post("/projects/{}/links".format(project.id), { + "nodes": [ + { + "node_id": node1.id, + "adapter_number": 0, + "port_number": 3, + "label": { + "text": "Text", + "x": 42, + "y": 0 + } + }, + { + "node_id": node2.id, + "adapter_number": 2, + "port_number": 4 + } + ] + }) + link_id = response.json["link_id"] + assert response.json["nodes"][0]["label"]["x"] == 42 + response = http_controller.put("/projects/{}/links/{}".format(project.id, link_id), { + "nodes": [ + { + "node_id": node1.id, + "adapter_number": 0, + "port_number": 3, + "label": { + "text": "Hello", + "x": 64, + "y": 0 + } + }, + { + "node_id": node2.id, + "adapter_number": 2, + "port_number": 4 + } + ] + }) + assert response.status == 201 + assert response.json["nodes"][0]["label"]["x"] == 64 def test_list_link(http_controller, tmpdir, project, compute, async_run):