From 91ec61b88de2349d9ea468091d2f255d9642e433 Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Mon, 11 Jul 2016 15:36:52 +0200 Subject: [PATCH] Check topology schema when loading/saving it Fix #583 --- gns3server/controller/node.py | 2 + gns3server/controller/project.py | 5 +- gns3server/controller/topology.py | 21 ++++- gns3server/schemas/compute.py | 2 +- gns3server/schemas/topology.py | 89 +++++++++++++++++++ gns3server/web/documentation.py | 8 +- tests/controller/test_compute.py | 2 +- tests/controller/test_controller.py | 15 ++-- .../{test_shape.py => test_drawing.py} | 2 +- tests/controller/test_link.py | 20 ++--- tests/controller/test_node.py | 6 +- tests/controller/test_notification.py | 2 +- tests/controller/test_project.py | 30 +++---- tests/controller/test_topology.py | 17 +++- tests/controller/test_udp_link.py | 2 +- .../{test_shape.py => test_drawing.py} | 2 +- tests/handlers/api/controller/test_link.py | 14 +-- tests/handlers/api/controller/test_node.py | 2 +- 18 files changed, 183 insertions(+), 58 deletions(-) create mode 100644 gns3server/schemas/topology.py rename tests/controller/{test_shape.py => test_drawing.py} (97%) rename tests/handlers/api/controller/{test_shape.py => test_drawing.py} (97%) diff --git a/gns3server/controller/node.py b/gns3server/controller/node.py index 221be900..09c9746a 100644 --- a/gns3server/controller/node.py +++ b/gns3server/controller/node.py @@ -43,6 +43,8 @@ class Node: :param kwargs: Node properties """ + assert node_type + if node_id is None: self._id = str(uuid.uuid4()) else: diff --git a/gns3server/controller/project.py b/gns3server/controller/project.py index 55fec391..34ec7bba 100644 --- a/gns3server/controller/project.py +++ b/gns3server/controller/project.py @@ -47,6 +47,7 @@ class Project: def __init__(self, name=None, project_id=None, path=None, controller=None, status="opened", filename=None): self._controller = controller + assert name is not None self._name = name self._status = status if project_id is None: @@ -62,9 +63,7 @@ class Project: path = os.path.join(get_default_project_directory(), self._id) self.path = path - if filename is None and name is None: - self._filename = "project.gns3" - elif filename is not None: + if filename is not None: self._filename = filename else: self._filename = self.name + ".gns3" diff --git a/gns3server/controller/topology.py b/gns3server/controller/topology.py index e9a13ed5..0bde0721 100644 --- a/gns3server/controller/topology.py +++ b/gns3server/controller/topology.py @@ -16,13 +16,30 @@ # along with this program. If not, see . import json +import jsonschema import aiohttp from ..version import __version__ +from ..schemas.topology import TOPOLOGY_SCHEMA + +import logging +log = logging.getLogger(__name__) + GNS3_FILE_FORMAT_REVISION = 5 +def _check_topology_schema(topo): + try: + jsonschema.validate(topo, TOPOLOGY_SCHEMA) + except jsonschema.ValidationError as e: + error = "Invalid data in topology file: {} in schema: {}".format( + e.message, + json.dumps(e.schema)) + log.critical(error) + raise aiohttp.web.HTTPConflict(text=error) + + def project_to_topology(project): """ :return: A dictionnary with the topology ready to dump to a .gns3 @@ -52,7 +69,7 @@ def project_to_topology(project): for compute in computes: if hasattr(compute, "__json__"): data["topology"]["computes"].append(compute.__json__(topology_dump=True)) - #TODO: check JSON schema + _check_topology_schema(data) return data @@ -65,7 +82,7 @@ def load_topology(path): topo = json.load(f) except OSError as e: raise aiohttp.web.HTTPConflict(text="Could not load topology {}: {}".format(path, str(e))) - #TODO: Check JSON schema if topo["revision"] < GNS3_FILE_FORMAT_REVISION: raise aiohttp.web.HTTPConflict(text="Old GNS3 project are not yet supported") + _check_topology_schema(topo) return topo diff --git a/gns3server/schemas/compute.py b/gns3server/schemas/compute.py index cbb4bb62..cf0d7ff5 100644 --- a/gns3server/schemas/compute.py +++ b/gns3server/schemas/compute.py @@ -108,5 +108,5 @@ COMPUTE_OBJECT_SCHEMA = { } }, "additionalProperties": False, - "required": ["compute_id", "protocol", "host", "port", "name", "connected"] + "required": ["compute_id", "protocol", "host", "port", "name"] } diff --git a/gns3server/schemas/topology.py b/gns3server/schemas/topology.py new file mode 100644 index 00000000..5a6dda8a --- /dev/null +++ b/gns3server/schemas/topology.py @@ -0,0 +1,89 @@ +#!/usr/bin/env python +# +# Copyright (C) 2015 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 . + +# +# This file contains the validation for checking a .gns3 file +# + +from .compute import COMPUTE_OBJECT_SCHEMA +from .drawing import DRAWING_OBJECT_SCHEMA +from .link import LINK_OBJECT_SCHEMA +from .node import NODE_OBJECT_SCHEMA + + +TOPOLOGY_SCHEMA = { + "$schema": "http://json-schema.org/draft-04/schema#", + "description": "The topology", + "type": "object", + "properties": { + "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}$" + }, + "type": { + "description": "Type of file. It's always topology", + "enum": ["topology"] + }, + "revision": { + "description": "Version of the .gns3 specification.", + "type": "integer" + }, + "version": { + "description": "Version of the GNS3 software which have update the file for the last time", + "type": "string" + }, + "name": { + "type": "string", + "description": "Name of the project" + }, + "topology": { + "description": "The topology content", + "type": "object", + "properties": { + "computes": { + "description": "Computes servers", + "type": "array", + "items": COMPUTE_OBJECT_SCHEMA + }, + "drawings": { + "description": "Drawings elements", + "type": "array", + "items": DRAWING_OBJECT_SCHEMA + }, + "links": { + "description": "Link elements", + "type": "array", + "items": LINK_OBJECT_SCHEMA + }, + "nodes": { + "description": "Nodes elements", + "type": "array", + "items": NODE_OBJECT_SCHEMA + } + }, + "required": ["nodes", "links", "drawings", "computes"], + "additionalProperties": False + } + }, + "required": [ + "project_id", "type", "revision", "version", "name", "topology" + ], + "additionalProperties": False +} diff --git a/gns3server/web/documentation.py b/gns3server/web/documentation.py index 19307cbf..cfecaca5 100644 --- a/gns3server/web/documentation.py +++ b/gns3server/web/documentation.py @@ -17,13 +17,14 @@ import re import os.path +import json import os from gns3server.handlers import * from gns3server.web.route import Route -class Documentation(object): +class Documentation: """Extract API documentation as Sphinx compatible files""" @@ -36,6 +37,11 @@ class Documentation(object): self._directory = directory def write(self): + with open(os.path.join(self._directory, "gns3_file.json"), "w+") as f: + from gns3server.schemas.topology import TOPOLOGY_SCHEMA + print("Dump .gns3 schema") + print(TOPOLOGY_SCHEMA) + json.dump(TOPOLOGY_SCHEMA, f, indent=4) self.write_documentation("compute") # Controller documentation self.write_documentation("controller") diff --git a/tests/controller/test_compute.py b/tests/controller/test_compute.py index e229ae69..2b0750de 100644 --- a/tests/controller/test_compute.py +++ b/tests/controller/test_compute.py @@ -160,7 +160,7 @@ def test_compute_httpQuery_project(compute, async_run): with asyncio_patch("aiohttp.ClientSession.request", return_value=response) as mock: response.status = 200 - project = Project() + project = Project(name="Test") async_run(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=False) diff --git a/tests/controller/test_controller.py b/tests/controller/test_controller.py index 2c194d26..d791cab0 100644 --- a/tests/controller/test_controller.py +++ b/tests/controller/test_controller.py @@ -168,15 +168,15 @@ def test_initControllerLocal(controller, controller_config_path, async_run): assert len(c._computes) == 1 -def test_addProject(controller, async_run): +def test_add_project(controller, async_run): uuid1 = str(uuid.uuid4()) uuid2 = str(uuid.uuid4()) - async_run(controller.add_project(project_id=uuid1)) + async_run(controller.add_project(project_id=uuid1, name="Test")) assert len(controller.projects) == 1 - async_run(controller.add_project(project_id=uuid1)) + async_run(controller.add_project(project_id=uuid1, name="Test")) assert len(controller.projects) == 1 - async_run(controller.add_project(project_id=uuid2)) + async_run(controller.add_project(project_id=uuid2, name="Test 2")) assert len(controller.projects) == 2 @@ -193,7 +193,7 @@ def test_addDuplicateProject(controller, async_run): def test_remove_project(controller, async_run): uuid1 = str(uuid.uuid4()) - project1 = async_run(controller.add_project(project_id=uuid1)) + project1 = async_run(controller.add_project(project_id=uuid1, name="Test")) assert len(controller.projects) == 1 controller.remove_project(project1) @@ -207,13 +207,13 @@ def test_addProject_with_compute(controller, async_run): compute.post = MagicMock() controller._computes = {"test1": compute} - project1 = async_run(controller.add_project(project_id=uuid1)) + project1 = async_run(controller.add_project(project_id=uuid1, name="Test")) def test_getProject(controller, async_run): uuid1 = str(uuid.uuid4()) - project = async_run(controller.add_project(project_id=uuid1)) + project = async_run(controller.add_project(project_id=uuid1, name="Test")) assert controller.get_project(uuid1) == project with pytest.raises(aiohttp.web.HTTPNotFound): assert controller.get_project("dsdssd") @@ -234,6 +234,7 @@ def test_load_project(controller, async_run, tmpdir): "type": "topology", "version": "2.0.0dev1", "topology": { + "drawings": [], "computes": [ { "compute_id": "my_remote", diff --git a/tests/controller/test_shape.py b/tests/controller/test_drawing.py similarity index 97% rename from tests/controller/test_shape.py rename to tests/controller/test_drawing.py index 2d9186ee..70bfc580 100644 --- a/tests/controller/test_shape.py +++ b/tests/controller/test_drawing.py @@ -28,7 +28,7 @@ from gns3server.controller.project import Project @pytest.fixture def project(controller, async_run): - return async_run(controller.add_project()) + return async_run(controller.add_project(name="Test")) @pytest.fixture diff --git a/tests/controller/test_link.py b/tests/controller/test_link.py index 28200e6a..94570a92 100644 --- a/tests/controller/test_link.py +++ b/tests/controller/test_link.py @@ -31,7 +31,7 @@ from tests.utils import AsyncioBytesIO, AsyncioMagicMock @pytest.fixture def project(controller): - return Project(controller=controller) + return Project(controller=controller, name="Test") @pytest.fixture @@ -41,8 +41,8 @@ def compute(): @pytest.fixture def link(async_run, project, compute): - node1 = Node(project, compute, "node1") - node2 = Node(project, compute, "node2") + node1 = Node(project, compute, "node1", node_type="qemu") + node2 = Node(project, compute, "node2", node_type="qemu") link = Link(project) async_run(link.add_node(node1, 0, 4)) @@ -57,7 +57,7 @@ def test_eq(project, link, controller): def test_add_node(async_run, project, compute): - node1 = Node(project, compute, "node1") + node1 = Node(project, compute, "node1", node_type="qemu") link = Link(project) link._project.controller.notification.emit = MagicMock() @@ -81,14 +81,14 @@ def test_add_node(async_run, project, compute): assert not link._project.controller.notification.emit.called # We call link.created only when both side are created - node2 = Node(project, compute, "node2") + node2 = Node(project, compute, "node2", node_type="qemu") async_run(link.add_node(node2, 0, 4)) link._project.controller.notification.emit.assert_called_with("link.created", link.__json__()) def test_update_nodes(async_run, project, compute): - node1 = Node(project, compute, "node1") + node1 = Node(project, compute, "node1", node_type="qemu") project._nodes[node1.id] = node1 link = Link(project) @@ -109,8 +109,8 @@ def test_update_nodes(async_run, project, compute): def test_json(async_run, project, compute): - node1 = Node(project, compute, "node1") - node2 = Node(project, compute, "node2") + node1 = Node(project, compute, "node1", node_type="qemu") + node2 = Node(project, compute, "node2", node_type="qemu") link = Link(project) async_run(link.add_node(node1, 0, 4)) @@ -197,8 +197,8 @@ def test_start_streaming_pcap(link, async_run, tmpdir, project): def test_default_capture_file_name(project, compute, async_run): - node1 = Node(project, compute, "Hello@") - node2 = Node(project, compute, "w0.rld") + node1 = Node(project, compute, "Hello@", node_type="qemu") + node2 = Node(project, compute, "w0.rld", node_type="qemu") link = Link(project) async_run(link.add_node(node1, 0, 4)) diff --git a/tests/controller/test_node.py b/tests/controller/test_node.py index 6f4eb8c6..5b525feb 100644 --- a/tests/controller/test_node.py +++ b/tests/controller/test_node.py @@ -51,10 +51,10 @@ def node(compute, project): def test_eq(compute, project, node, controller): - assert node == Node(project, compute, "demo", node_id=node.id) + assert node == Node(project, compute, "demo", node_id=node.id, node_type="qemu") assert node != "a" - assert node != Node(project, compute, "demo", node_id=str(uuid.uuid4())) - assert node != Node( Project(str(uuid.uuid4()), controller=controller), compute, "demo", node_id=node.id) + assert node != Node(project, compute, "demo", node_id=str(uuid.uuid4()), node_type="qemu") + assert node != Node(Project(str(uuid.uuid4()), controller=controller), compute, "demo", node_id=node.id, node_type="qemu") def test_json(node, compute): diff --git a/tests/controller/test_notification.py b/tests/controller/test_notification.py index 3bfe7bf7..1c0e6462 100644 --- a/tests/controller/test_notification.py +++ b/tests/controller/test_notification.py @@ -25,7 +25,7 @@ from tests.utils import AsyncioMagicMock @pytest.fixture def project(async_run): - return async_run(Controller.instance().add_project()) + return async_run(Controller.instance().add_project(name="Test")) @pytest.fixture diff --git a/tests/controller/test_project.py b/tests/controller/test_project.py index 8a236ae3..7a7c3180 100644 --- a/tests/controller/test_project.py +++ b/tests/controller/test_project.py @@ -30,20 +30,20 @@ from gns3server.config import Config @pytest.fixture def project(controller): - return Project(controller=controller) + return Project(controller=controller, name="Test") def test_affect_uuid(): - p = Project() + p = Project(name="Test") assert len(p.id) == 36 - p = Project(project_id='00010203-0405-0607-0809-0a0b0c0d0e0f') + p = Project(project_id='00010203-0405-0607-0809-0a0b0c0d0e0f', name="Test 2") assert p.id == '00010203-0405-0607-0809-0a0b0c0d0e0f' def test_json(tmpdir): - p = Project() - assert p.__json__() == {"name": p.name, "project_id": p.id, "path": p.path, "status": "opened", "filename": "project.gns3"} + p = Project(name="Test") + assert p.__json__() == {"name": "Test", "project_id": p.id, "path": p.path, "status": "opened", "filename": "Test.gns3"} def test_path(tmpdir): @@ -51,25 +51,25 @@ def test_path(tmpdir): directory = Config.instance().get_section_config("Server").get("projects_path") with patch("gns3server.utils.path.get_default_project_directory", return_value=directory): - p = Project(project_id=str(uuid4())) + p = Project(project_id=str(uuid4()), name="Test") assert p.path == os.path.join(directory, p.id) assert os.path.exists(os.path.join(directory, p.id)) def test_init_path(tmpdir): - p = Project(path=str(tmpdir), project_id=str(uuid4())) + p = Project(path=str(tmpdir), project_id=str(uuid4()), name="Test") assert p.path == str(tmpdir) def test_changing_path_with_quote_not_allowed(tmpdir): with pytest.raises(aiohttp.web.HTTPForbidden): - p = Project(project_id=str(uuid4())) + p = Project(project_id=str(uuid4()), name="Test") p.path = str(tmpdir / "project\"53") def test_captures_directory(tmpdir): - p = Project(path=str(tmpdir)) + p = Project(path=str(tmpdir), name="Test") assert p.captures_directory == str(tmpdir / "project-files" / "captures") assert os.path.exists(p.captures_directory) @@ -80,7 +80,7 @@ def test_add_node_local(async_run, controller): """ compute = MagicMock() compute.id = "local" - project = Project(controller=controller) + project = Project(controller=controller, name="Test") controller._notification = MagicMock() response = MagicMock() @@ -109,7 +109,7 @@ def test_add_node_non_local(async_run, controller): """ compute = MagicMock() compute.id = "remote" - project = Project(controller=controller) + project = Project(controller=controller, name="Test") controller._notification = MagicMock() response = MagicMock() @@ -135,7 +135,7 @@ def test_delete_node(async_run, controller): For a local server we send the project path """ compute = MagicMock() - project = Project(controller=controller) + project = Project(controller=controller, name="Test") controller._notification = MagicMock() response = MagicMock() @@ -156,7 +156,7 @@ def test_delete_node_delete_link(async_run, controller): Delete a node delete all the node connected """ compute = MagicMock() - project = Project(controller=controller) + project = Project(controller=controller, name="Test") controller._notification = MagicMock() response = MagicMock() @@ -179,7 +179,7 @@ def test_delete_node_delete_link(async_run, controller): def test_getVM(async_run, controller): compute = MagicMock() - project = Project(controller=controller) + project = Project(controller=controller, name="Test") response = MagicMock() response.json = {"console": 2048} @@ -283,7 +283,7 @@ def test_dump(): def test_open_close(async_run, controller): - project = Project(controller=controller, status="closed") + project = Project(controller=controller, status="closed", name="Test") assert project.status == "closed" async_run(project.open()) assert project.status == "opened" diff --git a/tests/controller/test_topology.py b/tests/controller/test_topology.py index c19ece60..899c759a 100644 --- a/tests/controller/test_topology.py +++ b/tests/controller/test_topology.py @@ -16,6 +16,7 @@ # along with this program. If not, see . import json +import uuid import pytest import aiohttp from unittest.mock import MagicMock @@ -23,7 +24,7 @@ from tests.utils import asyncio_patch from gns3server.controller.project import Project from gns3server.controller.compute import Compute -from gns3server.controller.topology import project_to_topology, load_topology +from gns3server.controller.topology import project_to_topology, load_topology, GNS3_FILE_FORMAT_REVISION from gns3server.version import __version__ @@ -51,8 +52,8 @@ def test_basic_topology(tmpdir, async_run, controller): compute.http_query = MagicMock() with asyncio_patch("gns3server.controller.node.Node.create"): - node1 = async_run(project.add_node(compute, "Node 1", "node_1")) - node2 = async_run(project.add_node(compute, "Node 2", "node_2")) + node1 = async_run(project.add_node(compute, "Node 1", str(uuid.uuid4()), node_type="qemu")) + node2 = async_run(project.add_node(compute, "Node 2", str(uuid.uuid4()), node_type="qemu")) link = async_run(project.add_link()) async_run(link.add_node(node1, 0, 0)) @@ -95,6 +96,16 @@ def test_load_topology_file_error(tmpdir): topo = load_topology(path) +def test_load_topology_file_error_schema_error(tmpdir): + path = str(tmpdir / "test.gns3") + with open(path, "w+") as f: + json.dump({ + "revision": GNS3_FILE_FORMAT_REVISION + }, f) + with pytest.raises(aiohttp.web.HTTPConflict): + topo = load_topology(path) + + def test_load_old_topology(tmpdir): data = { "project_id": "69f26504-7aa3-48aa-9f29-798d44841211", diff --git a/tests/controller/test_udp_link.py b/tests/controller/test_udp_link.py index 2d7cff66..c3f71253 100644 --- a/tests/controller/test_udp_link.py +++ b/tests/controller/test_udp_link.py @@ -28,7 +28,7 @@ from gns3server.controller.node import Node @pytest.fixture def project(controller): - return Project(controller=controller) + return Project(controller=controller, name="Test") def test_create(async_run, project): diff --git a/tests/handlers/api/controller/test_shape.py b/tests/handlers/api/controller/test_drawing.py similarity index 97% rename from tests/handlers/api/controller/test_shape.py rename to tests/handlers/api/controller/test_drawing.py index 3f0c02e5..ca5f899b 100644 --- a/tests/handlers/api/controller/test_shape.py +++ b/tests/handlers/api/controller/test_drawing.py @@ -35,7 +35,7 @@ from gns3server.controller.drawing import Drawing @pytest.fixture def project(http_controller, async_run): - return async_run(Controller.instance().add_project()) + return async_run(Controller.instance().add_project(name="Test")) def test_create_drawing(http_controller, tmpdir, project, async_run): diff --git a/tests/handlers/api/controller/test_link.py b/tests/handlers/api/controller/test_link.py index 67780d28..371a76fb 100644 --- a/tests/handlers/api/controller/test_link.py +++ b/tests/handlers/api/controller/test_link.py @@ -45,7 +45,7 @@ def compute(http_controller, async_run): @pytest.fixture def project(http_controller, async_run): - return async_run(Controller.instance().add_project()) + return async_run(Controller.instance().add_project(name="Test")) def test_create_link(http_controller, tmpdir, project, compute, async_run): @@ -53,8 +53,8 @@ def test_create_link(http_controller, tmpdir, project, compute, async_run): 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)) + node1 = async_run(project.add_node(compute, "node1", None, node_type="qemu")) + node2 = async_run(project.add_node(compute, "node2", None, node_type="qemu")) with asyncio_patch("gns3server.controller.udp_link.UDPLink.create") as mock: response = http_controller.post("/projects/{}/links".format(project.id), { @@ -88,8 +88,8 @@ def test_update_link(http_controller, tmpdir, project, compute, async_run): 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)) + node1 = async_run(project.add_node(compute, "node1", None, node_type="qemu")) + node2 = async_run(project.add_node(compute, "node2", None, node_type="qemu")) with asyncio_patch("gns3server.controller.udp_link.UDPLink.create") as mock: response = http_controller.post("/projects/{}/links".format(project.id), { @@ -141,8 +141,8 @@ def test_list_link(http_controller, tmpdir, project, compute, async_run): 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)) + node1 = async_run(project.add_node(compute, "node1", None, node_type="qemu")) + node2 = async_run(project.add_node(compute, "node2", None, node_type="qemu")) with asyncio_patch("gns3server.controller.udp_link.UDPLink.create") as mock: response = http_controller.post("/projects/{}/links".format(project.id), { diff --git a/tests/handlers/api/controller/test_node.py b/tests/handlers/api/controller/test_node.py index d860dcfc..81d54458 100644 --- a/tests/handlers/api/controller/test_node.py +++ b/tests/handlers/api/controller/test_node.py @@ -45,7 +45,7 @@ def compute(http_controller, async_run): @pytest.fixture def project(http_controller, async_run): - return async_run(Controller.instance().add_project()) + return async_run(Controller.instance().add_project(name="Test")) @pytest.fixture