From 6aa2afcf541c33f2d751dde2b2c0db46d458cc49 Mon Sep 17 00:00:00 2001 From: ziajka Date: Tue, 27 Jun 2017 10:09:21 +0200 Subject: [PATCH] Fix #557 - mac addess collision when running IOU on multiple GNS3 servers --- gns3server/compute/iou/iou_vm.py | 27 +++++++-- .../compute/iou/utils/application_id.py | 36 ++++++++++++ gns3server/controller/project.py | 5 +- gns3server/schemas/iou.py | 10 +++- tests/compute/iou/test_iou_vm.py | 11 ++++ .../compute/iou/utils/test_application_id.py | 38 +++++++++++++ tests/controller/test_project.py | 57 +++++++++---------- 7 files changed, 148 insertions(+), 36 deletions(-) create mode 100644 gns3server/compute/iou/utils/application_id.py create mode 100644 tests/compute/iou/utils/test_application_id.py diff --git a/gns3server/compute/iou/iou_vm.py b/gns3server/compute/iou/iou_vm.py index 2823e751..c99f18e6 100644 --- a/gns3server/compute/iou/iou_vm.py +++ b/gns3server/compute/iou/iou_vm.py @@ -86,6 +86,7 @@ class IOUVM(BaseNode): self._startup_config = "" self._private_config = "" self._ram = 256 # Megabytes + self._application_id = None self._l1_keepalives = False # used to overcome the always-up Ethernet interfaces (not supported by all IOSes). def _config(self): @@ -306,11 +307,6 @@ class IOUVM(BaseNode): super(IOUVM, IOUVM).name.__set__(self, new_name) - @property - def application_id(self): - - return self._manager.get_application_id(self.id) - @property def iourc_content(self): @@ -1065,6 +1061,27 @@ class IOUVM(BaseNode): else: return None + @property + def application_id(self): + """ + Returns application_id which unique identifier for IOU running script. Value is between 1 and 512. + When it's not set returns value from the local manager. + + :returns: integer between 1 and 512 + """ + if self._application_id is None: + return self._manager.get_application_id(self.id) + return self._application_id + + @application_id.setter + def application_id(self, application_id): + """ + Sets application_id for IOU. + + :param: integer between 1 and 512 + """ + self._application_id = application_id + def extract_configs(self): """ Gets the contents of the config files diff --git a/gns3server/compute/iou/utils/application_id.py b/gns3server/compute/iou/utils/application_id.py new file mode 100644 index 00000000..d3de5fe4 --- /dev/null +++ b/gns3server/compute/iou/utils/application_id.py @@ -0,0 +1,36 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2017 GNS3 Technologies Inc. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +from ..iou_error import IOUError + +import logging +log = logging.getLogger(__name__) + + +def get_next_application_id(nodes): + """ + Calculates free application_id from given nodes + :param nodes: + :raises IOUError when exceeds number + :return: integer first free id + """ + used = set([n.properties.get('application_id') for n in nodes if n.node_type == 'iou']) + pool = set(range(1, 512)) + try: + return (pool - used).pop() + except KeyError: + raise IOUError("Cannot create a new IOU VM (limit of 512 VMs reached)") diff --git a/gns3server/controller/project.py b/gns3server/controller/project.py index da594bf2..fc6e8dac 100644 --- a/gns3server/controller/project.py +++ b/gns3server/controller/project.py @@ -39,7 +39,7 @@ from ..utils.asyncio.pool import Pool from ..utils.asyncio import locked_coroutine from .export_project import export_project from .import_project import import_project - +from ..compute.iou.utils.application_id import get_next_application_id import logging log = logging.getLogger(__name__) @@ -353,6 +353,9 @@ class Project: if node_id in self._nodes: return self._nodes[node_id] + if node_type == "iou" and 'application_id' not in kwargs.keys(): + kwargs['application_id'] = get_next_application_id(self._nodes.values()) + node = Node(self, compute, name, node_id=node_id, node_type=node_type, **kwargs) if compute not in self._project_created_on_compute: # For a local server we send the project path diff --git a/gns3server/schemas/iou.py b/gns3server/schemas/iou.py index 389dea6e..775b5d33 100644 --- a/gns3server/schemas/iou.py +++ b/gns3server/schemas/iou.py @@ -86,6 +86,10 @@ IOU_CREATE_SCHEMA = { "description": "Private-config of IOU", "type": ["string", "null"] }, + "application_id": { + "description": "Application ID for running IOU image", + "type": ["integer", "null"] + }, }, "additionalProperties": False, "required": ["name", "path"] @@ -182,7 +186,11 @@ IOU_OBJECT_SCHEMA = { "command_line": { "description": "Last command line used by GNS3 to start QEMU", "type": "string" - } + }, + "application_id": { + "description": "Application ID for running IOU image", + "type": ["integer", "null"] + }, }, "additionalProperties": False } diff --git a/tests/compute/iou/test_iou_vm.py b/tests/compute/iou/test_iou_vm.py index 83e65041..8f899f7c 100644 --- a/tests/compute/iou/test_iou_vm.py +++ b/tests/compute/iou/test_iou_vm.py @@ -434,3 +434,14 @@ def test_extract_configs(vm): startup_config, private_config = vm.extract_configs() assert len(startup_config) == 1392 assert len(private_config) == 0 + + +def test_application_id(project, manager): + """ + Checks if uses local manager to get application_id when not set + """ + vm = IOUVM("test", str(uuid.uuid4()), project, manager) + assert vm.application_id == 1 + + vm.application_id = 3 + assert vm.application_id == 3 diff --git a/tests/compute/iou/utils/test_application_id.py b/tests/compute/iou/utils/test_application_id.py new file mode 100644 index 00000000..e7302c5c --- /dev/null +++ b/tests/compute/iou/utils/test_application_id.py @@ -0,0 +1,38 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2017 GNS3 Technologies Inc. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import pytest +from unittest.mock import MagicMock +from gns3server.compute.iou.utils.application_id import get_next_application_id, IOUError + + +def test_get_next_application_id(): + # test first node + assert get_next_application_id([]) == 1 + + # test second node + nodes = [ + MagicMock(node_type='different'), + MagicMock(node_type='iou', properties=dict(application_id=1)) + ] + assert get_next_application_id(nodes) == 2 + + # test reach out the limit + nodes = [MagicMock(node_type='iou', properties=dict(application_id=i)) for i in range(1, 512)] + + with pytest.raises(IOUError): + get_next_application_id(nodes) diff --git a/tests/controller/test_project.py b/tests/controller/test_project.py index 0c9a929f..d75d1f79 100644 --- a/tests/controller/test_project.py +++ b/tests/controller/test_project.py @@ -217,28 +217,6 @@ def test_add_node_from_appliance(async_run, controller): controller.notification.emit.assert_any_call("node.created", node.__json__()) -def test_create_iou_on_multiple_node(async_run, controller): - """ - Due to mac address collision you can't create an IOU node - on two different server - """ - compute = MagicMock() - compute.id = "remote" - - compute2 = MagicMock() - compute2.id = "remote2" - - project = Project(controller=controller, name="Test") - - response = MagicMock() - response.json = {"console": 2048} - compute.post = AsyncioMagicMock(return_value=response) - - node1 = async_run(project.add_node(compute, "test", None, node_type="iou")) - with pytest.raises(aiohttp.web_exceptions.HTTPConflict): - async_run(project.add_node(compute2, "test2", None, node_type="iou")) - - def test_delete_node(async_run, controller): """ For a local server we send the project path @@ -306,7 +284,7 @@ def test_get_node(async_run, controller): project.get_node(vm.id) -def test_addLink(async_run, project, controller): +def test_add_link(async_run, project, controller): compute = MagicMock() response = MagicMock() @@ -327,7 +305,7 @@ def test_addLink(async_run, project, controller): controller.notification.emit.assert_any_call("link.created", link.__json__()) -def test_getLink(async_run, project): +def test_get_link(async_run, project): compute = MagicMock() response = MagicMock() @@ -341,7 +319,7 @@ def test_getLink(async_run, project): project.get_link("test") -def test_deleteLink(async_run, project, controller): +def test_delete_link(async_run, project, controller): compute = MagicMock() response = MagicMock() @@ -357,7 +335,7 @@ def test_deleteLink(async_run, project, controller): assert len(project._links) == 0 -def test_addDrawing(async_run, project, controller): +def test_add_drawing(async_run, project, controller): controller.notification.emit = MagicMock() drawing = async_run(project.add_drawing(None, svg="")) @@ -365,7 +343,7 @@ def test_addDrawing(async_run, project, controller): controller.notification.emit.assert_any_call("drawing.created", drawing.__json__()) -def test_getDrawing(async_run, project): +def test_get_drawing(async_run, project): drawing = async_run(project.add_drawing(None)) assert project.get_drawing(drawing.id) == drawing @@ -373,7 +351,7 @@ def test_getDrawing(async_run, project): project.get_drawing("test") -def test_deleteDrawing(async_run, project, controller): +def test_delete_drawing(async_run, project, controller): assert len(project._drawings) == 0 drawing = async_run(project.add_drawing()) assert len(project._drawings) == 1 @@ -383,7 +361,7 @@ def test_deleteDrawing(async_run, project, controller): assert len(project._drawings) == 0 -def test_cleanPictures(async_run, project, controller): +def test_clean_pcictures(async_run, project, controller): """ When a project is close old pictures should be removed """ @@ -600,3 +578,24 @@ def test_node_name(project, async_run): assert node.name == "helloworld-1" node = async_run(project.add_node(compute, "hello world-{0}", None, node_type="vpcs", properties={"startup_config": "test.cfg"})) assert node.name == "helloworld-2" + + +def test_add_iou_node_and_check_if_gets_application_id(project, async_run): + compute = MagicMock() + compute.id = "local" + response = MagicMock() + response.json = {"console": 2048} + compute.post = AsyncioMagicMock(return_value=response) + + # tests if get_next_application_id is called + with patch('gns3server.controller.project.get_next_application_id', return_value=222) as mocked_get_app_id: + results = async_run(project.add_node( + compute, "test", None, node_type="iou", properties={"startup_config": "test.cfg"})) + assert mocked_get_app_id.called + assert results.properties['application_id'] == 222 + + # tests if we can send property and it will be used + results = async_run(project.add_node( + compute, "test", None, node_type="iou", application_id=333, properties={"startup_config": "test.cfg"})) + assert mocked_get_app_id.called + assert results.properties['application_id'] == 333 \ No newline at end of file