From 6d36429870f739496df5006efb177a60f7a210f2 Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Wed, 15 Jun 2016 15:12:38 +0200 Subject: [PATCH] Reload a topology work --- gns3server/controller/__init__.py | 6 +-- gns3server/controller/compute.py | 14 ++++++- gns3server/controller/link.py | 11 ++++- gns3server/controller/node.py | 30 +++++++++++++- gns3server/controller/project.py | 41 +++++++++++-------- gns3server/controller/topology.py | 6 +-- .../api/controller/project_handler.py | 22 +++++++++- gns3server/schemas/vpcs.py | 4 ++ tests/controller/test_compute.py | 8 ++++ tests/controller/test_link.py | 19 ++++++++- tests/controller/test_node.py | 14 +++++++ tests/controller/test_project.py | 9 ++++ tests/controller/test_topology.py | 6 +-- tests/handlers/api/controller/test_project.py | 11 ++++- 14 files changed, 167 insertions(+), 34 deletions(-) diff --git a/gns3server/controller/__init__.py b/gns3server/controller/__init__.py index e449ed75..ca64408c 100644 --- a/gns3server/controller/__init__.py +++ b/gns3server/controller/__init__.py @@ -92,6 +92,7 @@ class Controller: # Preload the list of projects from disk server_config = Config.instance().get_section_config("Server") projects_path = os.path.expanduser(server_config.get("projects_path", "~/GNS3/projects")) + os.makedirs(projects_path, exist_ok=True) try: for project_path in os.listdir(projects_path): project_dir = os.path.join(projects_path, project_path) @@ -101,11 +102,10 @@ class Controller: try: yield from self.load_project(os.path.join(project_dir, file), load=False) except aiohttp.web_exceptions.HTTPConflict: - pass # Skip not compatible projects + pass # Skip not compatible projects except OSError as e: log.error(str(e)) - def is_enabled(self): """ :returns: whether the current instance is the controller @@ -219,7 +219,7 @@ class Controller: project = yield from self.add_project(path=os.path.dirname(path), status="closed", **topo_data) if load: - yield from project.load() + yield from project.open() @property def projects(self): diff --git a/gns3server/controller/compute.py b/gns3server/controller/compute.py index c6db4cc0..80071f2b 100644 --- a/gns3server/controller/compute.py +++ b/gns3server/controller/compute.py @@ -228,7 +228,19 @@ class Compute: def password(self, value): self._set_auth(self._user, value) - def __json__(self): + def __json__(self, topology_dump=False): + """ + :param topology_dump: Filter to keep only properties require for saving on disk + """ + if topology_dump: + return { + "compute_id": self._id, + "name": self._name, + "protocol": self._protocol, + "host": self._host, + "port": self._port, + "user": self._user + } return { "compute_id": self._id, "name": self._name, diff --git a/gns3server/controller/link.py b/gns3server/controller/link.py index 4ef43257..e6929a6c 100644 --- a/gns3server/controller/link.py +++ b/gns3server/controller/link.py @@ -55,6 +55,7 @@ class Link: if len(self._nodes) == 2: self._project.controller.notification.emit("link.created", self.__json__()) + self._project.dump() @asyncio.coroutine def create(self): @@ -156,7 +157,10 @@ class Link: else: return None - def __json__(self): + def __json__(self, topology_dump=False): + """ + :param topology_dump: Filter to keep only properties require for saving on disk + """ res = [] for side in self._nodes: res.append({ @@ -164,6 +168,11 @@ class Link: "adapter_number": side["adapter_number"], "port_number": side["port_number"] }) + if topology_dump: + return { + "nodes": res, + "link_id": self._id + } return { "nodes": res, "link_id": self._id, diff --git a/gns3server/controller/node.py b/gns3server/controller/node.py index 0016a0de..8f63f393 100644 --- a/gns3server/controller/node.py +++ b/gns3server/controller/node.py @@ -25,6 +25,10 @@ from .compute import ComputeConflict from ..utils.images import images_directories +import logging +log = logging.getLogger(__name__) + + class Node: # This properties are used only on controller and are not forwarded to the compute CONTROLLER_ONLY_PROPERTIES = ["x", "y", "z", "symbol", "label", "console_host"] @@ -68,7 +72,11 @@ class Node: } # Update node properties with additional elements for prop in kwargs: - setattr(self, prop, kwargs[prop]) + try: + setattr(self, prop, kwargs[prop]) + except AttributeError as e: + log.critical("Can't set attribute %s", prop) + raise e @property def id(self): @@ -370,7 +378,25 @@ class Node: def __repr__(self): return "".format(self._node_type, self._name) - def __json__(self): + def __json__(self, topology_dump=False): + """ + :param topology_dump: Filter to keep only properties require for saving on disk + """ + if topology_dump: + return { + "compute_id": str(self._compute.id), + "node_id": self._id, + "node_type": self._node_type, + "name": self._name, + "console": self._console, + "console_type": self._console_type, + "properties": self._properties, + "label": self._label, + "x": self._x, + "y": self._y, + "z": self._z, + "symbol": self._symbol + } return { "compute_id": str(self._compute.id), "project_id": self._project.id, diff --git a/gns3server/controller/project.py b/gns3server/controller/project.py index f524addf..aafca8d3 100644 --- a/gns3server/controller/project.py +++ b/gns3server/controller/project.py @@ -60,7 +60,12 @@ class Project: if path is None: path = os.path.join(get_default_project_directory(), self._id) self.path = path + self.reset() + def reset(self): + """ + Called when open/close a project. Cleanup internal stuff + """ self._allocated_node_names = set() self._nodes = {} self._links = {} @@ -291,7 +296,8 @@ class Project: def close(self): for compute in self._project_created_on_compute: yield from compute.post("/projects/{}/close".format(self._id)) - self._allocated_node_names.clear() + self.reset() + self._status = "closed" @asyncio.coroutine def delete(self): @@ -324,24 +330,27 @@ class Project: return os.path.join(self.path, filename) @asyncio.coroutine - def load(self): + def open(self): """ Load topology elements """ + self.reset() path = self._topology_file() - topology = load_topology(path)["topology"] - for compute in topology["computes"]: - yield from self.controller.add_compute(**compute) - for node in topology["nodes"]: - compute = self.controller.get_compute(node.pop("compute_id")) - name = node.pop("name") - node_id = node.pop("node_id") - yield from self.add_node(compute, name, node_id, **node) - for link_data in topology["links"]: - link = yield from self.add_link(link_id=link_data["link_id"]) - for node_link in link_data["nodes"]: - node = self.get_node(node_link["node_id"]) - yield from link.add_node(node, node_link["adapter_number"], node_link["port_number"]) + if os.path.exists(path): + topology = load_topology(path)["topology"] + for compute in topology["computes"]: + yield from self.controller.add_compute(**compute) + for node in topology["nodes"]: + compute = self.controller.get_compute(node.pop("compute_id")) + name = node.pop("name") + node_id = node.pop("node_id") + yield from self.add_node(compute, name, node_id, **node) + for link_data in topology["links"]: + link = yield from self.add_link(link_id=link_data["link_id"]) + for node_link in link_data["nodes"]: + node = self.get_node(node_link["node_id"]) + yield from link.add_node(node, node_link["adapter_number"], node_link["port_number"]) + self._status = "opened" def dump(self): """ @@ -362,5 +371,5 @@ class Project: "name": self._name, "project_id": self._id, "path": self._path, - "status": "opened" + "status": self._status } diff --git a/gns3server/controller/topology.py b/gns3server/controller/topology.py index 0fdb5be7..bff056ba 100644 --- a/gns3server/controller/topology.py +++ b/gns3server/controller/topology.py @@ -43,12 +43,12 @@ def project_to_topology(project): computes = set() for node in project.nodes.values(): computes.add(node.compute) - data["topology"]["nodes"].append(node.__json__()) + data["topology"]["nodes"].append(node.__json__(topology_dump=True)) for link in project.links.values(): - data["topology"]["links"].append(link.__json__()) + data["topology"]["links"].append(link.__json__(topology_dump=True)) for compute in computes: if hasattr(compute, "__json__"): - data["topology"]["computes"].append(compute.__json__()) + data["topology"]["computes"].append(compute.__json__(topology_dump=True)) #TODO: check JSON schema return data diff --git a/gns3server/handlers/api/controller/project_handler.py b/gns3server/handlers/api/controller/project_handler.py index 200722ad..31626ae7 100644 --- a/gns3server/handlers/api/controller/project_handler.py +++ b/gns3server/handlers/api/controller/project_handler.py @@ -91,8 +91,26 @@ class ProjectHandler: controller = Controller.instance() project = controller.get_project(request.match_info["project_id"]) yield from project.close() - controller.remove_project(project) - response.set_status(204) + response.set_status(201) + response.json(project) + + @Route.post( + r"/projects/{project_id}/open", + description="Open a project", + parameters={ + "project_id": "Project UUID", + }, + status_codes={ + 201: "The project has been opened", + 404: "The project doesn't exist" + }) + def open(request, response): + + controller = Controller.instance() + project = controller.get_project(request.match_info["project_id"]) + yield from project.open() + response.set_status(201) + response.json(project) @Route.delete( r"/projects/{project_id}", diff --git a/gns3server/schemas/vpcs.py b/gns3server/schemas/vpcs.py index c85949af..283f1091 100644 --- a/gns3server/schemas/vpcs.py +++ b/gns3server/schemas/vpcs.py @@ -50,6 +50,10 @@ VPCS_CREATE_SCHEMA = { "description": "Content of the VPCS startup script", "type": ["string", "null"] }, + "startup_script_path": { + "description": "Path of the VPCS startup script relative to project directory (IGNORED)", + "type": ["string", "null"] + } }, "additionalProperties": False, "required": ["name"] diff --git a/tests/controller/test_compute.py b/tests/controller/test_compute.py index 0107d26d..7982fe8e 100644 --- a/tests/controller/test_compute.py +++ b/tests/controller/test_compute.py @@ -205,6 +205,14 @@ def test_json(compute): "user": "test", "connected": True } + assert compute.__json__(topology_dump=True) == { + "compute_id": "my_compute_id", + "name": compute.name, + "protocol": "https", + "host": "example.com", + "port": 84, + "user": "test", + } def test_streamFile(project, async_run, compute): diff --git a/tests/controller/test_link.py b/tests/controller/test_link.py index 676ecdf2..91eee08a 100644 --- a/tests/controller/test_link.py +++ b/tests/controller/test_link.py @@ -26,7 +26,7 @@ from gns3server.controller.node import Node from gns3server.controller.compute import Compute from gns3server.controller.project import Project -from tests.utils import AsyncioBytesIO +from tests.utils import AsyncioBytesIO, AsyncioMagicMock @pytest.fixture @@ -54,6 +54,7 @@ def test_addNode(async_run, project, compute): node1 = Node(project, compute, "node1") link = Link(project) + project.dump = AsyncioMagicMock() async_run(link.add_node(node1, 0, 4)) assert link._nodes == [ { @@ -62,6 +63,7 @@ def test_addNode(async_run, project, compute): "port_number": 4 } ] + assert project.dump.called def test_json(async_run, project, compute): @@ -90,6 +92,21 @@ def test_json(async_run, project, compute): "capture_file_name": None, "capture_file_path": None } + assert link.__json__(topology_dump=True) == { + "link_id": link.id, + "nodes": [ + { + "node_id": node1.id, + "adapter_number": 0, + "port_number": 4 + }, + { + "node_id": node2.id, + "adapter_number": 1, + "port_number": 3 + } + ] + } def test_start_streaming_pcap(link, async_run, tmpdir, project): diff --git a/tests/controller/test_node.py b/tests/controller/test_node.py index d67a1bfb..7f1296e4 100644 --- a/tests/controller/test_node.py +++ b/tests/controller/test_node.py @@ -70,6 +70,20 @@ def test_json(node, compute): "symbol": node.symbol, "label": node.label } + assert node.__json__(topology_dump=True) == { + "compute_id": str(compute.id), + "node_id": node.id, + "node_type": node.node_type, + "name": "demo", + "console": node.console, + "console_type": node.console_type, + "properties": node.properties, + "x": node.x, + "y": node.y, + "z": node.z, + "symbol": node.symbol, + "label": node.label + } def test_init_without_uuid(project, compute): diff --git a/tests/controller/test_project.py b/tests/controller/test_project.py index 54420c1f..1eabe5fc 100644 --- a/tests/controller/test_project.py +++ b/tests/controller/test_project.py @@ -228,3 +228,12 @@ def test_dump(): with open(os.path.join(directory, p.id, "Test.gns3")) as f: content = f.read() assert "00010203-0405-0607-0809-0a0b0c0d0e0f" in content + + +def test_open_close(async_run, controller): + project = Project(controller=controller, status="closed") + assert project.status == "closed" + async_run(project.open()) + assert project.status == "opened" + async_run(project.close()) + assert project.status == "closed" diff --git a/tests/controller/test_topology.py b/tests/controller/test_topology.py index 0aad6654..d7b27e69 100644 --- a/tests/controller/test_topology.py +++ b/tests/controller/test_topology.py @@ -59,9 +59,9 @@ def test_basic_topology(tmpdir, async_run, controller): topo = project_to_topology(project) assert len(topo["topology"]["nodes"]) == 2 - assert node1.__json__() in topo["topology"]["nodes"] - assert topo["topology"]["links"][0] == link.__json__() - assert topo["topology"]["computes"][0] == compute.__json__() + assert node1.__json__(topology_dump=True) in topo["topology"]["nodes"] + assert topo["topology"]["links"][0] == link.__json__(topology_dump=True) + assert topo["topology"]["computes"][0] == compute.__json__(topology_dump=True) def test_load_topology(tmpdir): diff --git a/tests/handlers/api/controller/test_project.py b/tests/handlers/api/controller/test_project.py index 5dc6156f..1d431f5c 100644 --- a/tests/handlers/api/controller/test_project.py +++ b/tests/handlers/api/controller/test_project.py @@ -47,6 +47,7 @@ def test_create_project_with_path(http_controller, tmpdir): assert response.status == 201 assert response.json["name"] == "test" assert response.json["project_id"] == "00010203-0405-0607-0809-0a0b0c0d0e0f" + assert response.json["status"] == "opened" def test_create_project_without_dir(http_controller): @@ -95,9 +96,15 @@ def test_delete_project_invalid_uuid(http_controller): def test_close_project(http_controller, project): with asyncio_patch("gns3server.controller.project.Project.close", return_value=True) as mock: response = http_controller.post("/projects/{project_id}/close".format(project_id=project.id), example=True) - assert response.status == 204 + assert response.status == 201 + assert mock.called + + +def test_close_project(http_controller, project): + with asyncio_patch("gns3server.controller.project.Project.open", return_value=True) as mock: + response = http_controller.post("/projects/{project_id}/open".format(project_id=project.id), example=True) + assert response.status == 201 assert mock.called - assert project not in Controller.instance().projects def test_notification(http_controller, project, controller, loop):